Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user