Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
@@ -1,20 +1,37 @@
|
||||
<!-- src/layout/configuracoes/BloqueiosPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/BloqueiosPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
import { useTenantStore } from '@/stores/tenantStore'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { useFeriados } from '@/composables/useFeriados'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
const tenantStore = useTenantStore()
|
||||
|
||||
// ── Feriados (nacionais + municipais) ───────────────────────
|
||||
const { nacionais, municipais, todos, loading: loadingF, load: loadFeriados, criar: criarFeriado, remover: removerFeriado, isDuplicata } = useFeriados()
|
||||
|
||||
// ── Estado bloqueios ────────────────────────────────────────
|
||||
const loadingB = ref(false)
|
||||
const loadingB = ref(true)
|
||||
const saving = ref(false)
|
||||
|
||||
const ownerId = ref(null)
|
||||
@@ -164,13 +181,23 @@ async function salvarFeriado () {
|
||||
}
|
||||
}
|
||||
|
||||
async function excluirFeriado (id) {
|
||||
try {
|
||||
await removerFeriado(id)
|
||||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
||||
}
|
||||
function excluirFeriado (id) {
|
||||
confirm.require({
|
||||
message: 'Deseja remover este feriado municipal?',
|
||||
header: 'Confirmar remoção',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Cancelar',
|
||||
acceptLabel: 'Remover',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
await removerFeriado(id)
|
||||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── Bloqueio CRUD ─────────────────────────────────────────────
|
||||
@@ -236,15 +263,26 @@ async function salvarBloqueio () {
|
||||
}
|
||||
}
|
||||
|
||||
async function excluirBloqueio (id) {
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').delete().eq('id', id)
|
||||
if (error) throw error
|
||||
bloqueios.value = bloqueios.value.filter(b => b.id !== id)
|
||||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
||||
}
|
||||
function excluirBloqueio (id) {
|
||||
const b = bloqueios.value.find(x => x.id === id)
|
||||
confirm.require({
|
||||
message: b?.titulo ? `Remover o bloqueio "${b.titulo}"?` : 'Deseja remover este bloqueio?',
|
||||
header: 'Confirmar remoção',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Cancelar',
|
||||
acceptLabel: 'Remover',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: async () => {
|
||||
try {
|
||||
const { error } = await supabase.from('agenda_bloqueios').delete().eq('id', id)
|
||||
if (error) throw error
|
||||
bloqueios.value = bloqueios.value.filter(b => b.id !== id)
|
||||
toast.add({ severity: 'success', summary: 'Removido', life: 1500 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message, life: 3000 })
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// ── fmtPeriodo ────────────────────────────────────────────────
|
||||
@@ -262,10 +300,12 @@ function fmtPeriodo (b) {
|
||||
}
|
||||
|
||||
const loading = computed(() => loadingF.value || loadingB.value)
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<ConfirmDialog />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -304,10 +344,50 @@ const loading = computed(() => loadingF.value || loadingB.value)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 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>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="loading">
|
||||
|
||||
<!-- Grupo skeleton: Feriados Nacionais — frases aqui -->
|
||||
<div class="blk-group">
|
||||
<div class="blk-group__head">
|
||||
<Skeleton shape="circle" size="1.6rem" />
|
||||
<Skeleton width="9rem" height="0.85rem" />
|
||||
<Skeleton width="2rem" height="1.2rem" border-radius="999px" />
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando bloqueios e feriados..." containerClass="py-10" />
|
||||
</div>
|
||||
|
||||
<!-- Grupo skeleton: Feriados Municipais -->
|
||||
<div class="blk-group">
|
||||
<div class="blk-group__head">
|
||||
<Skeleton shape="circle" size="1.6rem" />
|
||||
<Skeleton width="10rem" height="0.85rem" />
|
||||
<Skeleton width="2rem" height="1.2rem" border-radius="999px" />
|
||||
</div>
|
||||
<div class="blk-list">
|
||||
<div v-for="i in 3" :key="i" class="blk-item">
|
||||
<Skeleton width="4rem" height="0.75rem" />
|
||||
<Skeleton :width="`${8 + (i % 2) * 5}rem`" height="0.75rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Grupo skeleton: Bloqueios -->
|
||||
<div class="blk-group">
|
||||
<div class="blk-group__head">
|
||||
<Skeleton shape="circle" size="1.6rem" />
|
||||
<Skeleton width="5rem" height="0.85rem" />
|
||||
<Skeleton width="2rem" height="1.2rem" border-radius="999px" />
|
||||
</div>
|
||||
<div class="blk-list">
|
||||
<div v-for="i in 4" :key="i" class="blk-item">
|
||||
<Skeleton width="4rem" height="0.75rem" />
|
||||
<Skeleton :width="`${7 + (i % 3) * 3}rem`" height="0.75rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
@@ -380,6 +460,9 @@ const loading = computed(() => loadingF.value || loadingB.value)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bloco pós-carregamento -->
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -559,4 +642,5 @@ const loading = computed(() => loadingF.value || loadingB.value)
|
||||
|
||||
/* ── Dialog labels ──────────────────────────────── */
|
||||
.blk-label { font-size: 0.75rem; color: var(--text-color-secondary); font-weight: 500; }
|
||||
|
||||
</style>
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesAgendaPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesAgendaPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { computed, ref, watch, onMounted, nextTick } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -70,6 +85,9 @@ const jornadaPorDia = ref({
|
||||
0: { inicio: '08:00', fim: '18:00' }
|
||||
})
|
||||
|
||||
// Snapshot dos horários por dia do modo "diferente" — preservado ao alternar modos
|
||||
const jornadaPorDiaSnapshot = ref(null)
|
||||
|
||||
// ── Pausas ──────────────────────────────────────────────────────
|
||||
const pausasGlobais = ref([])
|
||||
const pausasPorDia = ref({ 1: [], 2: [], 3: [], 4: [], 5: [], 6: [], 0: [] })
|
||||
@@ -224,6 +242,14 @@ function getPausasForDay (dayValue) {
|
||||
|
||||
// ── Toggle igual/diferente ─────────────────────────────────────
|
||||
function switchToIgual () {
|
||||
// Salva o estado atual de "diferente" no snapshot antes de sair
|
||||
jornadaPorDiaSnapshot.value = {}
|
||||
selectedDays.value.forEach(d => {
|
||||
if (jornadaPorDia.value[d.value])
|
||||
jornadaPorDiaSnapshot.value[d.value] = { ...jornadaPorDia.value[d.value] }
|
||||
})
|
||||
|
||||
// jornadaStart/jornadaEnd ficam intocados — eram os valores do modo "igual"
|
||||
if (isValidHHMM(jornadaStart.value) && isValidHHMM(jornadaEnd.value)) {
|
||||
// Sync apenas Seg–Sex; Sáb e Dom mantêm horário próprio
|
||||
selectedDays.value
|
||||
@@ -241,12 +267,18 @@ function switchToIgual () {
|
||||
}
|
||||
|
||||
function switchToDiferente () {
|
||||
// Inicializa cada dia com o horário global atual e as pausas globais
|
||||
if (isValidHHMM(jornadaStart.value) && isValidHHMM(jornadaEnd.value)) {
|
||||
selectedDays.value.forEach(d => {
|
||||
jornadaPorDia.value[d.value] = { inicio: jornadaStart.value, fim: jornadaEnd.value }
|
||||
})
|
||||
}
|
||||
selectedDays.value.forEach(d => {
|
||||
const isWeekend = d.value === 6 || d.value === 0
|
||||
const snap = jornadaPorDiaSnapshot.value?.[d.value]
|
||||
if (snap && isValidHHMM(snap.inicio) && isValidHHMM(snap.fim)) {
|
||||
// Restaura do snapshot (vale para todos os dias)
|
||||
jornadaPorDia.value[d.value] = { ...snap }
|
||||
} else if (!isWeekend) {
|
||||
// Dia de semana sem snapshot: inicia com 08:00–18:00
|
||||
jornadaPorDia.value[d.value] = { inicio: '08:00', fim: '18:00' }
|
||||
}
|
||||
// Sáb/Dom sem snapshot: mantém o valor atual (já configurado no modo "igual")
|
||||
})
|
||||
selectedDays.value.forEach(d => {
|
||||
pausasPorDia.value[d.value] = pausasGlobais.value.map(p => ({ ...p, id: newId() }))
|
||||
})
|
||||
@@ -284,15 +316,29 @@ function hydrateWizardFromRegras (dbRegras) {
|
||||
workDays.value = map
|
||||
jornadaPorDia.value = byDay
|
||||
|
||||
const first = actives[0]
|
||||
const allSame = actives.every(r =>
|
||||
String(r.hora_inicio||'').slice(0,5) === String(first.hora_inicio||'').slice(0,5) &&
|
||||
String(r.hora_fim ||'').slice(0,5) === String(first.hora_fim ||'').slice(0,5)
|
||||
)
|
||||
// Usa apenas dias de semana (1–5) para determinar allSame e o horário padrão.
|
||||
// Sáb (6) e Dom (0) têm horários próprios e não devem afetar o modo "igual para todos".
|
||||
const weekdayActives = actives.filter(r => r.dia_semana >= 1 && r.dia_semana <= 5)
|
||||
const ref = weekdayActives.length ? weekdayActives[0] : actives[0]
|
||||
|
||||
const allSame = weekdayActives.length >= 2
|
||||
? weekdayActives.every(r =>
|
||||
String(r.hora_inicio||'').slice(0,5) === String(ref.hora_inicio||'').slice(0,5) &&
|
||||
String(r.hora_fim ||'').slice(0,5) === String(ref.hora_fim ||'').slice(0,5)
|
||||
)
|
||||
: true
|
||||
|
||||
jornadaIgualTodos.value = allSame
|
||||
jornadaStart.value = String(first.hora_inicio||'').slice(0,5) || '08:00'
|
||||
jornadaEnd.value = String(first.hora_fim ||'').slice(0,5) || '18:00'
|
||||
jornadaStart.value = String(ref.hora_inicio||'').slice(0,5) || '08:00'
|
||||
jornadaEnd.value = String(ref.hora_fim ||'').slice(0,5) || '18:00'
|
||||
|
||||
// Se carregou em modo "diferente", popula o snapshot para preservar ao alternar modos
|
||||
if (!allSame) {
|
||||
jornadaPorDiaSnapshot.value = {}
|
||||
Object.keys(byDay).forEach(k => {
|
||||
jornadaPorDiaSnapshot.value[k] = { ...byDay[k] }
|
||||
})
|
||||
}
|
||||
|
||||
regras.value = actives.map(r => ({
|
||||
...r,
|
||||
@@ -482,7 +528,7 @@ async function saveJornada () {
|
||||
|
||||
cfg.value.setup_clinica_concluido = true
|
||||
cfg.value.jornada_igual_todos = igualTodos
|
||||
toast.add({ severity: 'success', summary: 'Jornada salva', detail: 'Horários de trabalho atualizados.', life: 1800 })
|
||||
toast.add({ severity: 'success', summary: 'Jornada salva', detail: 'Horários de trabalho atualizados.', life: 3500 })
|
||||
} catch (e) {
|
||||
toast.add({ severity: 'error', summary: 'Erro', detail: e?.message || 'Falha ao salvar jornada.', life: 3500 })
|
||||
} finally {
|
||||
@@ -785,20 +831,76 @@ const jornadaEndDate = computed({
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div v-if="loading" class="flex items-center justify-center py-24">
|
||||
<i class="pi pi-spinner pi-spin text-3xl opacity-40" />
|
||||
<!-- ══ SKELETON ═══════════════════════════════════════════════ -->
|
||||
<div v-if="loading" class="flex flex-col xl:flex-row gap-4">
|
||||
|
||||
<!-- Coluna esquerda skeleton -->
|
||||
<div class="flex flex-col gap-3 xl:w-[58%]">
|
||||
|
||||
<!-- Subheader skeleton -->
|
||||
<div class="flex items-center gap-3 px-1 py-2">
|
||||
<Skeleton shape="circle" size="2rem" />
|
||||
<div class="flex flex-col gap-1.5">
|
||||
<Skeleton width="9rem" height="0.85rem" />
|
||||
<Skeleton width="14rem" height="0.75rem" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Card skeleton (×3) — frases dentro do primeiro -->
|
||||
<div
|
||||
v-for="i in 3"
|
||||
:key="i"
|
||||
class="rounded-[8px] border border-[var(--surface-border)] bg-[var(--surface-card)] shadow-sm"
|
||||
>
|
||||
<div class="flex items-center gap-3 px-4 py-4">
|
||||
<Skeleton shape="circle" size="2.2rem" />
|
||||
<div class="flex-1 flex flex-col gap-1.5">
|
||||
<Skeleton :width="i === 1 ? '11rem' : i === 2 ? '9rem' : '13rem'" height="0.85rem" />
|
||||
<Skeleton :width="i === 1 ? '18rem' : i === 2 ? '12rem' : '10rem'" height="0.7rem" />
|
||||
</div>
|
||||
<Skeleton width="6rem" height="1.4rem" border-radius="999px" />
|
||||
<Skeleton shape="circle" size="1rem" />
|
||||
</div>
|
||||
<AppLoadingPhrases
|
||||
v-if="i === 1"
|
||||
action="Carregando configurações da agenda..."
|
||||
containerClass="py-8"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Coluna direita: skeleton puro -->
|
||||
<div class="xl:w-[42%] xl:self-start">
|
||||
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm">
|
||||
<!-- Header skeleton -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
|
||||
<Skeleton width="8rem" height="0.85rem" />
|
||||
<div class="flex gap-1">
|
||||
<Skeleton v-for="j in 5" :key="j" width="2.2rem" height="1.6rem" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Legenda skeleton -->
|
||||
<div class="flex gap-3 px-4 py-2 border-b border-[var(--surface-border)]">
|
||||
<Skeleton v-for="k in 3" :key="k" width="4.5rem" height="0.7rem" />
|
||||
</div>
|
||||
<!-- Calendário skeleton -->
|
||||
<div class="flex flex-col gap-2 p-4">
|
||||
<Skeleton v-for="l in 10" :key="l" :width="`${70 + (l % 3) * 10}%`" height="2.2rem" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-else class="flex flex-col xl:flex-row gap-4">
|
||||
<Transition name="fade-up" appear>
|
||||
<div v-if="!loading" class="flex flex-col xl:flex-row gap-4">
|
||||
|
||||
<!-- ══ COLUNA ESQUERDA: CARDS ══════════════════════════════ -->
|
||||
<div class="flex flex-col gap-3 xl:w-[58%]">
|
||||
<div class="anim-child [--delay:0ms] flex flex-col gap-3 xl:w-[58%]">
|
||||
|
||||
<!-- Subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<i class="pi pi-calendar cfg-subheader__icon" />
|
||||
<i class="pi pi-calendar w-10 h-10 rounded-md 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>
|
||||
@@ -810,7 +912,7 @@ const jornadaEndDate = computed({
|
||||
|
||||
<!-- Cabeçalho clicável -->
|
||||
<button class="cfg-card__header" @click="expandedCard = expandedCard === 'jornada' ? null : 'jornada'">
|
||||
<div class="cfg-card__icon-wrap">
|
||||
<div class="cfg-card__icon-wrap w-10 h-10 rounded-md">
|
||||
<i class="pi pi-calendar text-lg" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
@@ -994,7 +1096,7 @@ const jornadaEndDate = computed({
|
||||
<!-- Pausas -->
|
||||
<div v-if="selectedDays.length > 0" class="mb-5">
|
||||
<div class="cfg-label mb-2">Pausas (opcional)</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mb-3">Ex.: almoço, intervalo fixo.</div>
|
||||
<div class="text-sm text-[var(--text-color-secondary)] mb-3">Ex.: almoço, jantar, etc.</div>
|
||||
|
||||
<div v-if="jornadaIgualTodos !== false">
|
||||
<PausasChipsEditor v-model="pausasGlobais" />
|
||||
@@ -1026,7 +1128,7 @@ const jornadaEndDate = computed({
|
||||
<div class="cfg-card" :class="{ 'cfg-card--open': expandedCard === 'ritmo' }">
|
||||
|
||||
<button class="cfg-card__header" @click="expandedCard = expandedCard === 'ritmo' ? null : 'ritmo'">
|
||||
<div class="cfg-card__icon-wrap">
|
||||
<div class="cfg-card__icon-wrap w-10 h-10 rounded-md">
|
||||
<i class="pi pi-stopwatch text-lg" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
@@ -1109,7 +1211,7 @@ const jornadaEndDate = computed({
|
||||
<div class="cfg-card" :class="{ 'cfg-card--open': expandedCard === 'online' }">
|
||||
|
||||
<button class="cfg-card__header" @click="expandedCard = expandedCard === 'online' ? null : 'online'">
|
||||
<div class="cfg-card__icon-wrap">
|
||||
<div class="cfg-card__icon-wrap w-10 h-10 rounded-md">
|
||||
<i class="pi pi-globe text-lg" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0 text-left">
|
||||
@@ -1247,38 +1349,43 @@ const jornadaEndDate = computed({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bloco pós-carregamento -->
|
||||
<LoadedPhraseBlock />
|
||||
</div>
|
||||
|
||||
<!-- ══ COLUNA DIREITA: PREVIEW ═════════════════════════════ -->
|
||||
<div class="xl:w-[42%] xl:sticky xl:top-4 xl:self-start">
|
||||
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm">
|
||||
<div class="anim-child [--delay:120ms] xl:w-[42%] xl:top-4 xl:self-start">
|
||||
<div class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden shadow-sm agenda-altura">
|
||||
|
||||
<!-- Header do preview -->
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
|
||||
<div class="font-semibold text-sm">Preview da agenda</div>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="d in selectedDays"
|
||||
:key="d.value"
|
||||
class="day-chip day-chip--sm"
|
||||
:class="(jornadaIgualTodos !== false || previewDay === d.value) ? 'day-chip--active' : ''"
|
||||
@click="previewDay = d.value"
|
||||
>
|
||||
{{ d.short }}
|
||||
</button>
|
||||
<span v-if="!selectedDays.length" class="text-xs text-[var(--text-color-secondary)]">
|
||||
Nenhum dia selecionado
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sticky top-0 z-10 bg-white">
|
||||
<div class="flex items-center justify-between px-4 py-3 border-b border-[var(--surface-border)]">
|
||||
<div class="font-semibold text-sm">Preview da agenda</div>
|
||||
<div class="flex gap-1">
|
||||
<button
|
||||
v-for="d in selectedDays"
|
||||
:key="d.value"
|
||||
class="day-chip day-chip--sm"
|
||||
:class="(jornadaIgualTodos !== false || previewDay === d.value) ? 'day-chip--active' : ''"
|
||||
@click="previewDay = d.value"
|
||||
>
|
||||
{{ d.short }}
|
||||
</button>
|
||||
<span v-if="!selectedDays.length" class="text-xs text-[var(--text-color-secondary)]">
|
||||
Nenhum dia selecionado
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Legenda -->
|
||||
<div class="flex gap-3 px-4 py-2 border-b border-[var(--surface-border)] text-xs text-[var(--text-color-secondary)]">
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--green-100)] inline-block border border-green-300"></span> Jornada</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--red-200)] inline-block border border-red-300"></span> Pausa</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--primary-color)] inline-block"></span> Sessão</span>
|
||||
<span v-if="cfg.online_ativo" class="flex items-center gap-1">🌐 Online</span>
|
||||
</div>
|
||||
<!-- Legenda -->
|
||||
<div class="flex gap-3 px-4 py-2 border-b border-[var(--surface-border)] text-xs text-[var(--text-color-secondary)]">
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--green-100)] inline-block border border-green-300"></span> Jornada</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--red-200)] inline-block border border-red-300"></span> Pausa</span>
|
||||
<span class="flex items-center gap-1"><span class="w-3 h-3 rounded bg-[var(--primary-color)] inline-block"></span> Sessão</span>
|
||||
<span v-if="cfg.online_ativo" class="flex items-center gap-1">🌐 Online</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FullCalendar -->
|
||||
<div v-if="previewDay != null && !loading" class="p-2">
|
||||
@@ -1291,6 +1398,7 @@ const jornadaEndDate = computed({
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
@@ -1338,16 +1446,6 @@ const jornadaEndDate = computed({
|
||||
}
|
||||
.cfg-card__header:hover { background: var(--surface-hover); }
|
||||
|
||||
.cfg-card__icon-wrap {
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
border-radius: .875rem;
|
||||
border: 1px solid var(--surface-border);
|
||||
background: var(--surface-ground);
|
||||
display: grid;
|
||||
place-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.cfg-card__title {
|
||||
font-size: .9375rem;
|
||||
font-weight: 600;
|
||||
@@ -1517,52 +1615,4 @@ const jornadaEndDate = computed({
|
||||
.toggle-switch--on .toggle-switch__thumb {
|
||||
transform: translateX(1.25rem);
|
||||
}
|
||||
|
||||
/* ── 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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesConveniosPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesConveniosPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -237,7 +252,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -253,10 +267,24 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
<ProgressSpinner style="width:40px;height:40px" />
|
||||
</div>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="pageLoading || loading">
|
||||
<div class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
|
||||
<Skeleton width="9rem" height="12px" />
|
||||
</div>
|
||||
<div v-for="n in 3" :key="n" class="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface-border)] last:border-b-0">
|
||||
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<Skeleton :width="n === 1 ? '12rem' : n === 2 ? '9rem' : '11rem'" height="11px" />
|
||||
<Skeleton width="5rem" height="10px" />
|
||||
</div>
|
||||
<Skeleton width="3rem" height="1.4rem" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando convênios..." containerClass="py-6" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
@@ -458,6 +486,8 @@ onMounted(async () => {
|
||||
</span>
|
||||
</Message>
|
||||
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesDescontosPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesDescontosPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -182,7 +197,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -198,10 +212,23 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
<ProgressSpinner style="width:40px;height:40px" />
|
||||
</div>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="pageLoading || loading">
|
||||
<div class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
|
||||
<Skeleton width="11rem" height="12px" />
|
||||
</div>
|
||||
<div v-for="n in 3" :key="n" class="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface-border)] last:border-b-0">
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<Skeleton :width="n % 2 === 0 ? '14rem' : '10rem'" height="11px" />
|
||||
<Skeleton width="8rem" height="10px" />
|
||||
</div>
|
||||
<Skeleton width="4rem" height="1.4rem" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando descontos por paciente..." containerClass="py-6" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
@@ -352,6 +379,8 @@ onMounted(async () => {
|
||||
</span>
|
||||
</Message>
|
||||
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
@@ -349,9 +364,23 @@ onMounted(async () => {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="flex justify-center py-10">
|
||||
<ProgressSpinner />
|
||||
</div>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="loading">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div v-for="n in 5" :key="n" class="border border-[var(--surface-border)] rounded-xl bg-[var(--surface-card)] px-4 py-3 flex gap-3 items-center">
|
||||
<Skeleton width="4.5rem" height="1.4rem" border-radius="999px" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<Skeleton :width="n % 2 === 0 ? '14rem' : '10rem'" height="11px" />
|
||||
<Skeleton :width="n % 3 === 0 ? '20rem' : '16rem'" height="10px" />
|
||||
</div>
|
||||
<div class="flex gap-1">
|
||||
<Skeleton width="2rem" height="2rem" border-radius="6px" />
|
||||
<Skeleton width="2rem" height="2rem" border-radius="6px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando templates de e-mail..." containerClass="py-6" />
|
||||
</template>
|
||||
|
||||
<!-- Lista -->
|
||||
<div v-else class="flex flex-col gap-2">
|
||||
@@ -392,6 +421,7 @@ onMounted(async () => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<LoadedPhraseBlock />
|
||||
</div>
|
||||
|
||||
<!-- ── Dialog Layout Global (Header/Footer) ─────────────── -->
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesExcecoesFinanceirasPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -143,7 +158,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -156,10 +170,20 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
<ProgressSpinner style="width:40px;height:40px" />
|
||||
</div>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="pageLoading || loading">
|
||||
<div v-for="n in 3" :key="n" class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
|
||||
<Skeleton :width="n === 1 ? '13rem' : n === 2 ? '11rem' : '15rem'" height="12px" />
|
||||
<Skeleton width="4rem" height="1.4rem" border-radius="999px" class="ml-auto" />
|
||||
</div>
|
||||
<div class="px-4 py-3">
|
||||
<Skeleton width="16rem" height="10px" />
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando exceções financeiras..." containerClass="py-6" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
@@ -266,6 +290,8 @@ onMounted(async () => {
|
||||
</span>
|
||||
</Message>
|
||||
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -1,10 +1,27 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
|
||||
const toast = useToast()
|
||||
const toast = useToast()
|
||||
const confirm = useConfirm()
|
||||
|
||||
// ── Constantes ────────────────────────────────────────────────
|
||||
const AVATAR_BUCKET = 'avatars'
|
||||
@@ -49,6 +66,7 @@ const ESTADOS = [
|
||||
// ── Estado ────────────────────────────────────────────────────
|
||||
const tenantId = ref(null)
|
||||
const recordId = ref(null)
|
||||
const loading = ref(true)
|
||||
const saving = ref(false)
|
||||
const loadingCep = ref(false)
|
||||
|
||||
@@ -165,24 +183,41 @@ async function buscarCep() {
|
||||
|
||||
// ── Redes sociais ─────────────────────────────────────────────
|
||||
function addRede() { form.value.redes_sociais.push({ rede: null, url: '' }) }
|
||||
function removeRede(i) { form.value.redes_sociais.splice(i, 1) }
|
||||
function removeRede(i) {
|
||||
const rede = form.value.redes_sociais[i]
|
||||
const label = REDES_OPTIONS.find(o => o.value === rede?.rede)?.label || rede?.rede || 'esta rede social'
|
||||
confirm.require({
|
||||
message: `Deseja remover ${label}?`,
|
||||
header: 'Remover rede social',
|
||||
icon: 'pi pi-exclamation-triangle',
|
||||
rejectLabel: 'Cancelar',
|
||||
acceptLabel: 'Remover',
|
||||
acceptClass: 'p-button-danger',
|
||||
accept: () => form.value.redes_sociais.splice(i, 1),
|
||||
})
|
||||
}
|
||||
|
||||
// ── Load / Save ───────────────────────────────────────────────
|
||||
async function load() {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return
|
||||
tenantId.value = user.id
|
||||
const { data } = await supabase
|
||||
.from('company_profiles')
|
||||
.select('*')
|
||||
.eq('tenant_id', user.id)
|
||||
.maybeSingle()
|
||||
if (data) {
|
||||
recordId.value = data.id
|
||||
Object.keys(form.value).forEach(k => {
|
||||
if (data[k] !== undefined && data[k] !== null) form.value[k] = data[k]
|
||||
})
|
||||
if (data.logo_url) logoPreview.value = data.logo_url
|
||||
loading.value = true
|
||||
try {
|
||||
const { data: { user } } = await supabase.auth.getUser()
|
||||
if (!user) return
|
||||
tenantId.value = user.id
|
||||
const { data } = await supabase
|
||||
.from('company_profiles')
|
||||
.select('*')
|
||||
.eq('tenant_id', user.id)
|
||||
.maybeSingle()
|
||||
if (data) {
|
||||
recordId.value = data.id
|
||||
Object.keys(form.value).forEach(k => {
|
||||
if (data[k] !== undefined && data[k] !== null) form.value[k] = data[k]
|
||||
})
|
||||
if (data.logo_url) logoPreview.value = data.logo_url
|
||||
}
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,6 +302,7 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ConfirmDialog />
|
||||
<div class="flex flex-col gap-4">
|
||||
|
||||
<!-- Subheader -->
|
||||
@@ -282,7 +318,35 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="loading">
|
||||
<div class="flex gap-4 items-start">
|
||||
<!-- Esqueleto formulário -->
|
||||
<div class="form-col flex flex-col gap-4">
|
||||
<div v-for="n in 3" :key="n" class="cfg-card">
|
||||
<div class="cfg-card__head">
|
||||
<Skeleton width="1rem" height="1rem" border-radius="4px" />
|
||||
<Skeleton :width="n === 1 ? '7rem' : n === 2 ? '9rem' : '8rem'" height="12px" />
|
||||
</div>
|
||||
<div class="flex flex-col gap-3 p-4">
|
||||
<div class="grid grid-cols-2 gap-3">
|
||||
<Skeleton height="2.5rem" border-radius="6px" />
|
||||
<Skeleton height="2.5rem" border-radius="6px" />
|
||||
</div>
|
||||
<Skeleton height="2.5rem" border-radius="6px" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Esqueleto preview -->
|
||||
<div class="preview-col">
|
||||
<Skeleton height="320px" border-radius="12px" />
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando dados da empresa..." containerClass="py-6" />
|
||||
</template>
|
||||
|
||||
<!-- Corpo: formulário + preview -->
|
||||
<template v-else>
|
||||
<div class="flex gap-4 items-start">
|
||||
|
||||
<!-- ── Formulário (60%) ───────────────────────────────── -->
|
||||
@@ -432,6 +496,8 @@ onMounted(load)
|
||||
<Button label="Salvar dados da empresa" icon="pi pi-check" :loading="saving" @click="save" />
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- ── Preview (40%) ──────────────────────────────────── -->
|
||||
@@ -533,6 +599,8 @@ onMounted(load)
|
||||
|
||||
</div>
|
||||
|
||||
</template>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesPagamentoPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesPagamentoPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -177,11 +192,32 @@ onMounted(load)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<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>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="loading">
|
||||
<div class="flex flex-col gap-4">
|
||||
<!-- Skeleton subheader -->
|
||||
<div class="cfg-subheader">
|
||||
<Skeleton width="2rem" height="2rem" border-radius="6px" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<Skeleton width="7rem" height="13px" />
|
||||
<Skeleton width="18rem" height="11px" />
|
||||
</div>
|
||||
</div>
|
||||
<!-- Skeleton cards de pagamento -->
|
||||
<div v-for="n in 4" :key="n" class="rounded-[6px] border border-[var(--surface-border)] bg-[var(--surface-card)] overflow-hidden">
|
||||
<div class="flex items-center gap-3 p-4">
|
||||
<Skeleton width="40px" height="40px" border-radius="6px" />
|
||||
<div class="flex flex-col gap-1.5 flex-1">
|
||||
<Skeleton :width="n % 2 === 0 ? '8rem' : '10rem'" height="12px" />
|
||||
<Skeleton :width="n % 3 === 0 ? '16rem' : '12rem'" height="10px" />
|
||||
</div>
|
||||
<Skeleton width="3rem" height="1.4rem" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando formas de pagamento..." containerClass="py-6" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div v-else class="flex flex-col gap-4">
|
||||
|
||||
@@ -550,6 +586,8 @@ onMounted(load)
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,19 @@
|
||||
<!-- src/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/layout/configuracoes/ConfiguracoesPrecificacaoPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<script setup>
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { supabase } from '@/lib/supabase/client'
|
||||
@@ -146,7 +161,6 @@ onMounted(async () => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
|
||||
<div class="flex flex-col gap-3">
|
||||
|
||||
@@ -162,10 +176,24 @@ onMounted(async () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="pageLoading || loading" class="flex justify-center py-10">
|
||||
<ProgressSpinner style="width:40px;height:40px" />
|
||||
</div>
|
||||
<!-- ══ SKELETON ══════════════════════════════════════════════ -->
|
||||
<template v-if="pageLoading || loading">
|
||||
<div class="cfg-wrap">
|
||||
<div class="cfg-wrap__head">
|
||||
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
|
||||
<Skeleton width="9rem" height="12px" />
|
||||
</div>
|
||||
<div v-for="n in 3" :key="n" class="flex items-center gap-3 px-4 py-3 border-b border-[var(--surface-border)] last:border-b-0">
|
||||
<Skeleton width="1.75rem" height="1.75rem" border-radius="6px" />
|
||||
<div class="flex flex-col gap-2 flex-1">
|
||||
<Skeleton :width="n === 1 ? '10rem' : n === 2 ? '8rem' : '12rem'" height="11px" />
|
||||
<Skeleton width="6rem" height="10px" />
|
||||
</div>
|
||||
<Skeleton width="3.5rem" height="1.4rem" border-radius="999px" />
|
||||
</div>
|
||||
</div>
|
||||
<AppLoadingPhrases action="Carregando serviços e precificação..." containerClass="py-6" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
|
||||
@@ -298,6 +326,8 @@ onMounted(async () => {
|
||||
<span class="text-sm">Os serviços cadastrados aqui aparecem para seleção ao criar ou editar um evento na agenda.</span>
|
||||
</Message>
|
||||
|
||||
<LoadedPhraseBlock />
|
||||
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user