Copyright, Financeiro, Lançamentos, aprimoramentos de ui

This commit is contained in:
Leonardo
2026-03-21 08:05:40 -03:00
parent 29ed349cf2
commit a89d1f5560
268 changed files with 58870 additions and 1752 deletions
@@ -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 SegSex; 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:0018: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 (15) 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>