Layout 100%, Notificações, SetupWizard
This commit is contained in:
@@ -267,94 +267,81 @@ const loading = computed(() => loadingF.value || loadingB.value)
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<!-- ── Cabeçalho do ano ─────────────────────────────────── -->
|
||||
<div class="flex flex-wrap items-center justify-between gap-3 rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] px-5 py-4">
|
||||
<div>
|
||||
<div class="font-semibold text-base">Bloqueios da agenda</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">
|
||||
Feriados e períodos em que não é possível agendar com pacientes.
|
||||
</div>
|
||||
<!-- Subheader degradê -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-ban" /></div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="cfg-subheader__title">Bloqueios</div>
|
||||
<div class="cfg-subheader__sub">Feriados e períodos em que não é possível agendar com pacientes</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Button icon="pi pi-chevron-left" text rounded severity="secondary" @click="anoAnterior" />
|
||||
<span class="font-bold text-lg w-14 text-center">{{ ano }}</span>
|
||||
<Button icon="pi pi-chevron-right" text rounded severity="secondary" @click="anoProximo" />
|
||||
<!-- Nav de ano -->
|
||||
<div class="flex items-center gap-1 shrink-0 relative z-10">
|
||||
<Button icon="pi pi-chevron-left" text rounded size="small" severity="secondary" @click="anoAnterior" />
|
||||
<span class="font-bold text-sm w-12 text-center text-[var(--primary-color)]">{{ ano }}</span>
|
||||
<Button icon="pi pi-chevron-right" text rounded size="small" severity="secondary" @click="anoProximo" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Stats rápidos ────────────────────────────────────── -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4 text-center">
|
||||
<div class="text-2xl font-bold text-blue-500">{{ nacionais.length }}</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] mt-1">Feriados nacionais</div>
|
||||
<!-- Stats + ações -->
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<div class="blk-stat blk-stat--blue">
|
||||
<div class="blk-stat__value">{{ nacionais.length }}</div>
|
||||
<div class="blk-stat__label">Nacionais</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4 text-center">
|
||||
<div class="text-2xl font-bold text-orange-500">{{ municipais.length }}</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] mt-1">Feriados municipais</div>
|
||||
<div class="blk-stat blk-stat--orange">
|
||||
<div class="blk-stat__value">{{ municipais.length }}</div>
|
||||
<div class="blk-stat__label">Municipais</div>
|
||||
</div>
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] p-4 text-center">
|
||||
<div class="text-2xl font-bold text-red-500">{{ bloqueios.length }}</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] mt-1">Bloqueios</div>
|
||||
<div class="blk-stat blk-stat--red">
|
||||
<div class="blk-stat__value">{{ bloqueios.length }}</div>
|
||||
<div class="blk-stat__label">Bloqueios</div>
|
||||
</div>
|
||||
<div class="ml-auto flex gap-2">
|
||||
<Button icon="pi pi-map-marker" label="Feriado municipal" severity="secondary" outlined class="rounded-full" size="small" @click="abrirFeriadoMunicipal" />
|
||||
<Button icon="pi pi-ban" label="Novo bloqueio" class="rounded-full" size="small" @click="abrirAddBloqueio" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Ações ─────────────────────────────────────────────── -->
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Button
|
||||
icon="pi pi-map-marker"
|
||||
label="Adicionar feriado municipal"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
@click="abrirFeriadoMunicipal"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-ban"
|
||||
label="Adicionar bloqueio"
|
||||
class="rounded-full"
|
||||
@click="abrirAddBloqueio"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<!-- ── Loading ────────────────────────────────────────────── -->
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="flex items-center justify-center py-16">
|
||||
<i class="pi pi-spinner pi-spin text-2xl opacity-40" />
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- ── Feriados Nacionais (somente leitura) ─────────────── -->
|
||||
<!-- Feriados Nacionais -->
|
||||
<div class="blk-group">
|
||||
<div class="blk-group__head">
|
||||
<i class="pi pi-flag text-blue-500" />
|
||||
<div class="blk-group__head-icon" style="background:color-mix(in srgb,#3b82f6 12%,transparent);color:#3b82f6">
|
||||
<i class="pi pi-flag" />
|
||||
</div>
|
||||
<span>Feriados Nacionais</span>
|
||||
<span class="blk-group__count">{{ nacionais.length }}</span>
|
||||
<span class="ml-auto mr-0 text-xs text-[var(--text-color-secondary)] font-normal">gerado automaticamente</span>
|
||||
<span class="ml-auto text-xs text-[var(--text-color-secondary)] opacity-60 font-normal">gerado automaticamente</span>
|
||||
</div>
|
||||
|
||||
<div class="blk-list">
|
||||
<div v-for="f in nacionais" :key="f.data + f.nome" class="blk-item">
|
||||
<div class="blk-item__date">{{ fmtDateShort(f.data) }}</div>
|
||||
<div class="blk-item__title">{{ f.nome }}</div>
|
||||
<Tag v-if="f.movel" value="Móvel" severity="secondary" class="text-xs shrink-0" />
|
||||
<Tag v-if="f.movel" value="Móvel" severity="secondary" class="text-xs shrink-0 ml-auto" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Feriados Municipais ──────────────────────────────── -->
|
||||
<!-- Feriados Municipais -->
|
||||
<div class="blk-group">
|
||||
<div class="blk-group__head">
|
||||
<i class="pi pi-map-marker text-orange-500" />
|
||||
<div class="blk-group__head-icon" style="background:color-mix(in srgb,#f97316 12%,transparent);color:#f97316">
|
||||
<i class="pi pi-map-marker" />
|
||||
</div>
|
||||
<span>Feriados Municipais</span>
|
||||
<span class="blk-group__count">{{ municipais.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!municipais.length" class="blk-empty">
|
||||
Nenhum feriado municipal cadastrado para {{ ano }}.
|
||||
</div>
|
||||
|
||||
<div v-else class="blk-list">
|
||||
<div v-for="f in municipais" :key="f.id" class="blk-item">
|
||||
<div class="blk-item__date">{{ fmtDate(f.data) }}</div>
|
||||
@@ -367,23 +354,23 @@ const loading = computed(() => loadingF.value || loadingB.value)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── Bloqueios ─────────────────────────────────────────── -->
|
||||
<!-- Bloqueios -->
|
||||
<div class="blk-group">
|
||||
<div class="blk-group__head">
|
||||
<i class="pi pi-ban text-red-500" />
|
||||
<div class="blk-group__head-icon" style="background:color-mix(in srgb,#ef4444 12%,transparent);color:#ef4444">
|
||||
<i class="pi pi-ban" />
|
||||
</div>
|
||||
<span>Bloqueios</span>
|
||||
<span class="blk-group__count">{{ bloqueios.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!bloqueios.length" class="blk-empty">
|
||||
Nenhum bloqueio cadastrado para {{ ano }}.
|
||||
</div>
|
||||
|
||||
<div v-else class="blk-list">
|
||||
<div v-for="b in bloqueios" :key="b.id" class="blk-item">
|
||||
<div class="blk-item__date">{{ fmtPeriodo(b) }}</div>
|
||||
<div class="blk-item__title">{{ b.titulo }}</div>
|
||||
<Tag v-if="b.recorrente" value="Recorrente" severity="warn" class="text-xs" />
|
||||
<Tag v-if="b.recorrente" value="Recorrente" severity="warn" class="text-xs shrink-0" />
|
||||
<div v-if="b.observacao" class="blk-item__obs">{{ b.observacao }}</div>
|
||||
<div class="blk-item__actions">
|
||||
<Button icon="pi pi-pencil" text rounded size="small" severity="secondary" @click="abrirEditBloqueio(b)" />
|
||||
@@ -396,222 +383,180 @@ const loading = computed(() => loadingF.value || loadingB.value)
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<!-- ══ Dialog feriado municipal ══════════════════════════════ -->
|
||||
<Dialog
|
||||
v-model:visible="fdlgOpen"
|
||||
modal
|
||||
:draggable="false"
|
||||
header="Cadastrar feriado municipal"
|
||||
:style="{ width: '420px' }"
|
||||
>
|
||||
<!-- Dialog feriado municipal -->
|
||||
<Dialog v-model:visible="fdlgOpen" modal :draggable="false" pt:mask:class="backdrop-blur-xs" header="Cadastrar feriado municipal" :style="{ width: '420px', maxWidth: '95vw' }">
|
||||
<div class="flex flex-col gap-4 pt-1">
|
||||
|
||||
<div>
|
||||
<label class="blk-label">Nome do feriado *</label>
|
||||
<InputText v-model="fform.nome" class="w-full mt-1" placeholder="Ex.: Aniversário da cidade, Padroeiro…" />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="blk-label">Data *</label>
|
||||
<DatePicker
|
||||
v-model="fform.data"
|
||||
showIcon fluid iconDisplay="input"
|
||||
dateFormat="dd/mm/yy"
|
||||
:manualInput="false"
|
||||
class="mt-1"
|
||||
>
|
||||
<DatePicker v-model="fform.data" showIcon fluid iconDisplay="input" dateFormat="dd/mm/yy" :manualInput="false" class="mt-1">
|
||||
<template #inputicon="sp"><i class="pi pi-calendar" @click="sp.clickCallback" /></template>
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="blk-label">Observação <span class="opacity-60">(opcional)</span></label>
|
||||
<Textarea v-model="fform.observacao" class="w-full mt-1" rows="2" autoResize placeholder="Nota interna…" />
|
||||
</div>
|
||||
|
||||
<div v-if="fform.data && fform.nome && isDuplicata(dateToISO(fform.data), fform.nome)"
|
||||
class="text-sm text-red-500 flex items-center gap-2">
|
||||
<i class="pi pi-exclamation-triangle" />
|
||||
Já existe um feriado com esse nome nessa data.
|
||||
<div v-if="fform.data && fform.nome && isDuplicata(dateToISO(fform.data), fform.nome)" class="text-sm text-red-500 flex items-center gap-2">
|
||||
<i class="pi pi-exclamation-triangle" /> Já existe um feriado com esse nome nessa data.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="fdlgOpen = false" />
|
||||
<Button
|
||||
label="Cadastrar"
|
||||
icon="pi pi-check"
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="fdlgOpen = false" />
|
||||
<Button label="Cadastrar" icon="pi pi-check" class="rounded-full"
|
||||
:disabled="!fformValid || (fform.data && fform.nome && isDuplicata(dateToISO(fform.data), fform.nome))"
|
||||
:loading="fsaving"
|
||||
@click="salvarFeriado"
|
||||
/>
|
||||
:loading="fsaving" @click="salvarFeriado" />
|
||||
</template>
|
||||
</Dialog>
|
||||
|
||||
<!-- ══ Dialog bloqueio add/edit ══════════════════════════════ -->
|
||||
<Dialog
|
||||
v-model:visible="dlgOpen"
|
||||
modal
|
||||
:draggable="false"
|
||||
<!-- Dialog bloqueio -->
|
||||
<Dialog v-model:visible="dlgOpen" modal :draggable="false" pt:mask:class="backdrop-blur-xs"
|
||||
:header="dlgMode === 'edit' ? 'Editar bloqueio' : 'Novo bloqueio'"
|
||||
:style="{ width: '480px' }"
|
||||
>
|
||||
:style="{ width: '480px', maxWidth: '95vw' }">
|
||||
<div class="flex flex-col gap-4 pt-1">
|
||||
|
||||
<div>
|
||||
<label class="blk-label">Título *</label>
|
||||
<InputText v-model="form.titulo" class="w-full mt-1" placeholder="Ex.: Recesso, Férias, Licença…" />
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="blk-label">Data início *</label>
|
||||
<DatePicker
|
||||
v-model="form.data_inicio"
|
||||
showIcon fluid iconDisplay="input"
|
||||
dateFormat="dd/mm/yy"
|
||||
:manualInput="false"
|
||||
class="mt-1"
|
||||
>
|
||||
<DatePicker v-model="form.data_inicio" showIcon fluid iconDisplay="input" dateFormat="dd/mm/yy" :manualInput="false" class="mt-1">
|
||||
<template #inputicon="sp"><i class="pi pi-calendar" @click="sp.clickCallback" /></template>
|
||||
</DatePicker>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="blk-label">Data fim <span class="opacity-60">(opcional)</span></label>
|
||||
<DatePicker
|
||||
v-model="form.data_fim"
|
||||
showIcon fluid iconDisplay="input"
|
||||
dateFormat="dd/mm/yy"
|
||||
:manualInput="false"
|
||||
:minDate="form.data_inicio || undefined"
|
||||
class="mt-1"
|
||||
>
|
||||
<DatePicker v-model="form.data_fim" showIcon fluid iconDisplay="input" dateFormat="dd/mm/yy" :manualInput="false" :minDate="form.data_inicio || undefined" class="mt-1">
|
||||
<template #inputicon="sp"><i class="pi pi-calendar" @click="sp.clickCallback" /></template>
|
||||
</DatePicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex gap-3">
|
||||
<div class="flex-1">
|
||||
<label class="blk-label">Hora início <span class="opacity-60">(opcional)</span></label>
|
||||
<DatePicker
|
||||
v-model="form.hora_inicio"
|
||||
showIcon fluid iconDisplay="input"
|
||||
timeOnly hourFormat="24" :stepMinute="15" :manualInput="false"
|
||||
class="mt-1"
|
||||
>
|
||||
<DatePicker v-model="form.hora_inicio" showIcon fluid iconDisplay="input" timeOnly hourFormat="24" :stepMinute="15" :manualInput="false" class="mt-1">
|
||||
<template #inputicon="sp"><i class="pi pi-clock" @click="sp.clickCallback" /></template>
|
||||
</DatePicker>
|
||||
</div>
|
||||
<div class="flex-1">
|
||||
<label class="blk-label">Hora fim</label>
|
||||
<DatePicker
|
||||
v-model="form.hora_fim"
|
||||
showIcon fluid iconDisplay="input"
|
||||
timeOnly hourFormat="24" :stepMinute="15" :manualInput="false"
|
||||
class="mt-1"
|
||||
>
|
||||
<label class="blk-label">Hora fim <span class="opacity-60">(opcional)</span></label>
|
||||
<DatePicker v-model="form.hora_fim" showIcon fluid iconDisplay="input" timeOnly hourFormat="24" :stepMinute="15" :manualInput="false" class="mt-1">
|
||||
<template #inputicon="sp"><i class="pi pi-clock" @click="sp.clickCallback" /></template>
|
||||
</DatePicker>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="blk-label">Observação <span class="opacity-60">(opcional)</span></label>
|
||||
<Textarea v-model="form.observacao" class="w-full mt-1" rows="2" autoResize placeholder="Nota interna…" />
|
||||
<Textarea v-model="form.observacao" class="w-full mt-1" rows="2" autoResize />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template #footer>
|
||||
<Button label="Cancelar" icon="pi pi-times" severity="secondary" outlined @click="dlgOpen = false" />
|
||||
<Button
|
||||
:label="dlgMode === 'edit' ? 'Salvar' : 'Adicionar'"
|
||||
icon="pi pi-check"
|
||||
:disabled="!formValid"
|
||||
:loading="saving"
|
||||
@click="salvarBloqueio"
|
||||
/>
|
||||
<Button label="Cancelar" icon="pi pi-times" severity="secondary" outlined class="rounded-full" @click="dlgOpen = false" />
|
||||
<Button :label="dlgMode === 'edit' ? 'Salvar' : 'Adicionar'" icon="pi pi-check" class="rounded-full"
|
||||
:disabled="!formValid" :loading="saving" @click="salvarBloqueio" />
|
||||
</template>
|
||||
</Dialog>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Grupos ──────────────────────────────────────────────── */
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute;
|
||||
top: -20px; right: -20px; width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
|
||||
color: var(--primary-color, #6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; color: var(--primary-color, #6366f1); letter-spacing: -0.01em; }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
|
||||
/* ── Stats ────────────────────────────────────────── */
|
||||
.blk-stat {
|
||||
display: flex; flex-direction: column; gap: 0.1rem;
|
||||
padding: 0.5rem 0.875rem; border-radius: 6px;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-card); min-width: 72px;
|
||||
}
|
||||
.blk-stat__value { font-size: 1.35rem; font-weight: 700; line-height: 1; }
|
||||
.blk-stat__label { font-size: 0.7rem; color: var(--text-color-secondary); opacity: 0.75; }
|
||||
.blk-stat--blue .blk-stat__value { color: #3b82f6; }
|
||||
.blk-stat--orange .blk-stat__value { color: #f97316; }
|
||||
.blk-stat--red .blk-stat__value { color: #ef4444; }
|
||||
|
||||
/* ── Grupos ──────────────────────────────────────── */
|
||||
.blk-group {
|
||||
border-radius: 1.25rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.blk-group__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.875rem 1.25rem;
|
||||
display: flex; align-items: center; gap: 0.5rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
font-weight: 600;
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600; font-size: 0.88rem;
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.blk-group__head-icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.blk-group__count {
|
||||
font-size: 0.75rem;
|
||||
background: var(--surface-ground);
|
||||
font-size: 0.7rem;
|
||||
background: var(--surface-card);
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 999px;
|
||||
padding: 1px 8px;
|
||||
border-radius: 999px; padding: 1px 8px;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
|
||||
/* ── Itens ───────────────────────────────────────────────── */
|
||||
/* ── Itens ──────────────────────────────────────── */
|
||||
.blk-empty {
|
||||
padding: 1.25rem;
|
||||
font-size: 0.875rem;
|
||||
color: var(--text-color-secondary);
|
||||
}
|
||||
.blk-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.blk-list { display: flex; flex-direction: column; }
|
||||
.blk-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.75rem 1.25rem;
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.625rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: wrap; transition: background 0.1s;
|
||||
}
|
||||
.blk-item:last-child { border-bottom: none; }
|
||||
.blk-item:hover { background: var(--surface-hover); }
|
||||
|
||||
.blk-item__date {
|
||||
font-size: 0.8rem;
|
||||
color: var(--text-color-secondary);
|
||||
white-space: nowrap;
|
||||
min-width: 5.5rem;
|
||||
font-size: 0.75rem; color: var(--text-color-secondary);
|
||||
white-space: nowrap; min-width: 5.5rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.blk-item__title {
|
||||
flex: 1;
|
||||
font-weight: 500;
|
||||
font-size: 0.875rem;
|
||||
min-width: 0;
|
||||
}
|
||||
.blk-item__title { flex: 1; font-weight: 500; font-size: 0.85rem; min-width: 0; }
|
||||
.blk-item__obs {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-color-secondary);
|
||||
width: 100%;
|
||||
padding-left: 6.25rem;
|
||||
margin-top: -0.25rem;
|
||||
}
|
||||
.blk-item__actions {
|
||||
display: flex;
|
||||
gap: 0.25rem;
|
||||
margin-left: auto;
|
||||
font-size: 0.72rem; color: var(--text-color-secondary);
|
||||
width: 100%; padding-left: 6.25rem; margin-top: -0.25rem;
|
||||
}
|
||||
.blk-item__actions { display: flex; gap: 0.25rem; margin-left: auto; }
|
||||
|
||||
/* ── Dialog ──────────────────────────────────────────────── */
|
||||
.blk-label {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-color-secondary);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
/* ── Dialog labels ──────────────────────────────── */
|
||||
.blk-label { font-size: 0.75rem; color: var(--text-color-secondary); font-weight: 500; }
|
||||
</style>
|
||||
@@ -796,6 +796,15 @@ const jornadaEndDate = computed({
|
||||
<!-- ══ COLUNA ESQUERDA: CARDS ══════════════════════════════ -->
|
||||
<div class="flex flex-col gap-3 xl:w-[58%]">
|
||||
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<i class="pi pi-calendar cfg-subheader__icon" />
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Agenda</div>
|
||||
<div class="cfg-subheader__sub">Horários semanais, duração e intervalo padrão</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD 1: JORNADA ─────────────────────────────────── -->
|
||||
<div class="cfg-card" :class="{ 'cfg-card--open': expandedCard === 'jornada' }">
|
||||
|
||||
@@ -952,7 +961,7 @@ const jornadaEndDate = computed({
|
||||
<div
|
||||
v-for="d in selectedDays"
|
||||
:key="d.value"
|
||||
class="flex items-center gap-3 p-2 rounded-xl bg-[var(--surface-ground)]"
|
||||
class="flex items-center gap-3 p-2 rounded-[6px] bg-[var(--surface-ground)]"
|
||||
>
|
||||
<span class="w-10 text-sm font-medium">{{ d.short }}</span>
|
||||
<div class="w-32">
|
||||
@@ -1060,7 +1069,7 @@ const jornadaEndDate = computed({
|
||||
</div>
|
||||
|
||||
<!-- Campos manuais (personalizado) -->
|
||||
<div v-if="showAdvancedRitmo || !durationPresets.some(p => isActivePreset(p))" class="mb-5 p-4 rounded-2xl bg-[var(--surface-ground)]">
|
||||
<div v-if="showAdvancedRitmo || !durationPresets.some(p => isActivePreset(p))" class="mb-5 p-4 rounded-[6px] bg-[var(--surface-ground)]">
|
||||
<div class="cfg-label mb-3">Personalizado</div>
|
||||
<div class="flex flex-row gap-6">
|
||||
<div class="flex flex-col gap-1">
|
||||
@@ -1121,7 +1130,7 @@ const jornadaEndDate = computed({
|
||||
<div class="border-t border-[var(--surface-border)] pt-4">
|
||||
|
||||
<!-- Aviso slots órfãos -->
|
||||
<div v-if="orphanSlotDays.length" class="flex items-start gap-2 mb-4 p-3 rounded-xl bg-[var(--yellow-50)] border border-[var(--yellow-200)] text-sm text-[var(--yellow-800)]">
|
||||
<div v-if="orphanSlotDays.length" class="flex items-start gap-2 mb-4 p-3 rounded-[6px] bg-[var(--yellow-50)] border border-[var(--yellow-200)] text-sm text-[var(--yellow-800)]">
|
||||
<i class="pi pi-exclamation-triangle mt-0.5 shrink-0" />
|
||||
<span>
|
||||
Há slots configurados para <b>{{ orphanSlotDays.join(', ') }}</b>, mas esses dias não estão mais na sua jornada.
|
||||
@@ -1130,7 +1139,7 @@ const jornadaEndDate = computed({
|
||||
</div>
|
||||
|
||||
<!-- Toggle ativo -->
|
||||
<div class="flex items-center justify-between mb-5 p-4 rounded-2xl bg-[var(--surface-ground)]">
|
||||
<div class="flex items-center justify-between mb-5 p-4 rounded-[6px] bg-[var(--surface-ground)]">
|
||||
<div>
|
||||
<div class="font-medium">Permitir que pacientes agendem online</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mt-0.5">
|
||||
@@ -1172,7 +1181,7 @@ const jornadaEndDate = computed({
|
||||
<template v-if="previewDay != null">
|
||||
|
||||
<!-- Área cinza: ações rápidas + slots -->
|
||||
<div class="mx-3 mb-2 p-3 rounded-xl bg-[var(--surface-ground)]">
|
||||
<div class="mx-3 mb-2 p-3 rounded-[6px] bg-[var(--surface-ground)]">
|
||||
<div v-if="(slotsByDay[previewDay] || []).length === 0" class="text-sm text-[var(--text-color-secondary)] py-1">
|
||||
Nenhum slot disponível para este dia. Configure a jornada primeiro.
|
||||
</div>
|
||||
@@ -1242,7 +1251,7 @@ const jornadaEndDate = computed({
|
||||
|
||||
<!-- ══ COLUNA DIREITA: PREVIEW ═════════════════════════════ -->
|
||||
<div class="xl:w-[42%] xl:sticky xl:top-4 xl:self-start">
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm">
|
||||
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm">
|
||||
|
||||
<!-- Header do preview -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
|
||||
@@ -1307,7 +1316,7 @@ const jornadaEndDate = computed({
|
||||
<style scoped>
|
||||
/* ── Cards ─────────────────────────────────────────────────── */
|
||||
.cfg-card {
|
||||
border-radius: 1.25rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
@@ -1508,4 +1517,52 @@ const jornadaEndDate = computed({
|
||||
.toggle-switch--on .toggle-switch__thumb {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
</style>
|
||||
|
||||
/* ── Subheader de seção ──────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.65rem;
|
||||
padding: 0.875rem 1rem;
|
||||
border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent);
|
||||
background: linear-gradient(
|
||||
135deg,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%
|
||||
);
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
/* Brilho sutil no canto */
|
||||
.cfg-subheader::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: -20px; right: -20px;
|
||||
width: 80px; height: 80px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
|
||||
filter: blur(20px);
|
||||
pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem;
|
||||
border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
|
||||
color: var(--primary-color, #6366f1);
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--primary-color, #6366f1);
|
||||
letter-spacing: -0.01em;
|
||||
}
|
||||
.cfg-subheader__sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-color-secondary);
|
||||
opacity: 0.85;
|
||||
}
|
||||
</style>
|
||||
@@ -17,7 +17,7 @@ const hasLinkPersonalizado = computed(() => entitlements.can('agendador.link_per
|
||||
// ── Estado ─────────────────────────────────────────────────────
|
||||
const loading = ref(true)
|
||||
const ownerId = ref(null)
|
||||
const expandedCard = ref(null)
|
||||
const expandedCard = ref(new Set())
|
||||
const savingCard = ref(null)
|
||||
|
||||
// ── Upload de imagens ────────────────────────────────────────────
|
||||
@@ -62,8 +62,8 @@ async function onFileSelected (event, field) {
|
||||
|
||||
// ── Expand / Collapse all ────────────────────────────────────────
|
||||
const CARDS = ['identidade', 'perfil', 'fluxo', 'pagamento', 'triagem', 'textos']
|
||||
function expandAll () { expandedCard.value = CARDS[0] } // abre o primeiro como ponto de entrada
|
||||
function collapseAll () { expandedCard.value = null }
|
||||
function expandAll () { expandedCard.value = new Set(CARDS) }
|
||||
function collapseAll () { expandedCard.value = new Set() }
|
||||
|
||||
// ── Defaults ───────────────────────────────────────────────────
|
||||
const DEFAULT_CFG = {
|
||||
@@ -405,7 +405,7 @@ async function saveCard (cardKey) {
|
||||
)
|
||||
|
||||
toast.add({ severity: 'success', summary: 'Salvo', life: 2500 })
|
||||
expandedCard.value = null
|
||||
expandedCard.value = new Set()
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro ao salvar', detail: e.message, life: 4000 })
|
||||
} finally {
|
||||
@@ -474,7 +474,10 @@ function buildPayload (cardKey) {
|
||||
}
|
||||
|
||||
function toggleCard (key) {
|
||||
expandedCard.value = expandedCard.value === key ? null : key
|
||||
const s = new Set(expandedCard.value)
|
||||
if (s.has(key)) s.delete(key)
|
||||
else s.add(key)
|
||||
expandedCard.value = s
|
||||
}
|
||||
|
||||
onMounted(load)
|
||||
@@ -490,43 +493,27 @@ onMounted(load)
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- ── HEADER SECUNDÁRIO ─────────────────────────────────── -->
|
||||
<div class="flex items-center justify-between gap-3 px-1">
|
||||
<div>
|
||||
<div class="text-base font-semibold">Configurações do Agendador</div>
|
||||
<div class="text-xs text-surface-400 mt-0.5">Personalize a aparência, fluxo e comportamento do seu agendador público.</div>
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-calendar-clock" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Agendador Online</div>
|
||||
<div class="cfg-subheader__sub">Personalize a aparência, fluxo e comportamento do seu agendador público</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Button
|
||||
size="small"
|
||||
icon="pi pi-arrows-v"
|
||||
label="Expandir tudo"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
@click="expandAll"
|
||||
/>
|
||||
<Button
|
||||
size="small"
|
||||
icon="pi pi-minus"
|
||||
label="Contrair"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
@click="collapseAll"
|
||||
/>
|
||||
<div class="cfg-subheader__actions">
|
||||
<Button size="small" icon="pi pi-arrows-v" label="Expandir" severity="secondary" outlined class="rounded-full" @click="expandAll" />
|
||||
<Button size="small" icon="pi pi-minus" label="Contrair" severity="secondary" outlined class="rounded-full" @click="collapseAll" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: STATUS / ATIVAR ──────────────────────────────── -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="agd-card">
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<!-- Cabeçalho PRO -->
|
||||
<div class="flex items-start justify-between gap-4">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-11 h-11 rounded-2xl shrink-0"
|
||||
<div class="grid place-items-center w-11 h-11 rounded-[6px] shrink-0"
|
||||
:class="cfg.ativo ? 'bg-green-100 dark:bg-green-900/30 text-green-600' : 'bg-surface-100 text-surface-400'">
|
||||
<i class="pi pi-calendar-clock text-xl" />
|
||||
</div>
|
||||
@@ -586,9 +573,9 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Link personalizado bloqueado -->
|
||||
<div v-if="!hasLinkPersonalizado" class="mt-3 flex items-center gap-3 p-3 rounded-xl border border-dashed
|
||||
<div v-if="!hasLinkPersonalizado" class="mt-3 flex items-center gap-3 p-3 rounded-[6px] border border-dashed
|
||||
border-surface-300 dark:border-surface-600 bg-surface-50 dark:bg-surface-800/50">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-amber-100 dark:bg-amber-900/30 text-amber-500 shrink-0">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-[6px] bg-amber-100 dark:bg-amber-900/30 text-amber-500 shrink-0">
|
||||
<i class="pi pi-lock text-base" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
@@ -617,34 +604,27 @@ onMounted(load)
|
||||
Você controla quem pode agendar e quais horários ficam disponíveis.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: IDENTIDADE VISUAL ────────────────────────────── -->
|
||||
<Card class="overflow-hidden">
|
||||
<template #content>
|
||||
<!-- Cabeçalho do card -->
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 text-left"
|
||||
@click="toggleCard('identidade')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-purple-100 dark:bg-purple-900/30 text-purple-600 shrink-0">
|
||||
<i class="pi pi-palette" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-none">Identidade Visual</div>
|
||||
<div v-if="expandedCard !== 'identidade'" class="text-xs text-surface-400 mt-1">{{ resumoIdentidade }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi transition-transform duration-200 text-surface-400 shrink-0"
|
||||
:class="expandedCard === 'identidade' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('identidade') }">
|
||||
<button
|
||||
type="button"
|
||||
class="agd-accordion__header"
|
||||
@click="toggleCard('identidade')"
|
||||
>
|
||||
<div class="agd-accordion__icon bg-purple-100 dark:bg-purple-900/30 text-purple-600">
|
||||
<i class="pi pi-palette" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="agd-accordion__title">Identidade Visual</div>
|
||||
<div v-if="!expandedCard.has('identidade')" class="agd-accordion__summary">{{ resumoIdentidade }}</div>
|
||||
</div>
|
||||
<i class="pi agd-accordion__chevron"
|
||||
:class="expandedCard.has('identidade') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<!-- Conteúdo expandido -->
|
||||
<template v-if="expandedCard === 'identidade'">
|
||||
<Divider />
|
||||
<div v-if="expandedCard.has('identidade')" class="agd-accordion__body">
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
<!-- Nome de exibição (aqui pois é parte da identidade) -->
|
||||
@@ -660,7 +640,7 @@ onMounted(load)
|
||||
<div class="flex items-center gap-3">
|
||||
<ColorPicker v-model="cfg.cor_primaria" format="hex" />
|
||||
<InputText v-model="cfg.cor_primaria" placeholder="#4b6bff" class="w-32 font-mono" maxlength="7" />
|
||||
<div class="w-10 h-10 rounded-xl border border-surface-200 shrink-0"
|
||||
<div class="w-10 h-10 rounded-[6px] border border-surface-200 shrink-0"
|
||||
:style="{ background: cfg.cor_primaria }" />
|
||||
</div>
|
||||
<div class="text-xs text-surface-400 mt-1">Botões e destaques do agendador.</div>
|
||||
@@ -708,7 +688,7 @@ onMounted(load)
|
||||
@change="e => onFileSelected(e, 'header')" />
|
||||
</div>
|
||||
<InputText v-model="cfg.imagem_header_url" placeholder="ou cole uma URL pública..." class="w-full text-xs" />
|
||||
<div v-if="cfg.imagem_header_url" class="rounded-xl overflow-hidden h-20 w-full">
|
||||
<div v-if="cfg.imagem_header_url" class="rounded-[6px] overflow-hidden h-20 w-full">
|
||||
<img :src="cfg.imagem_header_url" alt="Header" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -728,7 +708,7 @@ onMounted(load)
|
||||
@change="e => onFileSelected(e, 'fundo')" />
|
||||
</div>
|
||||
<InputText v-model="cfg.imagem_fundo_url" placeholder="ou cole uma URL pública..." class="w-full text-xs" />
|
||||
<div v-if="cfg.imagem_fundo_url" class="rounded-xl overflow-hidden h-28 w-full">
|
||||
<div v-if="cfg.imagem_fundo_url" class="rounded-[6px] overflow-hidden h-28 w-full">
|
||||
<img :src="cfg.imagem_fundo_url" alt="Fundo" class="w-full h-full object-cover" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -744,33 +724,28 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: PERFIL PÚBLICO ───────────────────────────────── -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 text-left"
|
||||
@click="toggleCard('perfil')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-blue-100 dark:bg-blue-900/30 text-blue-600 shrink-0">
|
||||
<i class="pi pi-map-marker" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-none">Perfil Público</div>
|
||||
<div v-if="expandedCard !== 'perfil'" class="text-xs text-surface-400 mt-1">{{ resumoPerfil }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi transition-transform duration-200 text-surface-400 shrink-0"
|
||||
:class="expandedCard === 'perfil' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('perfil') }">
|
||||
<button
|
||||
type="button"
|
||||
class="agd-accordion__header"
|
||||
@click="toggleCard('perfil')"
|
||||
>
|
||||
<div class="agd-accordion__icon bg-blue-100 dark:bg-blue-900/30 text-blue-600">
|
||||
<i class="pi pi-map-marker" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="agd-accordion__title">Perfil Público</div>
|
||||
<div v-if="!expandedCard.has('perfil')" class="agd-accordion__summary">{{ resumoPerfil }}</div>
|
||||
</div>
|
||||
<i class="pi agd-accordion__chevron"
|
||||
:class="expandedCard.has('perfil') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<template v-if="expandedCard === 'perfil'">
|
||||
<Divider />
|
||||
<div v-if="expandedCard.has('perfil')" class="agd-accordion__body">
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
<!-- Endereço -->
|
||||
@@ -780,7 +755,7 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Botão Como Chegar -->
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-xl">
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||||
<div>
|
||||
<div class="font-medium text-sm">Botão "Como chegar"</div>
|
||||
<div class="text-xs text-surface-400 mt-0.5">Exibe um botão que abre o mapa para o paciente.</div>
|
||||
@@ -804,33 +779,28 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: FLUXO DE AGENDAMENTO ─────────────────────────── -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 text-left"
|
||||
@click="toggleCard('fluxo')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-cyan-100 dark:bg-cyan-900/30 text-cyan-600 shrink-0">
|
||||
<i class="pi pi-sitemap" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-none">Fluxo de Agendamento</div>
|
||||
<div v-if="expandedCard !== 'fluxo'" class="text-xs text-surface-400 mt-1">{{ resumoFluxo }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi transition-transform duration-200 text-surface-400 shrink-0"
|
||||
:class="expandedCard === 'fluxo' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('fluxo') }">
|
||||
<button
|
||||
type="button"
|
||||
class="agd-accordion__header"
|
||||
@click="toggleCard('fluxo')"
|
||||
>
|
||||
<div class="agd-accordion__icon bg-cyan-100 dark:bg-cyan-900/30 text-cyan-600">
|
||||
<i class="pi pi-sitemap" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="agd-accordion__title">Fluxo de Agendamento</div>
|
||||
<div v-if="!expandedCard.has('fluxo')" class="agd-accordion__summary">{{ resumoFluxo }}</div>
|
||||
</div>
|
||||
<i class="pi agd-accordion__chevron"
|
||||
:class="expandedCard.has('fluxo') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<template v-if="expandedCard === 'fluxo'">
|
||||
<Divider />
|
||||
<div v-if="expandedCard.has('fluxo')" class="agd-accordion__body">
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
<!-- Modo de aprovação -->
|
||||
@@ -840,7 +810,7 @@ onMounted(load)
|
||||
<div
|
||||
v-for="opt in modoOptions"
|
||||
:key="opt.value"
|
||||
class="flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition"
|
||||
class="flex items-center gap-3 p-3 rounded-[6px] border cursor-pointer transition"
|
||||
:class="cfg.modo_aprovacao === opt.value
|
||||
? 'border-primary bg-primary/5 dark:bg-primary/10'
|
||||
: 'border-surface-200 dark:border-surface-700 hover:border-surface-300'"
|
||||
@@ -957,33 +927,28 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: PAGAMENTO ────────────────────────────────────── -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 text-left"
|
||||
@click="toggleCard('pagamento')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-green-100 dark:bg-green-900/30 text-green-600 shrink-0">
|
||||
<i class="pi pi-credit-card" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-none">Pagamento</div>
|
||||
<div v-if="expandedCard !== 'pagamento'" class="text-xs text-surface-400 mt-1">{{ resumoPagamento }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi transition-transform duration-200 text-surface-400 shrink-0"
|
||||
:class="expandedCard === 'pagamento' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('pagamento') }">
|
||||
<button
|
||||
type="button"
|
||||
class="agd-accordion__header"
|
||||
@click="toggleCard('pagamento')"
|
||||
>
|
||||
<div class="agd-accordion__icon bg-green-100 dark:bg-green-900/30 text-green-600">
|
||||
<i class="pi pi-credit-card" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="agd-accordion__title">Pagamento</div>
|
||||
<div v-if="!expandedCard.has('pagamento')" class="agd-accordion__summary">{{ resumoPagamento }}</div>
|
||||
</div>
|
||||
<i class="pi agd-accordion__chevron"
|
||||
:class="expandedCard.has('pagamento') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<template v-if="expandedCard === 'pagamento'">
|
||||
<Divider />
|
||||
<div v-if="expandedCard.has('pagamento')" class="agd-accordion__body">
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
<!-- Modo de pagamento -->
|
||||
@@ -994,13 +959,13 @@ onMounted(load)
|
||||
v-for="modo in modosPagamento"
|
||||
:key="modo.value"
|
||||
type="button"
|
||||
class="flex items-center gap-3 p-3 rounded-xl border text-left transition"
|
||||
class="flex items-center gap-3 p-3 rounded-[6px] border text-left transition"
|
||||
:class="cfg.pagamento_modo === modo.value
|
||||
? 'border-primary bg-primary/5 ring-1 ring-primary/30'
|
||||
: 'border-surface-border bg-surface-50 dark:bg-surface-800 hover:bg-surface-100 dark:hover:bg-surface-700'"
|
||||
@click="cfg.pagamento_modo = modo.value"
|
||||
>
|
||||
<div class="grid place-items-center w-9 h-9 rounded-lg shrink-0"
|
||||
<div class="grid place-items-center w-9 h-9 rounded-[6px] shrink-0"
|
||||
:class="cfg.pagamento_modo === modo.value ? 'bg-primary/15 text-primary' : 'bg-surface-200 dark:bg-surface-700 text-surface-400'">
|
||||
<i :class="['pi', modo.icon]" />
|
||||
</div>
|
||||
@@ -1023,7 +988,7 @@ onMounted(load)
|
||||
<RouterLink to="/configuracoes/pagamento" class="underline">Configurações › Pagamento</RouterLink>.
|
||||
</p>
|
||||
|
||||
<div v-if="!algumMetodoConfigurado" class="rounded-xl border border-orange-200 bg-orange-50 dark:bg-orange-900/20 p-3 text-sm text-orange-700 dark:text-orange-300">
|
||||
<div v-if="!algumMetodoConfigurado" class="rounded-[6px] border border-orange-200 bg-orange-50 dark:bg-orange-900/20 p-3 text-sm text-orange-700 dark:text-orange-300">
|
||||
<i class="pi pi-exclamation-triangle mr-1" />
|
||||
Nenhuma forma de pagamento configurada ainda.
|
||||
<RouterLink to="/configuracoes/pagamento" class="underline font-medium ml-1">Configurar agora</RouterLink>
|
||||
@@ -1033,7 +998,7 @@ onMounted(load)
|
||||
<label
|
||||
v-for="m in metodosDisponiveis"
|
||||
:key="m.key"
|
||||
class="flex items-center gap-3 p-3 rounded-xl border cursor-pointer transition select-none"
|
||||
class="flex items-center gap-3 p-3 rounded-[6px] border cursor-pointer transition select-none"
|
||||
:class="[
|
||||
!m.ativo ? 'opacity-40 cursor-not-allowed border-surface-border bg-surface-50 dark:bg-surface-800' :
|
||||
isMetodoVisivel(m.key) ? 'border-primary bg-primary/5 ring-1 ring-primary/20' : 'border-surface-border bg-surface-50 dark:bg-surface-800 hover:bg-surface-100 dark:hover:bg-surface-700'
|
||||
@@ -1125,39 +1090,34 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: TRIAGEM & CONFORMIDADE ───────────────────────── -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 text-left"
|
||||
@click="toggleCard('triagem')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-orange-100 dark:bg-orange-900/30 text-orange-600 shrink-0">
|
||||
<i class="pi pi-shield" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-none">Triagem & Conformidade</div>
|
||||
<div v-if="expandedCard !== 'triagem'" class="text-xs text-surface-400 mt-1">{{ resumoTriagem }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi transition-transform duration-200 text-surface-400 shrink-0"
|
||||
:class="expandedCard === 'triagem' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('triagem') }">
|
||||
<button
|
||||
type="button"
|
||||
class="agd-accordion__header"
|
||||
@click="toggleCard('triagem')"
|
||||
>
|
||||
<div class="agd-accordion__icon bg-orange-100 dark:bg-orange-900/30 text-orange-600">
|
||||
<i class="pi pi-shield" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="agd-accordion__title">Triagem & Conformidade</div>
|
||||
<div v-if="!expandedCard.has('triagem')" class="agd-accordion__summary">{{ resumoTriagem }}</div>
|
||||
</div>
|
||||
<i class="pi agd-accordion__chevron"
|
||||
:class="expandedCard.has('triagem') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<template v-if="expandedCard === 'triagem'">
|
||||
<Divider />
|
||||
<div v-if="expandedCard.has('triagem')" class="agd-accordion__body">
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<div class="text-sm font-semibold text-surface-600">Campos extras no formulário</div>
|
||||
|
||||
<!-- Triagem: motivo -->
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-xl">
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||||
<div>
|
||||
<div class="font-medium text-sm">Motivo da consulta</div>
|
||||
<div class="text-xs text-surface-400 mt-0.5">Campo de texto livre opcional para o paciente informar o motivo.</div>
|
||||
@@ -1166,7 +1126,7 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Triagem: como conheceu -->
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-xl">
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||||
<div>
|
||||
<div class="font-medium text-sm">Como nos conheceu?</div>
|
||||
<div class="text-xs text-surface-400 mt-0.5">Pergunta de origem (indicação, redes sociais, busca…).</div>
|
||||
@@ -1179,7 +1139,7 @@ onMounted(load)
|
||||
<div class="text-sm font-semibold text-surface-600">Segurança & LGPD</div>
|
||||
|
||||
<!-- Verificação de email -->
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-xl">
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||||
<div>
|
||||
<div class="font-medium text-sm">Verificação de e-mail</div>
|
||||
<div class="text-xs text-surface-400 mt-0.5">
|
||||
@@ -1190,7 +1150,7 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Aceite LGPD -->
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-xl">
|
||||
<div class="flex items-center justify-between p-3 bg-surface-50 dark:bg-surface-800 rounded-[6px]">
|
||||
<div>
|
||||
<div class="font-medium text-sm">Aceite obrigatório de termos (LGPD)</div>
|
||||
<div class="text-xs text-surface-400 mt-0.5">
|
||||
@@ -1209,33 +1169,28 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ── CARD: TEXTOS DA JORNADA ────────────────────────────── -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-3 text-left"
|
||||
@click="toggleCard('textos')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="grid place-items-center w-9 h-9 rounded-xl bg-pink-100 dark:bg-pink-900/30 text-pink-600 shrink-0">
|
||||
<i class="pi pi-file-edit" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold leading-none">Textos da Jornada</div>
|
||||
<div v-if="expandedCard !== 'textos'" class="text-xs text-surface-400 mt-1">{{ resumoTextos }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi transition-transform duration-200 text-surface-400 shrink-0"
|
||||
:class="expandedCard === 'textos' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
<div class="agd-accordion" :class="{ 'agd-accordion--open': expandedCard.has('textos') }">
|
||||
<button
|
||||
type="button"
|
||||
class="agd-accordion__header"
|
||||
@click="toggleCard('textos')"
|
||||
>
|
||||
<div class="agd-accordion__icon bg-pink-100 dark:bg-pink-900/30 text-pink-600">
|
||||
<i class="pi pi-file-edit" />
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 text-left">
|
||||
<div class="agd-accordion__title">Textos da Jornada</div>
|
||||
<div v-if="!expandedCard.has('textos')" class="agd-accordion__summary">{{ resumoTextos }}</div>
|
||||
</div>
|
||||
<i class="pi agd-accordion__chevron"
|
||||
:class="expandedCard.has('textos') ? 'pi-chevron-up' : 'pi-chevron-down'" />
|
||||
</button>
|
||||
|
||||
<template v-if="expandedCard === 'textos'">
|
||||
<Divider />
|
||||
<div v-if="expandedCard.has('textos')" class="agd-accordion__body">
|
||||
<div class="flex flex-col gap-5">
|
||||
|
||||
<!-- Mensagem de boas-vindas -->
|
||||
@@ -1289,28 +1244,119 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Upload zone ──────────────────────────────────── */
|
||||
.agd-upload-zone {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.625rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border: 1.5px dashed var(--surface-border);
|
||||
border-radius: 0.875rem;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.agd-upload-zone:hover {
|
||||
border-color: var(--p-primary-500, #6366f1);
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 5%, transparent);
|
||||
border-color: var(--primary-color, #6366f1);
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 5%, transparent);
|
||||
}
|
||||
</style>
|
||||
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color, #6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute;
|
||||
top: -20px; right: -20px;
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 20%, transparent);
|
||||
color: var(--primary-color, #6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color, #6366f1); }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
|
||||
/* ── Card status (sem accordion) ─────────────────── */
|
||||
.agd-card {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-card);
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
/* ── Accordion cards ──────────────────────────────── */
|
||||
.agd-accordion {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
transition: border-color 0.15s;
|
||||
}
|
||||
.agd-accordion--open {
|
||||
border-color: color-mix(in srgb, var(--primary-color, #6366f1) 35%, transparent);
|
||||
}
|
||||
|
||||
.agd-accordion__header {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
width: 100%; padding: 0.875rem 1rem;
|
||||
background: transparent; border: none; cursor: pointer;
|
||||
transition: background 0.12s;
|
||||
text-align: left;
|
||||
}
|
||||
.agd-accordion__header:hover { background: var(--surface-hover); }
|
||||
.agd-accordion--open .agd-accordion__header {
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 4%, var(--surface-card));
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
|
||||
.agd-accordion__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
}
|
||||
.agd-accordion__title {
|
||||
font-size: 0.88rem; font-weight: 700;
|
||||
color: var(--text-color);
|
||||
}
|
||||
.agd-accordion--open .agd-accordion__title {
|
||||
color: var(--primary-color, #6366f1);
|
||||
}
|
||||
.agd-accordion__summary {
|
||||
font-size: 0.72rem; color: var(--text-color-secondary);
|
||||
opacity: 0.75; margin-top: 1px;
|
||||
white-space: nowrap; overflow: hidden; text-overflow: ellipsis;
|
||||
}
|
||||
.agd-accordion__chevron {
|
||||
font-size: 0.7rem; color: var(--text-color-secondary);
|
||||
opacity: 0.5; flex-shrink: 0;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.agd-accordion--open .agd-accordion__chevron {
|
||||
color: var(--primary-color, #6366f1); opacity: 0.8;
|
||||
}
|
||||
|
||||
.agd-accordion__body {
|
||||
padding: 1rem;
|
||||
display: flex; flex-direction: column; gap: 1.25rem;
|
||||
}
|
||||
</style>
|
||||
@@ -239,32 +239,19 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<!-- Header -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="cfg-icon-box">
|
||||
<i class="pi pi-id-card text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-900 font-semibold text-lg">Convênios</div>
|
||||
<div class="text-600 text-sm">
|
||||
Cadastre os convênios que você atende e seus procedimentos com valores de tabela.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
label="Novo convênio"
|
||||
icon="pi pi-plus"
|
||||
:disabled="pageLoading || addingNew"
|
||||
@click="addingNew = true; cancelEdit()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-id-card" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Convênios</div>
|
||||
<div class="cfg-subheader__sub">Convênios e planos de saúde que você atende</div>
|
||||
</div>
|
||||
<div class="cfg-subheader__actions">
|
||||
<Button label="Novo convênio" icon="pi pi-plus" size="small" :disabled="pageLoading || addingNew" class="rounded-full" @click="addingNew = true; cancelEdit()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
@@ -273,15 +260,13 @@ onMounted(async () => {
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- Formulário novo convênio -->
|
||||
<Card v-if="addingNew">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-plus-circle text-primary-500" />
|
||||
<span>Novo convênio</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<!-- Form novo convênio -->
|
||||
<div v-if="addingNew" class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<div class="cfg-wrap__icon"><i class="pi pi-plus" /></div>
|
||||
<span class="cfg-wrap__title">Novo convênio</span>
|
||||
</div>
|
||||
<div class="cfg-wrap__body">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 sm:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
@@ -296,30 +281,30 @@ onMounted(async () => {
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="addingNew = false; newForm = emptyForm()" />
|
||||
<Button label="Salvar" icon="pi pi-check" :loading="savingNew" @click="saveNew" />
|
||||
<div class="flex gap-2 justify-end mt-3">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="addingNew = false; newForm = emptyForm()" />
|
||||
<Button label="Salvar" icon="pi pi-check" class="rounded-full" :loading="savingNew" @click="saveNew" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista vazia -->
|
||||
<Card v-if="!plans.length && !addingNew">
|
||||
<template #content>
|
||||
<div class="text-center py-6 text-color-secondary">
|
||||
<i class="pi pi-id-card text-4xl opacity-30 mb-3 block" />
|
||||
<div class="font-medium mb-1">Nenhum convênio cadastrado</div>
|
||||
<div class="text-sm">Clique em "Novo convênio" para começar.</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div v-if="!plans.length && !addingNew" class="cfg-empty">
|
||||
<i class="pi pi-id-card text-3xl opacity-25" />
|
||||
<div class="text-sm font-medium">Nenhum convênio cadastrado</div>
|
||||
<div class="text-xs opacity-70">Clique em "Novo convênio" para começar.</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de convênios -->
|
||||
<Card v-for="plan in plans" :key="plan.id" :class="{ 'opacity-60': !plan.active }">
|
||||
<template #content>
|
||||
|
||||
<!-- Modo edição do plano -->
|
||||
<template v-if="editingId === plan.id">
|
||||
<div
|
||||
v-for="plan in plans"
|
||||
:key="plan.id"
|
||||
class="cfg-wrap"
|
||||
:class="{ 'opacity-60': !plan.active }"
|
||||
>
|
||||
<!-- Modo edição do plano -->
|
||||
<template v-if="editingId === plan.id">
|
||||
<div class="cfg-wrap__body">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 sm:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
@@ -334,169 +319,138 @@ onMounted(async () => {
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="cancelEdit" />
|
||||
<Button label="Salvar" icon="pi pi-check" :loading="savingEdit" @click="saveEdit" />
|
||||
<div class="flex gap-2 justify-end mt-3">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="cancelEdit" />
|
||||
<Button label="Salvar" icon="pi pi-check" class="rounded-full" :loading="savingEdit" @click="saveEdit" />
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Modo leitura -->
|
||||
<template v-else>
|
||||
<!-- Cabeçalho do plano -->
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-3 min-w-0">
|
||||
<div class="cfg-icon-box-sm shrink-0">
|
||||
<i class="pi pi-id-card" />
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-900">{{ plan.name }}</div>
|
||||
<div v-if="plan.notes" class="text-sm text-color-secondary italic truncate">{{ plan.notes }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0 flex-wrap">
|
||||
<Tag :value="plan.active ? 'Ativo' : 'Inativo'" :severity="plan.active ? 'success' : 'secondary'" />
|
||||
<Button
|
||||
:label="`Procedimentos (${totalProcedimentos(plan)})`"
|
||||
:icon="expandedPlanId === plan.id ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
@click="expandedPlanId === plan.id ? (expandedPlanId = null, addingServicePlanId = null) : (expandedPlanId = plan.id, addingServicePlanId = null)"
|
||||
/>
|
||||
<Button
|
||||
:icon="plan.active ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
:severity="plan.active ? 'secondary' : 'success'"
|
||||
outlined
|
||||
size="small"
|
||||
v-tooltip.top="plan.active ? 'Desativar' : 'Ativar'"
|
||||
@click="togglePlan(plan)"
|
||||
/>
|
||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" v-tooltip.top="'Editar'" @click="startEdit(plan)" />
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small" v-tooltip.top="'Desativar'" @click="removePlan(plan.id)" />
|
||||
<!-- Modo leitura -->
|
||||
<template v-else>
|
||||
|
||||
<!-- Cabeçalho do plano -->
|
||||
<div class="cnv-plan-head">
|
||||
<div class="flex items-center gap-2.5 min-w-0 flex-1">
|
||||
<div class="cfg-wrap__icon shrink-0"><i class="pi pi-id-card" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="font-semibold text-sm">{{ plan.name }}</div>
|
||||
<div v-if="plan.notes" class="text-xs text-[var(--text-color-secondary)] opacity-70 truncate">{{ plan.notes }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 shrink-0 flex-wrap">
|
||||
<Tag :value="plan.active ? 'Ativo' : 'Inativo'" :severity="plan.active ? 'success' : 'secondary'" />
|
||||
<Button
|
||||
:label="`Procedimentos (${totalProcedimentos(plan)})`"
|
||||
:icon="expandedPlanId === plan.id ? 'pi pi-chevron-up' : 'pi pi-chevron-down'"
|
||||
severity="secondary" outlined size="small" class="rounded-full"
|
||||
@click="togglePanel(plan.id)"
|
||||
/>
|
||||
<Button
|
||||
:icon="plan.active ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
:severity="plan.active ? 'secondary' : 'success'"
|
||||
outlined size="small"
|
||||
v-tooltip.top="plan.active ? 'Desativar' : 'Ativar'"
|
||||
@click="togglePlan(plan)"
|
||||
/>
|
||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" v-tooltip.top="'Editar'" @click="startEdit(plan)" />
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small" v-tooltip.top="'Remover'" @click="removePlan(plan.id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Painel expansível: procedimentos -->
|
||||
<div v-if="expandedPlanId === plan.id" class="mt-4 border-t border-surface pt-4">
|
||||
<!-- Painel procedimentos expandível -->
|
||||
<div v-if="expandedPlanId === plan.id" class="cnv-procedures">
|
||||
|
||||
<!-- Lista de procedimentos (ativos e inativos) -->
|
||||
<div v-if="plan.insurance_plan_services?.length" class="mb-3 flex flex-col gap-1">
|
||||
<template v-for="ps in plan.insurance_plan_services" :key="ps.id">
|
||||
<!-- Lista de procedimentos -->
|
||||
<div v-if="plan.insurance_plan_services?.length" class="cnv-proc-list">
|
||||
<template v-for="ps in plan.insurance_plan_services" :key="ps.id">
|
||||
|
||||
<!-- Modo edição inline do procedimento -->
|
||||
<div v-if="editingServiceId === ps.id" class="flex flex-wrap gap-2 items-end py-2 border-b border-surface">
|
||||
<div class="flex-1 min-w-[140px]">
|
||||
<label class="text-xs text-color-secondary mb-1 block">Nome</label>
|
||||
<!-- Edição inline do procedimento -->
|
||||
<div v-if="editingServiceId === ps.id" class="cnv-proc-edit">
|
||||
<div class="grid grid-cols-12 gap-2 flex-1">
|
||||
<div class="col-span-12 sm:col-span-6">
|
||||
<label class="cnv-label">Nome</label>
|
||||
<InputText v-model="editServiceForm.name" class="w-full" size="small" />
|
||||
</div>
|
||||
<div class="w-36">
|
||||
<label class="text-xs text-color-secondary mb-1 block">Valor (R$)</label>
|
||||
<InputNumber
|
||||
v-model="editServiceForm.value"
|
||||
mode="currency" currency="BRL" locale="pt-BR"
|
||||
:min="0" :minFractionDigits="2"
|
||||
class="w-full" size="small"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined size="small" @click="cancelEditService" />
|
||||
<Button label="Salvar" icon="pi pi-check" size="small" :loading="savingServiceEdit" @click="saveServiceEdit" />
|
||||
<div class="col-span-12 sm:col-span-6">
|
||||
<label class="cnv-label">Valor (R$)</label>
|
||||
<InputNumber v-model="editServiceForm.value" mode="currency" currency="BRL" locale="pt-BR" :min="0" :minFractionDigits="2" class="w-full" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modo leitura do procedimento -->
|
||||
<div
|
||||
v-else
|
||||
class="flex items-center justify-between gap-2 py-2 border-b border-surface last:border-0"
|
||||
:class="{ 'opacity-60': !ps.active }"
|
||||
>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Tag v-if="!ps.active" value="Inativo" severity="secondary" class="text-xs" />
|
||||
<span class="text-sm font-medium text-900 truncate">{{ ps.name }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<span class="text-sm font-semibold text-primary-500">{{ fmtBRL(ps.value) }}</span>
|
||||
<Button
|
||||
:icon="ps.active ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
:severity="ps.active ? 'secondary' : 'success'"
|
||||
text size="small"
|
||||
v-tooltip.top="ps.active ? 'Desativar' : 'Ativar'"
|
||||
@click="onToggleService(ps)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
severity="secondary" text size="small"
|
||||
v-tooltip.top="'Editar'"
|
||||
@click="startEditService(ps)"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-trash"
|
||||
severity="danger" text size="small"
|
||||
v-tooltip.top="'Remover definitivamente'"
|
||||
@click="deleteService(ps.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="addingServicePlanId !== plan.id" class="text-sm text-color-secondary mb-3 italic">
|
||||
Nenhum procedimento cadastrado.
|
||||
</div>
|
||||
|
||||
<!-- Formulário adicionar procedimento -->
|
||||
<div v-if="addingServicePlanId === plan.id" class="mt-3">
|
||||
<!-- Cards de serviços para auto-preencher -->
|
||||
<div v-if="services.filter(s => s.active).length" class="mb-3">
|
||||
<div class="text-xs text-color-secondary mb-2">Clique num serviço para pré-preencher:</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-2">
|
||||
<button
|
||||
v-for="svc in services.filter(s => s.active)"
|
||||
:key="svc.id"
|
||||
class="svc-quick-card"
|
||||
@click="fillFromService(svc)"
|
||||
>
|
||||
<span class="svc-quick-name">{{ svc.name }}</span>
|
||||
<span class="svc-quick-price">{{ fmtBRL(svc.price) }}</span>
|
||||
</button>
|
||||
<div class="flex gap-2 justify-end mt-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined size="small" class="rounded-full" @click="cancelEditService" />
|
||||
<Button label="Salvar" icon="pi pi-check" size="small" class="rounded-full" :loading="savingServiceEdit" @click="saveServiceEdit" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2 items-end">
|
||||
<div class="flex-1 min-w-[140px]">
|
||||
<label class="text-xs text-color-secondary mb-1 block">Nome do procedimento *</label>
|
||||
<InputText v-model="newServiceForm.name" placeholder="Ex: Consulta" class="w-full" size="small" />
|
||||
<!-- Leitura do procedimento -->
|
||||
<div v-else class="cnv-proc-row" :class="{ 'opacity-50': !ps.active }">
|
||||
<div class="flex items-center gap-2 min-w-0 flex-1">
|
||||
<Tag v-if="!ps.active" value="Inativo" severity="secondary" class="text-xs shrink-0" />
|
||||
<span class="text-sm font-medium truncate">{{ ps.name }}</span>
|
||||
</div>
|
||||
<div class="w-36">
|
||||
<label class="text-xs text-color-secondary mb-1 block">Valor (R$) *</label>
|
||||
<InputNumber
|
||||
v-model="newServiceForm.value"
|
||||
mode="currency" currency="BRL" locale="pt-BR"
|
||||
:min="0" :minFractionDigits="2"
|
||||
class="w-full" size="small"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined size="small" @click="cancelAddService" />
|
||||
<Button label="Adicionar" icon="pi pi-check" size="small" :loading="savingService" @click="saveService(plan.id)" />
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<span class="text-sm font-semibold text-[var(--primary-color)]">{{ fmtBRL(ps.value) }}</span>
|
||||
<Button :icon="ps.active ? 'pi pi-eye-slash' : 'pi pi-eye'" :severity="ps.active ? 'secondary' : 'success'" text size="small" v-tooltip.top="ps.active ? 'Desativar' : 'Ativar'" @click="onToggleService(ps)" />
|
||||
<Button icon="pi pi-pencil" severity="secondary" text size="small" v-tooltip.top="'Editar'" @click="startEditService(ps)" />
|
||||
<Button icon="pi pi-trash" severity="danger" text size="small" v-tooltip.top="'Remover'" @click="deleteService(ps.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="addingServicePlanId !== plan.id"
|
||||
label="Adicionar procedimento"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary"
|
||||
outlined
|
||||
size="small"
|
||||
class="mt-2"
|
||||
@click="startAddService(plan.id)"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
<div v-else-if="addingServicePlanId !== plan.id" class="text-sm text-[var(--text-color-secondary)] italic px-1 py-2">
|
||||
Nenhum procedimento cadastrado.
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Form adicionar procedimento -->
|
||||
<div v-if="addingServicePlanId === plan.id" class="cnv-proc-form">
|
||||
|
||||
<!-- Quick-fill dos serviços -->
|
||||
<div v-if="services.filter(s => s.active).length" class="mb-3">
|
||||
<div class="cnv-label mb-1.5">Clique num serviço para pré-preencher:</div>
|
||||
<div class="grid grid-cols-2 sm:grid-cols-3 gap-1.5">
|
||||
<button
|
||||
v-for="svc in services.filter(s => s.active)"
|
||||
:key="svc.id"
|
||||
class="svc-quick-card"
|
||||
@click="fillFromService(svc)"
|
||||
>
|
||||
<span class="svc-quick-name">{{ svc.name }}</span>
|
||||
<span class="svc-quick-price">{{ fmtBRL(svc.price) }}</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Campos nome + valor -->
|
||||
<div class="grid grid-cols-12 gap-2">
|
||||
<div class="col-span-12 sm:col-span-7">
|
||||
<label class="cnv-label">Nome do procedimento *</label>
|
||||
<InputText v-model="newServiceForm.name" placeholder="Ex: Consulta" class="w-full" size="small" />
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-5">
|
||||
<label class="cnv-label">Valor (R$) *</label>
|
||||
<InputNumber v-model="newServiceForm.value" mode="currency" currency="BRL" locale="pt-BR" :min="0" :minFractionDigits="2" class="w-full" size="small" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Botões em linha separada -->
|
||||
<div class="flex gap-2 justify-end mt-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined size="small" class="rounded-full" @click="cancelAddService" />
|
||||
<Button label="Adicionar" icon="pi pi-check" size="small" class="rounded-full" :loading="savingService" @click="saveService(plan.id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="addingServicePlanId !== plan.id"
|
||||
label="Adicionar procedimento"
|
||||
icon="pi pi-plus"
|
||||
severity="secondary" outlined size="small" class="mt-2 rounded-full"
|
||||
@click="startAddService(plan.id)"
|
||||
/>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<Message severity="info" :closable="false">
|
||||
<span class="text-sm">
|
||||
@@ -509,44 +463,129 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cfg-icon-box {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.875rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
flex-shrink: 0;
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color,#6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute; top: -20px; right: -20px;
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 20%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color,#6366f1); }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
|
||||
/* ── Card wrap ────────────────────────────────────── */
|
||||
.cfg-wrap {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cfg-wrap__head {
|
||||
display: flex; align-items: center; gap: 0.625rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.cfg-wrap__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 12%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.8rem;
|
||||
}
|
||||
.cfg-wrap__title { font-size: 0.88rem; font-weight: 700; color: var(--text-color); }
|
||||
.cfg-wrap__body { padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
|
||||
/* ── Empty state ──────────────────────────────────── */
|
||||
.cfg-empty {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; padding: 2.5rem 1rem; text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
border: 1px dashed var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-ground);
|
||||
}
|
||||
|
||||
.cfg-icon-box-sm {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.625rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 10%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
flex-shrink: 0;
|
||||
/* ── Cabeçalho do plano ───────────────────────────── */
|
||||
.cnv-plan-head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.75rem 1rem; flex-wrap: wrap;
|
||||
}
|
||||
|
||||
/* ── Painel procedimentos ─────────────────────────── */
|
||||
.cnv-procedures {
|
||||
border-top: 1px solid var(--surface-border);
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex; flex-direction: column; gap: 0.25rem;
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.cnv-proc-list {
|
||||
display: flex; flex-direction: column;
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; overflow: hidden;
|
||||
background: var(--surface-card);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
.cnv-proc-row {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.5rem; padding: 0.5rem 0.75rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.cnv-proc-row:last-child { border-bottom: none; }
|
||||
.cnv-proc-row:hover { background: var(--surface-hover); }
|
||||
|
||||
.cnv-proc-edit {
|
||||
padding: 0.75rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 3%, var(--surface-card));
|
||||
}
|
||||
.cnv-proc-edit:last-child { border-bottom: none; }
|
||||
|
||||
/* ── Form adicionar procedimento ──────────────────── */
|
||||
.cnv-proc-form {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px;
|
||||
background: var(--surface-card);
|
||||
padding: 0.75rem;
|
||||
margin-top: 0.25rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Labels ───────────────────────────────────────── */
|
||||
.cnv-label {
|
||||
display: block;
|
||||
font-size: 0.72rem; font-weight: 500;
|
||||
color: var(--text-color-secondary); margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
/* ── Quick-fill serviços ──────────────────────────── */
|
||||
.svc-quick-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.125rem;
|
||||
padding: 0.375rem 0.625rem;
|
||||
border-radius: 0.5rem;
|
||||
border: 1px solid var(--p-surface-200, #e5e7eb);
|
||||
background: var(--p-surface-50, #f9fafb);
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s, background 0.15s;
|
||||
display: flex; flex-direction: column; gap: 0.1rem;
|
||||
padding: 0.375rem 0.625rem; border-radius: 6px;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
text-align: left; cursor: pointer;
|
||||
transition: border-color 0.12s, background 0.12s;
|
||||
}
|
||||
.svc-quick-card:hover {
|
||||
border-color: var(--p-primary-400, #818cf8);
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 6%, transparent);
|
||||
border-color: var(--primary-color,#6366f1);
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 5%, transparent);
|
||||
}
|
||||
.svc-quick-name { font-size: 0.75rem; font-weight: 600; color: var(--p-text-color); }
|
||||
.svc-quick-price { font-size: 0.7rem; color: var(--p-text-muted-color); }
|
||||
</style>
|
||||
.svc-quick-name { font-size: 0.72rem; font-weight: 600; color: var(--text-color); }
|
||||
.svc-quick-price { font-size: 0.68rem; color: var(--text-color-secondary); }
|
||||
</style>
|
||||
@@ -184,32 +184,19 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<!-- Header -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="cfg-icon-box">
|
||||
<i class="pi pi-percentage text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-900 font-semibold text-lg">Descontos por Paciente</div>
|
||||
<div class="text-600 text-sm">
|
||||
Configure descontos recorrentes aplicados automaticamente por paciente.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
label="Novo desconto"
|
||||
icon="pi pi-plus"
|
||||
:disabled="pageLoading || addingNew"
|
||||
@click="addingNew = true; cancelEdit()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-percentage" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Descontos por Paciente</div>
|
||||
<div class="cfg-subheader__sub">Descontos recorrentes aplicados automaticamente por paciente</div>
|
||||
</div>
|
||||
<div class="cfg-subheader__actions">
|
||||
<Button label="Novo desconto" icon="pi pi-plus" size="small" :disabled="pageLoading || addingNew" class="rounded-full" @click="addingNew = true; cancelEdit()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
@@ -218,309 +205,144 @@ onMounted(async () => {
|
||||
|
||||
<template v-else>
|
||||
|
||||
<!-- Lista de descontos -->
|
||||
<Card v-if="discounts.length || addingNew">
|
||||
<template #content>
|
||||
<div class="flex flex-col gap-3">
|
||||
<!-- Lista + form -->
|
||||
<div v-if="discounts.length || addingNew" class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<div class="cfg-wrap__icon"><i class="pi pi-percentage" /></div>
|
||||
<span class="cfg-wrap__title">Descontos cadastrados</span>
|
||||
<span class="cfg-wrap__count">{{ discounts.length }}</span>
|
||||
</div>
|
||||
|
||||
<template v-for="disc in discounts" :key="disc.id">
|
||||
<div class="dsc-list">
|
||||
|
||||
<!-- Modo edição inline -->
|
||||
<div v-if="editingId === disc.id" class="discount-row editing">
|
||||
<div class="grid grid-cols-12 gap-3 flex-1">
|
||||
<template v-for="disc in discounts" :key="disc.id">
|
||||
|
||||
<!-- Paciente (desabilitado na edição) -->
|
||||
<div class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<Select
|
||||
v-model="editForm.patient_id"
|
||||
inputId="edit-patient"
|
||||
:options="patients"
|
||||
optionLabel="nome_completo"
|
||||
optionValue="id"
|
||||
disabled
|
||||
class="w-full"
|
||||
/>
|
||||
<label for="edit-patient">Paciente</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Desconto % -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="editForm.discount_pct"
|
||||
inputId="edit-pct"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:minFractionDigits="0"
|
||||
:maxFractionDigits="2"
|
||||
suffix="%"
|
||||
fluid
|
||||
/>
|
||||
<label for="edit-pct">Desconto %</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Desconto R$ -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="editForm.discount_flat"
|
||||
inputId="edit-flat"
|
||||
mode="currency"
|
||||
currency="BRL"
|
||||
locale="pt-BR"
|
||||
:min="0"
|
||||
fluid
|
||||
/>
|
||||
<label for="edit-flat">Desconto R$</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Vigência: de -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<DatePicker
|
||||
v-model="editForm.active_from"
|
||||
inputId="edit-from"
|
||||
dateFormat="dd/mm/yy"
|
||||
showButtonBar
|
||||
fluid
|
||||
/>
|
||||
<label for="edit-from">Vigência: de</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Vigência: até -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<DatePicker
|
||||
v-model="editForm.active_to"
|
||||
inputId="edit-to"
|
||||
dateFormat="dd/mm/yy"
|
||||
showButtonBar
|
||||
fluid
|
||||
/>
|
||||
<label for="edit-to">Vigência: até</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Motivo -->
|
||||
<div class="col-span-12">
|
||||
<FloatLabel variant="on">
|
||||
<InputText
|
||||
v-model="editForm.reason"
|
||||
inputId="edit-reason"
|
||||
class="w-full"
|
||||
/>
|
||||
<label for="edit-reason">Motivo (opcional)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<Button
|
||||
icon="pi pi-check"
|
||||
size="small"
|
||||
:loading="savingEdit"
|
||||
@click="saveEdit"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="cancelEdit"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modo leitura -->
|
||||
<div v-else class="discount-row">
|
||||
<div class="discount-info">
|
||||
<div class="font-medium text-900">{{ patientName(disc.patient_id) }}</div>
|
||||
<div class="flex flex-wrap gap-2 mt-1">
|
||||
<span v-if="fmtPct(disc.discount_pct)" class="discount-badge">
|
||||
{{ fmtPct(disc.discount_pct) }}
|
||||
</span>
|
||||
<span v-if="fmtBRL(disc.discount_flat)" class="discount-badge">
|
||||
{{ fmtBRL(disc.discount_flat) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-sm text-600 mt-0.5">
|
||||
<span v-if="disc.active_from || disc.active_to">
|
||||
{{ fmtDate(disc.active_from) || 'Indefinido' }} →
|
||||
{{ fmtDate(disc.active_to) || 'Indefinido' }}
|
||||
</span>
|
||||
<span v-else>Vigência indefinida</span>
|
||||
</div>
|
||||
<div v-if="disc.reason" class="text-sm text-500 mt-0.5 italic">
|
||||
{{ disc.reason }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="discount-meta">
|
||||
<Tag
|
||||
:value="disc.active ? 'Ativo' : 'Inativo'"
|
||||
:severity="disc.active ? 'success' : 'secondary'"
|
||||
/>
|
||||
</div>
|
||||
<div class="flex gap-2 ml-auto">
|
||||
<Button
|
||||
icon="pi pi-pencil"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
text
|
||||
@click="startEdit(disc); addingNew = false"
|
||||
/>
|
||||
<Button
|
||||
v-if="disc.active"
|
||||
icon="pi pi-ban"
|
||||
size="small"
|
||||
severity="danger"
|
||||
text
|
||||
v-tooltip.top="'Desativar'"
|
||||
@click="confirmRemove(disc.id)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Divisor antes do form novo -->
|
||||
<Divider v-if="discounts.length && addingNew" />
|
||||
|
||||
<!-- Formulário novo desconto inline -->
|
||||
<div v-if="addingNew" class="discount-row new-row">
|
||||
<div class="grid grid-cols-12 gap-3 flex-1">
|
||||
|
||||
<!-- Paciente -->
|
||||
<!-- Edição inline -->
|
||||
<div v-if="editingId === disc.id" class="dsc-form-row dsc-form-row--editing">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<Select
|
||||
v-model="newForm.patient_id"
|
||||
inputId="new-patient"
|
||||
:options="patients"
|
||||
optionLabel="nome_completo"
|
||||
optionValue="id"
|
||||
filter
|
||||
class="w-full"
|
||||
/>
|
||||
<label for="new-patient">Paciente *</label>
|
||||
<Select v-model="editForm.patient_id" inputId="edit-patient" :options="patients" optionLabel="nome_completo" optionValue="id" disabled class="w-full" />
|
||||
<label for="edit-patient">Paciente</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Desconto % -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="newForm.discount_pct"
|
||||
inputId="new-pct"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:minFractionDigits="0"
|
||||
:maxFractionDigits="2"
|
||||
suffix="%"
|
||||
fluid
|
||||
/>
|
||||
<label for="new-pct">Desconto %</label>
|
||||
<InputNumber v-model="editForm.discount_pct" inputId="edit-pct" :min="0" :max="100" :minFractionDigits="0" :maxFractionDigits="2" suffix="%" fluid />
|
||||
<label for="edit-pct">Desconto %</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Desconto R$ -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="newForm.discount_flat"
|
||||
inputId="new-flat"
|
||||
mode="currency"
|
||||
currency="BRL"
|
||||
locale="pt-BR"
|
||||
:min="0"
|
||||
fluid
|
||||
/>
|
||||
<label for="new-flat">Desconto R$</label>
|
||||
<InputNumber v-model="editForm.discount_flat" inputId="edit-flat" mode="currency" currency="BRL" locale="pt-BR" :min="0" fluid />
|
||||
<label for="edit-flat">Desconto R$</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Vigência: de -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<DatePicker
|
||||
v-model="newForm.active_from"
|
||||
inputId="new-from"
|
||||
dateFormat="dd/mm/yy"
|
||||
showButtonBar
|
||||
fluid
|
||||
/>
|
||||
<label for="new-from">Vigência: de</label>
|
||||
<DatePicker v-model="editForm.active_from" inputId="edit-from" dateFormat="dd/mm/yy" showButtonBar fluid />
|
||||
<label for="edit-from">Vigência: de</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Vigência: até -->
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<DatePicker
|
||||
v-model="newForm.active_to"
|
||||
inputId="new-to"
|
||||
dateFormat="dd/mm/yy"
|
||||
showButtonBar
|
||||
fluid
|
||||
/>
|
||||
<label for="new-to">Vigência: até</label>
|
||||
<DatePicker v-model="editForm.active_to" inputId="edit-to" dateFormat="dd/mm/yy" showButtonBar fluid />
|
||||
<label for="edit-to">Vigência: até</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Motivo -->
|
||||
<div class="col-span-12">
|
||||
<FloatLabel variant="on">
|
||||
<InputText
|
||||
v-model="newForm.reason"
|
||||
inputId="new-reason"
|
||||
class="w-full"
|
||||
/>
|
||||
<label for="new-reason">Motivo (opcional)</label>
|
||||
<InputText v-model="editForm.reason" inputId="edit-reason" class="w-full" />
|
||||
<label for="edit-reason">Motivo (opcional)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="flex gap-2 mt-1">
|
||||
<Button
|
||||
icon="pi pi-check"
|
||||
label="Adicionar"
|
||||
size="small"
|
||||
:loading="savingNew"
|
||||
@click="saveNew"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="addingNew = false; newForm = emptyForm()"
|
||||
/>
|
||||
<div class="flex gap-2 justify-end mt-3">
|
||||
<Button label="Cancelar" icon="pi pi-times" size="small" severity="secondary" outlined class="rounded-full" @click="cancelEdit" />
|
||||
<Button label="Salvar" icon="pi pi-check" size="small" :loading="savingEdit" class="rounded-full" @click="saveEdit" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Leitura -->
|
||||
<div v-else class="dsc-row">
|
||||
<div class="dsc-row__info">
|
||||
<div class="font-semibold text-sm">{{ patientName(disc.patient_id) }}</div>
|
||||
<div class="flex flex-wrap gap-1.5 mt-1">
|
||||
<span v-if="fmtPct(disc.discount_pct)" class="dsc-badge">{{ fmtPct(disc.discount_pct) }}</span>
|
||||
<span v-if="fmtBRL(disc.discount_flat)" class="dsc-badge">{{ fmtBRL(disc.discount_flat) }}</span>
|
||||
</div>
|
||||
<div class="text-xs text-[var(--text-color-secondary)] mt-0.5">
|
||||
<span v-if="disc.active_from || disc.active_to">
|
||||
{{ fmtDate(disc.active_from) || 'Indefinido' }} → {{ fmtDate(disc.active_to) || 'Indefinido' }}
|
||||
</span>
|
||||
<span v-else>Vigência indefinida</span>
|
||||
</div>
|
||||
<div v-if="disc.reason" class="text-xs text-[var(--text-color-secondary)] italic mt-0.5">{{ disc.reason }}</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<Tag :value="disc.active ? 'Ativo' : 'Inativo'" :severity="disc.active ? 'success' : 'secondary'" />
|
||||
<Button icon="pi pi-pencil" size="small" severity="secondary" text v-tooltip.top="'Editar'" @click="startEdit(disc); addingNew = false" />
|
||||
<Button v-if="disc.active" icon="pi pi-ban" size="small" severity="danger" text v-tooltip.top="'Desativar'" @click="confirmRemove(disc.id)" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<!-- Form novo desconto -->
|
||||
<div v-if="addingNew" class="dsc-form-row dsc-form-row--new">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<Select v-model="newForm.patient_id" inputId="new-patient" :options="patients" optionLabel="nome_completo" optionValue="id" filter class="w-full" />
|
||||
<label for="new-patient">Paciente *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber v-model="newForm.discount_pct" inputId="new-pct" :min="0" :max="100" :minFractionDigits="0" :maxFractionDigits="2" suffix="%" fluid />
|
||||
<label for="new-pct">Desconto %</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber v-model="newForm.discount_flat" inputId="new-flat" mode="currency" currency="BRL" locale="pt-BR" :min="0" fluid />
|
||||
<label for="new-flat">Desconto R$</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<DatePicker v-model="newForm.active_from" inputId="new-from" dateFormat="dd/mm/yy" showButtonBar fluid />
|
||||
<label for="new-from">Vigência: de</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="col-span-6 sm:col-span-2">
|
||||
<FloatLabel variant="on">
|
||||
<DatePicker v-model="newForm.active_to" inputId="new-to" dateFormat="dd/mm/yy" showButtonBar fluid />
|
||||
<label for="new-to">Vigência: até</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
<div class="col-span-12">
|
||||
<FloatLabel variant="on">
|
||||
<InputText v-model="newForm.reason" inputId="new-reason" class="w-full" />
|
||||
<label for="new-reason">Motivo (opcional)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-3">
|
||||
<Button label="Cancelar" icon="pi pi-times" size="small" severity="secondary" outlined class="rounded-full" @click="addingNew = false; newForm = emptyForm()" />
|
||||
<Button label="Adicionar" icon="pi pi-check" size="small" :loading="savingNew" class="rounded-full" @click="saveNew" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Estado vazio -->
|
||||
<Card v-else>
|
||||
<template #content>
|
||||
<div class="flex flex-col items-center gap-3 py-6 text-center">
|
||||
<i class="pi pi-percentage text-4xl text-400" />
|
||||
<div class="text-600">Nenhum desconto cadastrado ainda.</div>
|
||||
<Button
|
||||
label="Adicionar primeiro desconto"
|
||||
icon="pi pi-plus"
|
||||
outlined
|
||||
@click="addingNew = true"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div v-else class="cfg-empty">
|
||||
<i class="pi pi-percentage text-3xl opacity-25" />
|
||||
<div class="text-sm font-medium">Nenhum desconto cadastrado ainda.</div>
|
||||
<Button label="Adicionar primeiro desconto" icon="pi pi-plus" outlined size="small" class="rounded-full" @click="addingNew = true" />
|
||||
</div>
|
||||
|
||||
<!-- Dica -->
|
||||
<Message severity="info" :closable="false">
|
||||
@@ -535,56 +357,103 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cfg-icon-box {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.875rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
flex-shrink: 0;
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color,#6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute; top: -20px; right: -20px;
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 20%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color,#6366f1); }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
|
||||
.discount-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.75rem;
|
||||
/* ── Card wrap ────────────────────────────────────── */
|
||||
.cfg-wrap {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cfg-wrap__head {
|
||||
display: flex; align-items: center; gap: 0.625rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.cfg-wrap__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 12%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.8rem;
|
||||
}
|
||||
.cfg-wrap__title { font-size: 0.88rem; font-weight: 700; color: var(--text-color); flex: 1; }
|
||||
.cfg-wrap__count {
|
||||
font-size: 0.7rem; font-weight: 700;
|
||||
background: var(--primary-color,#6366f1); color: #fff;
|
||||
padding: 1px 8px; border-radius: 999px; flex-shrink: 0;
|
||||
}
|
||||
|
||||
.discount-row.editing {
|
||||
border-color: var(--p-primary-300, #a5b4fc);
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 4%, var(--surface-ground));
|
||||
}
|
||||
/* ── Lista de descontos ───────────────────────────── */
|
||||
.dsc-list { display: flex; flex-direction: column; }
|
||||
|
||||
.discount-row.new-row {
|
||||
border-style: dashed;
|
||||
/* Linha de leitura */
|
||||
.dsc-row {
|
||||
display: flex; align-items: flex-start; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
transition: background 0.1s; flex-wrap: wrap;
|
||||
}
|
||||
.dsc-row:last-child { border-bottom: none; }
|
||||
.dsc-row:hover { background: var(--surface-hover); }
|
||||
.dsc-row__info { flex: 1; min-width: 0; }
|
||||
|
||||
.discount-info {
|
||||
flex: 1;
|
||||
min-width: 8rem;
|
||||
}
|
||||
|
||||
.discount-meta {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.discount-badge {
|
||||
font-weight: 600;
|
||||
color: var(--p-primary-600, #4f46e5);
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 10%, transparent);
|
||||
padding: 0.2rem 0.6rem;
|
||||
border-radius: 1rem;
|
||||
font-size: 0.875rem;
|
||||
/* Badge de valor */
|
||||
.dsc-badge {
|
||||
font-size: 0.75rem; font-weight: 600;
|
||||
color: var(--primary-color,#6366f1);
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 10%, transparent);
|
||||
padding: 0.15rem 0.5rem; border-radius: 6px;
|
||||
white-space: nowrap;
|
||||
}
|
||||
</style>
|
||||
|
||||
/* Form de adição/edição */
|
||||
.dsc-form-row {
|
||||
padding: 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
}
|
||||
.dsc-form-row:last-child { border-bottom: none; }
|
||||
.dsc-form-row--editing {
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 3%, var(--surface-card));
|
||||
border-left: 3px solid color-mix(in srgb, var(--primary-color,#6366f1) 50%, transparent);
|
||||
}
|
||||
.dsc-form-row--new {
|
||||
background: var(--surface-ground);
|
||||
border-top: 1px dashed var(--surface-border);
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
/* ── Empty state ──────────────────────────────────── */
|
||||
.cfg-empty {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; padding: 2.5rem 1rem; text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
border: 1px dashed var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-ground);
|
||||
}
|
||||
</style>
|
||||
@@ -145,24 +145,16 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<!-- Header -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="cfg-icon-box">
|
||||
<i class="pi pi-exclamation-triangle text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-900 font-semibold text-lg">Exceções Financeiras</div>
|
||||
<div class="text-600 text-sm">
|
||||
Defina o que cobrar em situações excepcionais de cancelamento ou falta.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-exclamation-triangle" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Exceções Financeiras</div>
|
||||
<div class="cfg-subheader__sub">Defina o que cobrar em cancelamentos, faltas e situações excepcionais</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
@@ -172,154 +164,99 @@ onMounted(async () => {
|
||||
<template v-else>
|
||||
|
||||
<!-- Um card por tipo de exceção -->
|
||||
<Card v-for="et in exceptionTypes" :key="et.value">
|
||||
<template #content>
|
||||
<div v-for="et in exceptionTypes" :key="et.value" class="cfg-wrap">
|
||||
|
||||
<!-- Modo leitura -->
|
||||
<template v-if="editingType !== et.value">
|
||||
<div class="exception-row">
|
||||
<div class="exception-info">
|
||||
<div class="font-semibold text-900 text-base">{{ et.label }}</div>
|
||||
|
||||
<template v-if="recordFor(et.value)">
|
||||
<div class="text-sm text-600 mt-1">
|
||||
{{ summaryFor(recordFor(et.value)) }}
|
||||
<span
|
||||
v-if="et.value === 'patient_cancellation' && recordFor(et.value)?.min_hours_notice"
|
||||
class="text-500"
|
||||
>
|
||||
— cobrar apenas se cancelado com menos de
|
||||
{{ recordFor(et.value).min_hours_notice }}h de antecedência
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex gap-2 mt-2 flex-wrap">
|
||||
<Tag
|
||||
:value="chargeModeLabel(recordFor(et.value)?.charge_mode)"
|
||||
:severity="chargeModeSeverity[recordFor(et.value)?.charge_mode] ?? 'secondary'"
|
||||
/>
|
||||
<Tag
|
||||
v-if="isGlobalRecord(recordFor(et.value))"
|
||||
value="Regra da clínica"
|
||||
severity="secondary"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-sm text-500 mt-1 italic">
|
||||
Não configurado — comportamento padrão: não cobrar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
v-if="!isGlobalRecord(recordFor(et.value))"
|
||||
label="Configurar"
|
||||
icon="pi pi-cog"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="ml-auto flex-shrink-0"
|
||||
@click="startEdit(et.value)"
|
||||
<!-- Cabeçalho do card -->
|
||||
<div class="cfg-wrap__head">
|
||||
<div class="cfg-wrap__icon"><i class="pi pi-exclamation-triangle" /></div>
|
||||
<span class="cfg-wrap__title">{{ et.label }}</span>
|
||||
<div class="ml-auto flex items-center gap-2 shrink-0">
|
||||
<template v-if="recordFor(et.value)">
|
||||
<Tag
|
||||
:value="chargeModeLabel(recordFor(et.value)?.charge_mode)"
|
||||
:severity="chargeModeSeverity[recordFor(et.value)?.charge_mode] ?? 'secondary'"
|
||||
/>
|
||||
<Tag v-if="isGlobalRecord(recordFor(et.value))" value="Regra da clínica" severity="secondary" />
|
||||
</template>
|
||||
<Button
|
||||
v-if="editingType !== et.value && !isGlobalRecord(recordFor(et.value))"
|
||||
label="Configurar"
|
||||
icon="pi pi-cog"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
class="rounded-full"
|
||||
@click="startEdit(et.value)"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modo leitura -->
|
||||
<div v-if="editingType !== et.value" class="exc-read">
|
||||
<template v-if="recordFor(et.value)">
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">
|
||||
{{ summaryFor(recordFor(et.value)) }}
|
||||
<span v-if="et.value === 'patient_cancellation' && recordFor(et.value)?.min_hours_notice">
|
||||
— cobrar apenas se cancelado com menos de {{ recordFor(et.value).min_hours_notice }}h de antecedência
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<div v-else class="text-sm text-[var(--text-color-secondary)] italic">
|
||||
Não configurado — comportamento padrão: não cobrar.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modo edição inline -->
|
||||
<template v-else>
|
||||
<div class="exception-row editing">
|
||||
<div class="flex flex-col gap-4 flex-1">
|
||||
<!-- Modo edição -->
|
||||
<div v-else class="exc-edit">
|
||||
<!-- Modo de cobrança -->
|
||||
<div>
|
||||
<label class="exc-label">Modo de cobrança</label>
|
||||
<SelectButton
|
||||
v-model="editForm.charge_mode"
|
||||
:options="chargeModeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="flex-wrap mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="font-semibold text-900">{{ et.label }}</div>
|
||||
|
||||
<!-- Modo de cobrança -->
|
||||
<div>
|
||||
<label class="text-sm text-600 block mb-2">Modo de cobrança</label>
|
||||
<SelectButton
|
||||
v-model="editForm.charge_mode"
|
||||
:options="chargeModeOptions"
|
||||
optionLabel="label"
|
||||
optionValue="value"
|
||||
class="flex-wrap"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
|
||||
<!-- Taxa fixa (R$) -->
|
||||
<div v-if="showChargeValue" class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="editForm.charge_value"
|
||||
inputId="edit-charge-value"
|
||||
mode="currency"
|
||||
currency="BRL"
|
||||
locale="pt-BR"
|
||||
:min="0"
|
||||
fluid
|
||||
/>
|
||||
<label for="edit-charge-value">Taxa fixa (R$)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Percentual (%) -->
|
||||
<div v-if="showChargePct" class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="editForm.charge_pct"
|
||||
inputId="edit-charge-pct"
|
||||
:min="0"
|
||||
:max="100"
|
||||
:minFractionDigits="0"
|
||||
:maxFractionDigits="2"
|
||||
suffix="%"
|
||||
fluid
|
||||
/>
|
||||
<label for="edit-charge-pct">Percentual da sessão (%)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Antecedência mínima (apenas patient_cancellation) -->
|
||||
<div v-if="showMinHours" class="col-span-12 sm:col-span-5">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="editForm.min_hours_notice"
|
||||
inputId="edit-min-hours"
|
||||
:min="0"
|
||||
:max="720"
|
||||
suffix=" h"
|
||||
fluid
|
||||
showButtons
|
||||
/>
|
||||
<label for="edit-min-hours">Cobrar se cancelado com menos de X horas (vazio = sempre)</label>
|
||||
</FloatLabel>
|
||||
<small class="text-500 mt-1 block">
|
||||
Deixe em branco para cobrar independentemente da antecedência.
|
||||
</small>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex gap-2">
|
||||
<Button
|
||||
icon="pi pi-check"
|
||||
label="Salvar"
|
||||
size="small"
|
||||
:loading="savingEdit"
|
||||
@click="saveEdit"
|
||||
/>
|
||||
<Button
|
||||
icon="pi pi-times"
|
||||
size="small"
|
||||
severity="secondary"
|
||||
outlined
|
||||
@click="cancelEdit"
|
||||
/>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<!-- Taxa fixa -->
|
||||
<div v-if="showChargeValue" class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber v-model="editForm.charge_value" inputId="edit-charge-value" mode="currency" currency="BRL" locale="pt-BR" :min="0" fluid />
|
||||
<label for="edit-charge-value">Taxa fixa (R$)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</template>
|
||||
</Card>
|
||||
<!-- Percentual -->
|
||||
<div v-if="showChargePct" class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber v-model="editForm.charge_pct" inputId="edit-charge-pct" :min="0" :max="100" :minFractionDigits="0" :maxFractionDigits="2" suffix="%" fluid />
|
||||
<label for="edit-charge-pct">Percentual da sessão (%)</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
|
||||
<!-- Antecedência mínima -->
|
||||
<div v-if="showMinHours" class="col-span-12 sm:col-span-6">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber v-model="editForm.min_hours_notice" inputId="edit-min-hours" :min="0" :max="720" suffix=" h" fluid showButtons />
|
||||
<label for="edit-min-hours">Cobrar se cancelado com menos de X horas (vazio = sempre)</label>
|
||||
</FloatLabel>
|
||||
<small class="text-[var(--text-color-secondary)] opacity-70 mt-1 block text-xs">
|
||||
Deixe em branco para cobrar independentemente da antecedência.
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Botões na linha separada -->
|
||||
<div class="flex gap-2 justify-end mt-1">
|
||||
<Button label="Cancelar" icon="pi pi-times" size="small" severity="secondary" outlined class="rounded-full" @click="cancelEdit" />
|
||||
<Button label="Salvar" icon="pi pi-check" size="small" :loading="savingEdit" class="rounded-full" @click="saveEdit" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Dica -->
|
||||
<Message severity="info" :closable="false">
|
||||
@@ -334,33 +271,69 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cfg-icon-box {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.875rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
flex-shrink: 0;
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color,#6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute; top: -20px; right: -20px;
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 20%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color,#6366f1); }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
|
||||
/* ── Card wrap ────────────────────────────────────── */
|
||||
.cfg-wrap {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cfg-wrap__head {
|
||||
display: flex; align-items: center; gap: 0.625rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground); flex-wrap: wrap;
|
||||
}
|
||||
.cfg-wrap__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 12%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.8rem;
|
||||
}
|
||||
.cfg-wrap__title { font-size: 0.88rem; font-weight: 700; color: var(--text-color); }
|
||||
|
||||
/* ── Leitura ──────────────────────────────────────── */
|
||||
.exc-read {
|
||||
padding: 0.75rem 1rem;
|
||||
}
|
||||
|
||||
.exception-row {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 1rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.exception-row.editing {
|
||||
border: 1px solid var(--p-primary-300, #a5b4fc);
|
||||
border-radius: 0.75rem;
|
||||
/* ── Edição ───────────────────────────────────────── */
|
||||
.exc-edit {
|
||||
padding: 1rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 4%, var(--surface-ground));
|
||||
display: flex; flex-direction: column; gap: 1rem;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 3%, var(--surface-card));
|
||||
border-top: 2px solid color-mix(in srgb, var(--primary-color,#6366f1) 40%, transparent);
|
||||
}
|
||||
|
||||
.exception-info {
|
||||
flex: 1;
|
||||
min-width: 10rem;
|
||||
/* ── Label ────────────────────────────────────────── */
|
||||
.exc-label {
|
||||
display: block; font-size: 0.75rem; font-weight: 600;
|
||||
color: var(--text-color-secondary); margin-bottom: 0.375rem;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
@@ -10,7 +10,8 @@ const tenantStore = useTenantStore()
|
||||
|
||||
const loading = ref(true)
|
||||
const ownerId = ref(null)
|
||||
const expandedCard = ref(null)
|
||||
const CARDS = ['pix', 'deposito', 'dinheiro', 'cartao', 'convenio', 'observacoes']
|
||||
const expandedCard = ref(new Set())
|
||||
const savingCard = ref(null)
|
||||
|
||||
// ── Defaults ────────────────────────────────────────────────────
|
||||
@@ -84,8 +85,13 @@ const bancos = [
|
||||
]
|
||||
|
||||
// ── Toggle cards ─────────────────────────────────────────────────
|
||||
function expandAll () { expandedCard.value = new Set(CARDS) }
|
||||
function collapseAll () { expandedCard.value = new Set() }
|
||||
function toggleCard (key) {
|
||||
expandedCard.value = expandedCard.value === key ? null : key
|
||||
const s = new Set(expandedCard.value)
|
||||
if (s.has(key)) s.delete(key)
|
||||
else s.add(key)
|
||||
expandedCard.value = s
|
||||
}
|
||||
|
||||
// ── Load ─────────────────────────────────────────────────────────
|
||||
@@ -173,30 +179,40 @@ onMounted(load)
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div v-if="loading" class="flex items-center gap-2 p-6 text-slate-500">
|
||||
<div v-if="loading" class="flex items-center gap-2 p-6 text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-spin pi-spinner" /> Carregando…
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-wallet" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Pagamento</div>
|
||||
<div class="cfg-subheader__sub">Formas de pagamento aceitas: Pix, depósito, dinheiro, cartão e convênio</div>
|
||||
</div>
|
||||
<div class="cfg-subheader__actions">
|
||||
<Button size="small" icon="pi pi-arrows-v" label="Expandir" severity="secondary" outlined class="rounded-full" @click="expandAll" />
|
||||
<Button size="small" icon="pi pi-minus" label="Contrair" severity="secondary" outlined class="rounded-full" @click="collapseAll" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pix ──────────────────────────────────────────────────── -->
|
||||
<div class="rounded-2xl border bg-[var(--surface-card)] overflow-hidden"
|
||||
<div class="rounded-[6px] border bg-[var(--surface-card)] overflow-hidden"
|
||||
:class="cfg.pix_ativo ? 'border-green-300' : 'border-[var(--surface-border)]'">
|
||||
|
||||
<!-- Header -->
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 hover:bg-[var(--surface-hover)] transition text-left"
|
||||
@click="toggleCard('pix')"
|
||||
<button type="button" class="pag-accordion__header" @click="toggleCard('pix')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
||||
:class="cfg.pix_ativo ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-400'">
|
||||
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center shrink-0"
|
||||
:class="cfg.pix_ativo ? 'bg-green-100 text-green-700' : 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'">
|
||||
<i class="pi pi-qrcode text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-800">Pix</div>
|
||||
<div class="text-sm text-slate-500">
|
||||
<div class="font-semibold text-[var(--text-color)]">Pix</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">
|
||||
{{ cfg.pix_ativo && cfg.pix_chave ? `Chave: ${cfg.pix_chave}` : 'Pagamento instantâneo via chave Pix' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -204,21 +220,21 @@ onMounted(load)
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<Tag v-if="cfg.pix_ativo" value="Ativo" severity="success" />
|
||||
<Tag v-else value="Inativo" severity="secondary" />
|
||||
<i class="pi text-slate-400" :class="expandedCard === 'pix' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
<i class="pi text-[var(--text-color-secondary)]" :class="expandedCard.has('pix') ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<!-- Body -->
|
||||
<div v-if="expandedCard === 'pix'" class="border-t border-[var(--surface-border)] px-5 py-5 flex flex-col gap-4">
|
||||
<div v-if="expandedCard.has('pix')" class="pag-accordion__body">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-slate-700">Habilitar Pix</span>
|
||||
<span class="font-medium text-[var(--text-color)]">Habilitar Pix</span>
|
||||
<ToggleSwitch v-model="cfg.pix_ativo" />
|
||||
</div>
|
||||
|
||||
<template v-if="cfg.pix_ativo">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Tipo de chave</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Tipo de chave</label>
|
||||
<Select
|
||||
v-model="cfg.pix_tipo"
|
||||
:options="pixTipoOptions"
|
||||
@@ -228,13 +244,13 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">
|
||||
{{ pixTipoLabel[cfg.pix_tipo] || 'Chave' }}
|
||||
</label>
|
||||
<InputText v-model="cfg.pix_chave" class="w-full" :placeholder="pixTipoLabel[cfg.pix_tipo]" />
|
||||
</div>
|
||||
<div class="md:col-span-2">
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Nome do titular</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Nome do titular</label>
|
||||
<InputText v-model="cfg.pix_nome_titular" class="w-full" placeholder="Nome que aparece na chave" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -252,22 +268,19 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Depósito bancário ───────────────────────────────────── -->
|
||||
<div class="rounded-2xl border bg-[var(--surface-card)] overflow-hidden"
|
||||
<div class="rounded-[6px] border bg-[var(--surface-card)] overflow-hidden"
|
||||
:class="cfg.deposito_ativo ? 'border-blue-300' : 'border-[var(--surface-border)]'">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 hover:bg-[var(--surface-hover)] transition text-left"
|
||||
@click="toggleCard('deposito')"
|
||||
<button type="button" class="pag-accordion__header" @click="toggleCard('deposito')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
||||
:class="cfg.deposito_ativo ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-400'">
|
||||
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center shrink-0"
|
||||
:class="cfg.deposito_ativo ? 'bg-blue-100 text-blue-700' : 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'">
|
||||
<i class="pi pi-building-columns text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-800">Depósito / TED</div>
|
||||
<div class="text-sm text-slate-500">
|
||||
<div class="font-semibold text-[var(--text-color)]">Depósito / TED</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">
|
||||
{{ cfg.deposito_ativo && cfg.deposito_banco ? `${cfg.deposito_banco} · Ag. ${cfg.deposito_agencia || '—'} · Conta ${cfg.deposito_conta || '—'}` : 'Transferência bancária ou depósito' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -275,20 +288,20 @@ onMounted(load)
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<Tag v-if="cfg.deposito_ativo" value="Ativo" severity="success" />
|
||||
<Tag v-else value="Inativo" severity="secondary" />
|
||||
<i class="pi text-slate-400" :class="expandedCard === 'deposito' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
<i class="pi text-[var(--text-color-secondary)]" :class="expandedCard.has('deposito') ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="expandedCard === 'deposito'" class="border-t border-[var(--surface-border)] px-5 py-5 flex flex-col gap-4">
|
||||
<div v-if="expandedCard.has('deposito')" class="pag-accordion__body">
|
||||
<div class="flex items-center justify-between">
|
||||
<span class="font-medium text-slate-700">Habilitar Depósito / TED</span>
|
||||
<span class="font-medium text-[var(--text-color)]">Habilitar Depósito / TED</span>
|
||||
<ToggleSwitch v-model="cfg.deposito_ativo" />
|
||||
</div>
|
||||
|
||||
<template v-if="cfg.deposito_ativo">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Banco</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Banco</label>
|
||||
<Select
|
||||
v-model="cfg.deposito_banco"
|
||||
:options="bancos"
|
||||
@@ -300,7 +313,7 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Tipo de conta</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Tipo de conta</label>
|
||||
<Select
|
||||
v-model="cfg.deposito_tipo_conta"
|
||||
:options="tipoConta"
|
||||
@@ -310,19 +323,19 @@ onMounted(load)
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Agência</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Agência</label>
|
||||
<InputText v-model="cfg.deposito_agencia" class="w-full" placeholder="0000" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Conta</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Conta</label>
|
||||
<InputText v-model="cfg.deposito_conta" class="w-full" placeholder="00000-0" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Titular</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Titular</label>
|
||||
<InputText v-model="cfg.deposito_titular" class="w-full" placeholder="Nome completo ou razão social" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">CPF / CNPJ do titular</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">CPF / CNPJ do titular</label>
|
||||
<InputText v-model="cfg.deposito_cpf_cnpj" class="w-full" placeholder="000.000.000-00" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -340,36 +353,33 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Dinheiro ─────────────────────────────────────────────── -->
|
||||
<div class="rounded-2xl border bg-[var(--surface-card)] overflow-hidden"
|
||||
<div class="rounded-[6px] border bg-[var(--surface-card)] overflow-hidden"
|
||||
:class="cfg.dinheiro_ativo ? 'border-yellow-300' : 'border-[var(--surface-border)]'">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 hover:bg-[var(--surface-hover)] transition text-left"
|
||||
@click="toggleCard('dinheiro')"
|
||||
<button type="button" class="pag-accordion__header" @click="toggleCard('dinheiro')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
||||
:class="cfg.dinheiro_ativo ? 'bg-yellow-100 text-yellow-700' : 'bg-slate-100 text-slate-400'">
|
||||
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center shrink-0"
|
||||
:class="cfg.dinheiro_ativo ? 'bg-yellow-100 text-yellow-700' : 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'">
|
||||
<i class="pi pi-wallet text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-800">Dinheiro (espécie)</div>
|
||||
<div class="text-sm text-slate-500">Pagamento presencial em dinheiro</div>
|
||||
<div class="font-semibold text-[var(--text-color)]">Dinheiro (espécie)</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">Pagamento presencial em dinheiro</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<Tag v-if="cfg.dinheiro_ativo" value="Ativo" severity="success" />
|
||||
<Tag v-else value="Inativo" severity="secondary" />
|
||||
<i class="pi text-slate-400" :class="expandedCard === 'dinheiro' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
<i class="pi text-[var(--text-color-secondary)]" :class="expandedCard.has('dinheiro') ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="expandedCard === 'dinheiro'" class="border-t border-[var(--surface-border)] px-5 py-5 flex flex-col gap-4">
|
||||
<div v-if="expandedCard.has('dinheiro')" class="pag-accordion__body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-slate-700">Habilitar pagamento em dinheiro</span>
|
||||
<p class="text-sm text-slate-500 mt-1">
|
||||
<span class="font-medium text-[var(--text-color)]">Habilitar pagamento em dinheiro</span>
|
||||
<p class="text-sm text-[var(--text-color-secondary)] mt-1">
|
||||
Aceitar pagamento em espécie nas sessões presenciais.
|
||||
</p>
|
||||
</div>
|
||||
@@ -388,36 +398,33 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Cartão ───────────────────────────────────────────────── -->
|
||||
<div class="rounded-2xl border bg-[var(--surface-card)] overflow-hidden"
|
||||
<div class="rounded-[6px] border bg-[var(--surface-card)] overflow-hidden"
|
||||
:class="cfg.cartao_ativo ? 'border-purple-300' : 'border-[var(--surface-border)]'">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 hover:bg-[var(--surface-hover)] transition text-left"
|
||||
@click="toggleCard('cartao')"
|
||||
<button type="button" class="pag-accordion__header" @click="toggleCard('cartao')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
||||
:class="cfg.cartao_ativo ? 'bg-purple-100 text-purple-700' : 'bg-slate-100 text-slate-400'">
|
||||
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center shrink-0"
|
||||
:class="cfg.cartao_ativo ? 'bg-purple-100 text-purple-700' : 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'">
|
||||
<i class="pi pi-credit-card text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-800">Cartão (maquininha)</div>
|
||||
<div class="text-sm text-slate-500">Crédito e débito presencial</div>
|
||||
<div class="font-semibold text-[var(--text-color)]">Cartão (maquininha)</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">Crédito e débito presencial</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<Tag v-if="cfg.cartao_ativo" value="Ativo" severity="success" />
|
||||
<Tag v-else value="Inativo" severity="secondary" />
|
||||
<i class="pi text-slate-400" :class="expandedCard === 'cartao' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
<i class="pi text-[var(--text-color-secondary)]" :class="expandedCard.has('cartao') ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="expandedCard === 'cartao'" class="border-t border-[var(--surface-border)] px-5 py-5 flex flex-col gap-4">
|
||||
<div v-if="expandedCard.has('cartao')" class="pag-accordion__body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-slate-700">Habilitar pagamento por cartão</span>
|
||||
<p class="text-sm text-slate-500 mt-1">
|
||||
<span class="font-medium text-[var(--text-color)]">Habilitar pagamento por cartão</span>
|
||||
<p class="text-sm text-[var(--text-color-secondary)] mt-1">
|
||||
Aceitar cartão de crédito e débito via maquininha nas sessões presenciais.
|
||||
</p>
|
||||
</div>
|
||||
@@ -426,7 +433,7 @@ onMounted(load)
|
||||
|
||||
<template v-if="cfg.cartao_ativo">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Instrução ao paciente <span class="text-slate-400">(opcional)</span></label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Instrução ao paciente <span class="text-[var(--text-color-secondary)]">(opcional)</span></label>
|
||||
<InputText
|
||||
v-model="cfg.cartao_instrucao"
|
||||
class="w-full"
|
||||
@@ -447,22 +454,19 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Plano de Saúde / Convênio ────────────────────────────── -->
|
||||
<div class="rounded-2xl border bg-[var(--surface-card)] overflow-hidden"
|
||||
<div class="rounded-[6px] border bg-[var(--surface-card)] overflow-hidden"
|
||||
:class="cfg.convenio_ativo ? 'border-teal-300' : 'border-[var(--surface-border)]'">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 hover:bg-[var(--surface-hover)] transition text-left"
|
||||
@click="toggleCard('convenio')"
|
||||
<button type="button" class="pag-accordion__header" @click="toggleCard('convenio')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl flex items-center justify-center shrink-0"
|
||||
:class="cfg.convenio_ativo ? 'bg-teal-100 text-teal-700' : 'bg-slate-100 text-slate-400'">
|
||||
<div class="w-10 h-10 rounded-[6px] flex items-center justify-center shrink-0"
|
||||
:class="cfg.convenio_ativo ? 'bg-teal-100 text-teal-700' : 'bg-[var(--surface-ground)] text-[var(--text-color-secondary)]'">
|
||||
<i class="pi pi-heart text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-800">Plano de saúde / Convênio</div>
|
||||
<div class="text-sm text-slate-500">
|
||||
<div class="font-semibold text-[var(--text-color)]">Plano de saúde / Convênio</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">
|
||||
{{ cfg.convenio_ativo && cfg.convenio_lista ? cfg.convenio_lista.slice(0, 60) + (cfg.convenio_lista.length > 60 ? '…' : '') : 'Atendimento por convênio' }}
|
||||
</div>
|
||||
</div>
|
||||
@@ -470,15 +474,15 @@ onMounted(load)
|
||||
<div class="flex items-center gap-3 shrink-0">
|
||||
<Tag v-if="cfg.convenio_ativo" value="Ativo" severity="success" />
|
||||
<Tag v-else value="Inativo" severity="secondary" />
|
||||
<i class="pi text-slate-400" :class="expandedCard === 'convenio' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
<i class="pi text-[var(--text-color-secondary)]" :class="expandedCard.has('convenio') ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div v-if="expandedCard === 'convenio'" class="border-t border-[var(--surface-border)] px-5 py-5 flex flex-col gap-4">
|
||||
<div v-if="expandedCard.has('convenio')" class="pag-accordion__body">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<span class="font-medium text-slate-700">Aceitar plano de saúde / convênio</span>
|
||||
<p class="text-sm text-slate-500 mt-1">
|
||||
<span class="font-medium text-[var(--text-color)]">Aceitar plano de saúde / convênio</span>
|
||||
<p class="text-sm text-[var(--text-color-secondary)] mt-1">
|
||||
Habilite para informar quais convênios são aceitos.
|
||||
</p>
|
||||
</div>
|
||||
@@ -487,7 +491,7 @@ onMounted(load)
|
||||
|
||||
<template v-if="cfg.convenio_ativo">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-slate-600 mb-1">Convênios aceitos</label>
|
||||
<label class="block text-sm font-medium text-[var(--text-color)] mb-1">Convênios aceitos</label>
|
||||
<Textarea
|
||||
v-model="cfg.convenio_lista"
|
||||
rows="3"
|
||||
@@ -495,7 +499,7 @@ onMounted(load)
|
||||
placeholder="Ex: Unimed, Bradesco Saúde, Amil, SulAmérica..."
|
||||
autoResize
|
||||
/>
|
||||
<small class="text-slate-400">Liste os convênios separados por vírgula ou um por linha.</small>
|
||||
<small class="text-[var(--text-color-secondary)]">Liste os convênios separados por vírgula ou um por linha.</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -511,26 +515,23 @@ onMounted(load)
|
||||
</div>
|
||||
|
||||
<!-- Observações gerais ───────────────────────────────────── -->
|
||||
<div class="rounded-2xl border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<div class="pag-accordion">
|
||||
|
||||
<button
|
||||
type="button"
|
||||
class="w-full flex items-center justify-between gap-4 px-5 py-4 hover:bg-[var(--surface-hover)] transition text-left"
|
||||
@click="toggleCard('observacoes')"
|
||||
<button type="button" class="pag-accordion__header" @click="toggleCard('observacoes')"
|
||||
>
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="w-10 h-10 rounded-xl bg-slate-100 text-slate-400 flex items-center justify-center shrink-0">
|
||||
<div class="pag-accordion__icon bg-[var(--surface-ground)] text-[var(--text-color-secondary)]">
|
||||
<i class="pi pi-comment text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-slate-800">Observações ao paciente</div>
|
||||
<div class="text-sm text-slate-500">Texto exibido junto às formas de pagamento</div>
|
||||
<div class="font-semibold text-[var(--text-color)]">Observações ao paciente</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)]">Texto exibido junto às formas de pagamento</div>
|
||||
</div>
|
||||
</div>
|
||||
<i class="pi text-slate-400" :class="expandedCard === 'observacoes' ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
<i class="pi text-[var(--text-color-secondary)]" :class="expandedCard.has('observacoes') ? 'pi-angle-up' : 'pi-angle-down'" />
|
||||
</button>
|
||||
|
||||
<div v-if="expandedCard === 'observacoes'" class="border-t border-[var(--surface-border)] px-5 py-5 flex flex-col gap-4">
|
||||
<div v-if="expandedCard.has('observacoes')" class="pag-accordion__body">
|
||||
<Textarea
|
||||
v-model="cfg.observacoes_pagamento"
|
||||
rows="4"
|
||||
@@ -551,3 +552,87 @@ onMounted(load)
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color,#6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute; top: -20px; right: -20px;
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 20%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color,#6366f1); }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
|
||||
/* ── Card wrap ────────────────────────────────────── */
|
||||
.cfg-wrap {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cfg-wrap__head {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 0.75rem; padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.cfg-wrap__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 12%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.8rem;
|
||||
}
|
||||
.cfg-wrap__title { font-size: 0.88rem; font-weight: 700; color: var(--text-color); }
|
||||
.cfg-wrap__body { padding: 1rem; display: flex; flex-direction: column; gap: 0.75rem; }
|
||||
|
||||
/* ── Empty state ──────────────────────────────────── */
|
||||
.cfg-empty {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; padding: 2.5rem 1rem; text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
border: 1px dashed var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-ground);
|
||||
}
|
||||
|
||||
/* ── Pagamento accordions ─────────────────────────── */
|
||||
.pag-accordion {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-card);
|
||||
overflow: hidden; transition: border-color 0.15s;
|
||||
}
|
||||
.pag-accordion--on {
|
||||
border-color: color-mix(in srgb, #22c55e 40%, transparent);
|
||||
}
|
||||
.pag-accordion__header {
|
||||
display: flex; align-items: center; justify-content: space-between;
|
||||
gap: 1rem; width: 100%; padding: 0.875rem 1rem;
|
||||
background: transparent; border: none; cursor: pointer;
|
||||
transition: background 0.12s; text-align: left;
|
||||
}
|
||||
.pag-accordion__header:hover { background: var(--surface-hover); }
|
||||
.pag-accordion__icon {
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
width: 2.25rem; height: 2.25rem; border-radius: 6px; flex-shrink: 0;
|
||||
}
|
||||
.pag-accordion__body {
|
||||
border-top: 1px solid var(--surface-border);
|
||||
padding: 1rem; display: flex; flex-direction: column; gap: 1rem;
|
||||
}
|
||||
</style>
|
||||
@@ -148,32 +148,19 @@ onMounted(async () => {
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
<!-- Header -->
|
||||
<Card>
|
||||
<template #content>
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="cfg-icon-box">
|
||||
<i class="pi pi-tag text-lg" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="text-900 font-semibold text-lg">Serviços e Precificação</div>
|
||||
<div class="text-600 text-sm">
|
||||
Gerencie os serviços que você oferece e seus respectivos preços.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
label="Novo serviço"
|
||||
icon="pi pi-plus"
|
||||
:disabled="pageLoading || addingNew"
|
||||
@click="addingNew = true; cancelEdit()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<div class="cfg-subheader__icon"><i class="pi pi-tag" /></div>
|
||||
<div class="min-w-0">
|
||||
<div class="cfg-subheader__title">Precificação</div>
|
||||
<div class="cfg-subheader__sub">Valor padrão da sessão e preços por tipo de compromisso</div>
|
||||
</div>
|
||||
<div class="cfg-subheader__actions">
|
||||
<Button label="Novo serviço" icon="pi pi-plus" size="small" :disabled="pageLoading || addingNew" class="rounded-full" @click="addingNew = true; cancelEdit()" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
@@ -183,20 +170,16 @@ onMounted(async () => {
|
||||
<template v-else>
|
||||
|
||||
<Message v-if="isDynamic" severity="info" :closable="false">
|
||||
<span class="text-sm">
|
||||
Modo <b>dinâmico</b> ativo — a duração da sessão é definida pelo serviço selecionado.
|
||||
</span>
|
||||
<span class="text-sm">Modo <b>dinâmico</b> ativo — a duração da sessão é definida pelo serviço selecionado.</span>
|
||||
</Message>
|
||||
|
||||
<!-- Formulário novo serviço -->
|
||||
<Card v-if="addingNew">
|
||||
<template #title>
|
||||
<div class="flex items-center gap-2">
|
||||
<i class="pi pi-plus-circle text-primary-500" />
|
||||
<span>Novo serviço</span>
|
||||
</div>
|
||||
</template>
|
||||
<template #content>
|
||||
<!-- Form novo serviço -->
|
||||
<div v-if="addingNew" class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<div class="cfg-wrap__icon"><i class="pi pi-plus" /></div>
|
||||
<span class="cfg-wrap__title">Novo serviço</span>
|
||||
</div>
|
||||
<div class="svc-form">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
@@ -206,16 +189,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-3">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="newForm.price"
|
||||
inputId="new-price"
|
||||
mode="currency"
|
||||
currency="BRL"
|
||||
locale="pt-BR"
|
||||
:min="0"
|
||||
:minFractionDigits="2"
|
||||
fluid
|
||||
/>
|
||||
<InputNumber v-model="newForm.price" inputId="new-price" mode="currency" currency="BRL" locale="pt-BR" :min="0" :minFractionDigits="2" fluid />
|
||||
<label for="new-price">Preço (R$) *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
@@ -232,30 +206,59 @@ onMounted(async () => {
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="addingNew = false; newForm = emptyForm()" />
|
||||
<Button label="Salvar" icon="pi pi-check" :loading="savingNew" @click="saveNew" />
|
||||
<div class="flex gap-2 justify-end mt-3">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="addingNew = false; newForm = emptyForm()" />
|
||||
<Button label="Salvar" icon="pi pi-check" class="rounded-full" :loading="savingNew" @click="saveNew" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista vazia -->
|
||||
<Card v-if="!services.length && !addingNew">
|
||||
<template #content>
|
||||
<div class="text-center py-6 text-color-secondary">
|
||||
<i class="pi pi-tag text-4xl opacity-30 mb-3 block" />
|
||||
<div class="font-medium mb-1">Nenhum serviço cadastrado</div>
|
||||
<div class="text-sm">Clique em "Novo serviço" para começar.</div>
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
<div v-if="!services.length && !addingNew" class="cfg-empty">
|
||||
<i class="pi pi-tag text-3xl opacity-25" />
|
||||
<div class="text-sm font-medium">Nenhum serviço cadastrado</div>
|
||||
<div class="text-xs opacity-70">Clique em "Novo serviço" para começar.</div>
|
||||
</div>
|
||||
|
||||
<!-- Lista de serviços -->
|
||||
<Card v-for="svc in services" :key="svc.id" :class="{ 'opacity-60': !svc.active }">
|
||||
<template #content>
|
||||
<div v-for="svc in services" :key="svc.id" class="cfg-wrap" :class="{ 'opacity-60': !svc.active }">
|
||||
|
||||
<!-- Modo edição -->
|
||||
<template v-if="editingId === svc.id">
|
||||
<!-- Modo leitura: head clicável -->
|
||||
<template v-if="editingId !== svc.id">
|
||||
<div class="svc-row">
|
||||
<div class="svc-row__icon">
|
||||
<i class="pi pi-tag" />
|
||||
</div>
|
||||
<div class="svc-row__info">
|
||||
<div class="font-semibold text-sm">{{ svc.name }}</div>
|
||||
<div class="flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5 text-xs text-[var(--text-color-secondary)]">
|
||||
<span class="font-semibold text-[var(--primary-color)]">{{ fmtBRL(svc.price) }}</span>
|
||||
<span v-if="svc.duration_min">{{ svc.duration_min }} min</span>
|
||||
<span v-if="svc.description" class="italic">{{ svc.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-1.5 shrink-0">
|
||||
<Tag :value="svc.active ? 'Ativo' : 'Inativo'" :severity="svc.active ? 'success' : 'secondary'" />
|
||||
<Button
|
||||
:icon="svc.active ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
:severity="svc.active ? 'secondary' : 'success'"
|
||||
outlined size="small"
|
||||
v-tooltip.top="svc.active ? 'Desativar' : 'Ativar'"
|
||||
@click="toggleService(svc)"
|
||||
/>
|
||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" v-tooltip.top="'Editar'" @click="startEdit(svc)" />
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small" v-tooltip.top="'Remover'" @click="confirmRemove(svc.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Modo edição -->
|
||||
<template v-else>
|
||||
<div class="cfg-wrap__head">
|
||||
<div class="cfg-wrap__icon"><i class="pi pi-pencil" /></div>
|
||||
<span class="cfg-wrap__title">Editar — {{ svc.name }}</span>
|
||||
</div>
|
||||
<div class="svc-form svc-form--editing">
|
||||
<div class="grid grid-cols-12 gap-3">
|
||||
<div class="col-span-12 sm:col-span-4">
|
||||
<FloatLabel variant="on">
|
||||
@@ -265,16 +268,7 @@ onMounted(async () => {
|
||||
</div>
|
||||
<div class="col-span-12 sm:col-span-3">
|
||||
<FloatLabel variant="on">
|
||||
<InputNumber
|
||||
v-model="editForm.price"
|
||||
:inputId="`edit-price-${svc.id}`"
|
||||
mode="currency"
|
||||
currency="BRL"
|
||||
locale="pt-BR"
|
||||
:min="0"
|
||||
:minFractionDigits="2"
|
||||
fluid
|
||||
/>
|
||||
<InputNumber v-model="editForm.price" :inputId="`edit-price-${svc.id}`" mode="currency" currency="BRL" locale="pt-BR" :min="0" :minFractionDigits="2" fluid />
|
||||
<label :for="`edit-price-${svc.id}`">Preço (R$) *</label>
|
||||
</FloatLabel>
|
||||
</div>
|
||||
@@ -291,51 +285,17 @@ onMounted(async () => {
|
||||
</FloatLabel>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex gap-2 justify-end mt-4">
|
||||
<Button label="Cancelar" severity="secondary" outlined @click="cancelEdit" />
|
||||
<Button label="Salvar" icon="pi pi-check" :loading="savingEdit" @click="saveEdit" />
|
||||
<div class="flex gap-2 justify-end mt-3">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="cancelEdit" />
|
||||
<Button label="Salvar" icon="pi pi-check" class="rounded-full" :loading="savingEdit" @click="saveEdit" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Modo leitura -->
|
||||
<template v-else>
|
||||
<div class="flex items-center justify-between gap-3 flex-wrap">
|
||||
<div class="flex items-center gap-3">
|
||||
<div class="cfg-icon-box-sm">
|
||||
<i class="pi pi-tag" />
|
||||
</div>
|
||||
<div>
|
||||
<div class="font-semibold text-900">{{ svc.name }}</div>
|
||||
<div class="text-sm text-color-secondary flex flex-wrap gap-x-3 gap-y-0.5 mt-0.5">
|
||||
<span><b class="text-primary-500">{{ fmtBRL(svc.price) }}</b></span>
|
||||
<span v-if="svc.duration_min">{{ svc.duration_min }}min</span>
|
||||
<span v-if="svc.description" class="italic">{{ svc.description }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Tag :value="svc.active ? 'Ativo' : 'Inativo'" :severity="svc.active ? 'success' : 'secondary'" />
|
||||
<Button
|
||||
:icon="svc.active ? 'pi pi-eye-slash' : 'pi pi-eye'"
|
||||
:severity="svc.active ? 'secondary' : 'success'"
|
||||
outlined
|
||||
size="small"
|
||||
v-tooltip.top="svc.active ? 'Desativar' : 'Ativar'"
|
||||
@click="toggleService(svc)"
|
||||
/>
|
||||
<Button icon="pi pi-pencil" severity="secondary" outlined size="small" v-tooltip.top="'Editar'" @click="startEdit(svc)" />
|
||||
<Button icon="pi pi-trash" severity="danger" outlined size="small" v-tooltip.top="'Remover'" @click="confirmRemove(svc.id)" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
|
||||
</div>
|
||||
|
||||
<Message severity="info" :closable="false">
|
||||
<span class="text-sm">
|
||||
Os serviços cadastrados aqui aparecem para seleção ao criar ou editar um evento na agenda.
|
||||
</span>
|
||||
<span class="text-sm">Os serviços cadastrados aqui aparecem para seleção ao criar ou editar um evento na agenda.</span>
|
||||
</Message>
|
||||
|
||||
</template>
|
||||
@@ -343,25 +303,84 @@ onMounted(async () => {
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.cfg-icon-box {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 0.875rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 12%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
flex-shrink: 0;
|
||||
/* ── Subheader degradê ────────────────────────────── */
|
||||
.cfg-subheader {
|
||||
display: flex; align-items: center; gap: 0.65rem;
|
||||
padding: 0.875rem 1rem; border-radius: 6px;
|
||||
border: 1px solid color-mix(in srgb, var(--primary-color,#6366f1) 30%, transparent);
|
||||
background: linear-gradient(135deg,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 12%, var(--surface-card)) 0%,
|
||||
color-mix(in srgb, var(--primary-color,#6366f1) 4%, var(--surface-card)) 60%,
|
||||
var(--surface-card) 100%);
|
||||
position: relative; overflow: hidden;
|
||||
}
|
||||
.cfg-subheader::before {
|
||||
content: ''; position: absolute; top: -20px; right: -20px;
|
||||
width: 80px; height: 80px; border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 15%, transparent);
|
||||
filter: blur(20px); pointer-events: none;
|
||||
}
|
||||
.cfg-subheader__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 2rem; height: 2rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 20%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.85rem;
|
||||
}
|
||||
.cfg-subheader__title { font-size: 0.95rem; font-weight: 700; letter-spacing: -0.01em; color: var(--primary-color,#6366f1); }
|
||||
.cfg-subheader__sub { font-size: 0.75rem; color: var(--text-color-secondary); opacity: 0.85; }
|
||||
.cfg-subheader__actions { display: flex; align-items: center; gap: 0.5rem; margin-left: auto; flex-shrink: 0; position: relative; z-index: 1; }
|
||||
|
||||
/* ── Card wrap ────────────────────────────────────── */
|
||||
.cfg-wrap {
|
||||
border: 1px solid var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-card);
|
||||
overflow: hidden;
|
||||
}
|
||||
.cfg-wrap__head {
|
||||
display: flex; align-items: center; gap: 0.625rem;
|
||||
padding: 0.75rem 1rem;
|
||||
border-bottom: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
}
|
||||
.cfg-wrap__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 12%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.8rem;
|
||||
}
|
||||
.cfg-wrap__title { font-size: 0.88rem; font-weight: 700; color: var(--text-color); }
|
||||
|
||||
/* ── Linha de leitura do serviço ──────────────────── */
|
||||
.svc-row {
|
||||
display: flex; align-items: center; gap: 0.75rem;
|
||||
padding: 0.75rem 1rem; flex-wrap: wrap;
|
||||
transition: background 0.1s;
|
||||
}
|
||||
.svc-row:hover { background: var(--surface-hover); }
|
||||
.svc-row__icon {
|
||||
display: grid; place-items: center;
|
||||
width: 1.75rem; height: 1.75rem; border-radius: 6px; flex-shrink: 0;
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 10%, transparent);
|
||||
color: var(--primary-color,#6366f1); font-size: 0.78rem;
|
||||
}
|
||||
.svc-row__info { flex: 1; min-width: 0; }
|
||||
|
||||
/* ── Form (novo + edição) ─────────────────────────── */
|
||||
.svc-form {
|
||||
padding: 1rem;
|
||||
display: flex; flex-direction: column; gap: 0.75rem;
|
||||
}
|
||||
.svc-form--editing {
|
||||
background: color-mix(in srgb, var(--primary-color,#6366f1) 3%, var(--surface-card));
|
||||
border-top: 2px solid color-mix(in srgb, var(--primary-color,#6366f1) 40%, transparent);
|
||||
}
|
||||
|
||||
.cfg-icon-box-sm {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 2rem;
|
||||
height: 2rem;
|
||||
border-radius: 0.625rem;
|
||||
background: color-mix(in srgb, var(--p-primary-500, #6366f1) 10%, transparent);
|
||||
color: var(--p-primary-500, #6366f1);
|
||||
flex-shrink: 0;
|
||||
/* ── Empty state ──────────────────────────────────── */
|
||||
.cfg-empty {
|
||||
display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||
gap: 0.75rem; padding: 2.5rem 1rem; text-align: center;
|
||||
color: var(--text-color-secondary);
|
||||
border: 1px dashed var(--surface-border);
|
||||
border-radius: 6px; background: var(--surface-ground);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user