Layout 100%, Notificações, SetupWizard

This commit is contained in:
Leonardo
2026-03-17 21:08:14 -03:00
parent 84d65e49c0
commit 66f67cd40f
77 changed files with 35823 additions and 15023 deletions
+130 -185
View File
@@ -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" />
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" /> 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>
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>