Copyright, Financeiro, Lançamentos, aprimoramentos de ui
This commit is contained in:
@@ -1,6 +1,20 @@
|
||||
<!-- src/views/pages/agenda/AgendaTerapeutaPage.vue -->
|
||||
<!--
|
||||
|--------------------------------------------------------------------------
|
||||
| Agência PSI
|
||||
|--------------------------------------------------------------------------
|
||||
| Criado e desenvolvido por Leonardo Nohama
|
||||
|
|
||||
| Tecnologia aplicada à escuta.
|
||||
| Estrutura para o cuidado.
|
||||
|
|
||||
| Arquivo: src/features/agenda/pages/AgendaTerapeutaPage.vue
|
||||
| Data: 2026
|
||||
| Local: São Carlos/SP — Brasil
|
||||
|--------------------------------------------------------------------------
|
||||
| © 2026 — Todos os direitos reservados
|
||||
|--------------------------------------------------------------------------
|
||||
-->
|
||||
<template>
|
||||
<Toast />
|
||||
<ConfirmDialog />
|
||||
|
||||
<!-- ════ AGENDA TERAPEUTA — Layout 3 colunas ════ -->
|
||||
@@ -117,7 +131,7 @@
|
||||
<Button icon="pi pi-chevron-right" severity="secondary" text class="h-7 w-7 rounded-full" @click="miniNextMonth" />
|
||||
</div>
|
||||
</div>
|
||||
<Calendar
|
||||
<DatePicker
|
||||
v-model="miniDate"
|
||||
inline
|
||||
class="w-full"
|
||||
@@ -128,7 +142,7 @@
|
||||
<span class="mini-day-num">{{ date.day }}</span>
|
||||
<span v-if="hasMiniEvent(date)" class="mini-day-dot" />
|
||||
</template>
|
||||
</Calendar>
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<div v-if="jornadaHoje" class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] p-3">
|
||||
@@ -140,6 +154,8 @@
|
||||
|
||||
<ProximosFeriadosCard :ownerId="ownerId" :tenantId="clinicTenantId" :workRules="workRules" @bloqueado="refetch" />
|
||||
|
||||
<LoadedPhraseBlock v-if="eventsHasLoaded" />
|
||||
|
||||
<!-- Divisor -->
|
||||
<div class="border-t border-[var(--surface-border)] my-1" />
|
||||
|
||||
@@ -149,10 +165,15 @@
|
||||
<span class="flex items-center gap-1.5 text-[1rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65"><i class="pi pi-chart-bar" />Hoje</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
<template v-if="eventsLoading">
|
||||
<Skeleton v-for="n in 4" :key="n" height="3rem" class="rounded-md" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -162,7 +183,12 @@
|
||||
<span class="flex items-center gap-1.5 text-[1rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65"><i class="pi pi-list" />Sessões hoje</span>
|
||||
<span class="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[0.65rem] font-bold">{{ todayEvents.length }}</span>
|
||||
</div>
|
||||
<div v-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<template v-if="eventsLoading">
|
||||
<div class="flex flex-col gap-1.5 mt-1">
|
||||
<Skeleton v-for="n in 3" :key="n" height="3.5rem" class="rounded-md" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<i class="pi pi-sun text-2xl opacity-20" />
|
||||
<span>Nenhuma sessão hoje</span>
|
||||
</div>
|
||||
@@ -318,7 +344,7 @@
|
||||
<Button icon="pi pi-chevron-right" severity="secondary" outlined class="h-8 w-8 rounded-full" @click="goNext" />
|
||||
</div>
|
||||
</div>
|
||||
<Calendar
|
||||
<DatePicker
|
||||
v-model="miniDate"
|
||||
inline
|
||||
class="w-full"
|
||||
@@ -329,7 +355,7 @@
|
||||
<span class="mini-day-num">{{ date.day }}</span>
|
||||
<span v-if="hasMiniEvent(date)" class="mini-day-dot" />
|
||||
</template>
|
||||
</Calendar>
|
||||
</DatePicker>
|
||||
</div>
|
||||
|
||||
<div v-if="jornadaHoje" class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] p-3">
|
||||
@@ -341,6 +367,8 @@
|
||||
|
||||
<ProximosFeriadosCard :ownerId="ownerId" :tenantId="clinicTenantId" :workRules="workRules" @bloqueado="refetch" />
|
||||
|
||||
<LoadedPhraseBlock v-if="eventsHasLoaded" />
|
||||
|
||||
</div>
|
||||
|
||||
<!-- Botão toggle painel (só mobile <xl) -->
|
||||
@@ -377,7 +405,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden">
|
||||
<div class="border border-[var(--surface-border)] rounded-md bg-[var(--surface-card)] overflow-hidden shadow-sm agenda-altura">
|
||||
<div v-if="calendarView === 'day' && miniBlockedDaySet.has(currentDateISO)" class="flex items-center gap-2 px-4 py-2 text-sm font-semibold text-red-700 bg-red-400/10 border-b border-red-400/25">
|
||||
<i class="pi pi-lock text-xs" /> Dia bloqueado — sessões não permitidas
|
||||
</div>
|
||||
@@ -396,10 +424,15 @@
|
||||
<span class="flex items-center gap-1.5 text-[1rem] font-bold uppercase tracking-[0.06em] text-[var(--text-color-secondary)] opacity-65"><i class="pi pi-chart-bar" />Hoje</span>
|
||||
</div>
|
||||
<div class="grid grid-cols-4 gap-2">
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
<template v-if="eventsLoading">
|
||||
<Skeleton v-for="n in 4" :key="n" height="3rem" class="rounded-md" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="flex flex-col gap-0.5 p-2 rounded-md bg-[var(--surface-ground)] border border-[var(--surface-border)] text-center" v-for="s in todayStats" :key="s.label">
|
||||
<div class="text-[1.25rem] font-bold leading-none text-[var(--text-color)]" :class="{ 'text-green-500': s.cls === 'ag-stat--ok', 'text-red-500': s.cls === 'ag-stat--warn' }">{{ s.value }}</div>
|
||||
<div class="text-[0.65rem] font-semibold uppercase tracking-[0.04em] text-[var(--text-color-secondary)] opacity-70">{{ s.label }}</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -410,7 +443,12 @@
|
||||
<span class="inline-flex items-center justify-center min-w-[18px] h-[18px] px-1 rounded-full bg-[var(--primary-color,#6366f1)] text-white text-[0.65rem] font-bold">{{ todayEvents.length }}</span>
|
||||
</div>
|
||||
|
||||
<div v-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<template v-if="eventsLoading">
|
||||
<div class="flex flex-col gap-1.5 mt-1">
|
||||
<Skeleton v-for="n in 3" :key="n" height="3.5rem" class="rounded-md" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-else-if="!todayEvents.length" class="flex flex-col items-center justify-center gap-2 py-6 text-[var(--text-color-secondary)] text-sm text-center">
|
||||
<i class="pi pi-sun text-2xl opacity-20" />
|
||||
<span>Nenhuma sessão hoje</span>
|
||||
</div>
|
||||
@@ -670,7 +708,7 @@
|
||||
<!-- Month Picker -->
|
||||
<Dialog v-model:visible="monthPickerVisible" modal header="Escolher mês" :style="{ width: '420px' }">
|
||||
<div class="p-2">
|
||||
<Calendar v-model="monthPickerDate" view="month" dateFormat="mm/yy" class="w-full" />
|
||||
<DatePicker v-model="monthPickerDate" view="month" dateFormat="mm/yy" class="w-full" />
|
||||
<div class="mt-3 flex justify-end gap-2">
|
||||
<Button label="Cancelar" severity="secondary" outlined class="rounded-full" @click="monthPickerVisible = false" />
|
||||
<Button label="Ir" class="rounded-full" @click="applyMonthPick" />
|
||||
@@ -939,7 +977,7 @@ import { supabase } from '@/lib/supabase/client'
|
||||
import { useToast } from 'primevue/usetoast'
|
||||
import { useConfirm } from 'primevue/useconfirm'
|
||||
|
||||
import Calendar from 'primevue/calendar'
|
||||
import DatePicker from 'primevue/datepicker'
|
||||
|
||||
import FullCalendar from '@fullcalendar/vue3'
|
||||
import timeGridPlugin from '@fullcalendar/timegrid'
|
||||
@@ -1043,7 +1081,9 @@ const commitmentOptionsNormalized = computed(() => {
|
||||
// settings + events
|
||||
// -----------------------------
|
||||
const { error: settingsError, settings, workRules, load: loadSettings } = useAgendaSettings()
|
||||
const { error: eventsError, rows, loadMyRange, create, update, remove } = useAgendaEvents()
|
||||
const { error: eventsError, rows, loading: eventsLoading, loadMyRange, create, update, remove } = useAgendaEvents()
|
||||
const eventsHasLoaded = ref(false)
|
||||
watch(eventsLoading, (val) => { if (!val) eventsHasLoaded.value = true })
|
||||
|
||||
const {
|
||||
loadAndExpand,
|
||||
@@ -1096,9 +1136,10 @@ const onlySessionsOptions = [
|
||||
{ label: 'Tudo', value: false }
|
||||
]
|
||||
const viewOptions = [
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Dia', value: 'day' },
|
||||
{ label: 'Semana', value: 'week' },
|
||||
{ label: 'Mês', value: 'month' }
|
||||
{ label: 'Mês', value: 'month' },
|
||||
{ label: 'Lista', value: 'list' }
|
||||
]
|
||||
const timeModeOptions = [
|
||||
{ label: '24h', value: '24' },
|
||||
@@ -1327,8 +1368,9 @@ const slotMaxTime = computed(() => {
|
||||
})
|
||||
|
||||
const fcViewName = computed(() => {
|
||||
if (calendarView.value === 'day') return 'timeGridDay'
|
||||
if (calendarView.value === 'week') return 'timeGridWeek'
|
||||
if (calendarView.value === 'day') return 'timeGridDay'
|
||||
if (calendarView.value === 'week') return 'timeGridWeek'
|
||||
if (calendarView.value === 'list') return 'listWeek'
|
||||
return 'dayGridMonth'
|
||||
})
|
||||
|
||||
@@ -1581,7 +1623,7 @@ const _initSlotMax = slotMaxTime.value
|
||||
// NÃO incluímos 'events' no fcOptions — evita que o Vue FC adapter gerencie
|
||||
// a fonte e conflite com o watch que usa getEventSources + addEventSource.
|
||||
const fcOptions = computed(() => ({
|
||||
plugins: [timeGridPlugin, dayGridPlugin, interactionPlugin],
|
||||
plugins: [timeGridPlugin, dayGridPlugin, listPlugin, interactionPlugin],
|
||||
locale: ptBrLocale,
|
||||
timeZone: timezone.value,
|
||||
|
||||
@@ -1602,12 +1644,16 @@ const fcOptions = computed(() => ({
|
||||
slotLabelContent,
|
||||
expandRows: false,
|
||||
height: 'auto',
|
||||
slotMinHeight: 14,
|
||||
|
||||
dayMaxEvents: true,
|
||||
weekends: true,
|
||||
eventMinHeight: 14,
|
||||
|
||||
views: {
|
||||
timeGridDay: {
|
||||
dayHeaderFormat: { day: 'numeric', month: 'long', year: 'numeric' },
|
||||
},
|
||||
},
|
||||
|
||||
businessHours: businessHours.value,
|
||||
|
||||
datesSet: async (arg) => {
|
||||
@@ -2052,13 +2098,42 @@ const currentDateISO = computed(() => {
|
||||
return `${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`
|
||||
})
|
||||
|
||||
// ── Mini calendário: set de dias da semana atual ─────────────
|
||||
const currentWeekIsoSet = computed(() => {
|
||||
const now = new Date()
|
||||
const monday = new Date(now)
|
||||
monday.setDate(now.getDate() - ((now.getDay() + 6) % 7))
|
||||
monday.setHours(0, 0, 0, 0)
|
||||
const set = new Set()
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const d = new Date(monday)
|
||||
d.setDate(monday.getDate() + i)
|
||||
set.add(`${d.getFullYear()}-${String(d.getMonth()+1).padStart(2,'0')}-${String(d.getDate()).padStart(2,'0')}`)
|
||||
}
|
||||
return set
|
||||
})
|
||||
|
||||
const todayISO = computed(() => {
|
||||
const n = new Date()
|
||||
return `${n.getFullYear()}-${String(n.getMonth()+1).padStart(2,'0')}-${String(n.getDate()).padStart(2,'0')}`
|
||||
})
|
||||
|
||||
// ── Mini calendário: classes por dia ──────────────────────────
|
||||
// Prioridade: bloqueado total > dia de trabalho > folga
|
||||
function miniDayClass (date) {
|
||||
const iso = `${date.year}-${String(date.month + 1).padStart(2,'0')}-${String(date.day).padStart(2,'0')}`
|
||||
if (miniBlockedDaySet.value.has(iso)) return 'mini-day-blocked'
|
||||
const dow = new Date(date.year, date.month, date.day).getDay()
|
||||
return workDowSet.value.has(dow) ? 'mini-day-work' : 'mini-day-off'
|
||||
const classes = []
|
||||
if (currentWeekIsoSet.value.has(iso)) {
|
||||
classes.push('mini-week-hl')
|
||||
if (dow === 1) classes.push('mini-week-hl--start')
|
||||
else if (dow === 0) classes.push('mini-week-hl--end')
|
||||
else classes.push('mini-week-hl--mid')
|
||||
}
|
||||
if (iso === todayISO.value) classes.push('mini-day-today')
|
||||
if (miniBlockedDaySet.value.has(iso)) classes.push('mini-day-blocked')
|
||||
else classes.push(workDowSet.value.has(dow) ? 'mini-day-work' : 'mini-day-off')
|
||||
return classes
|
||||
}
|
||||
|
||||
// ── Mini calendário: bolinhas de compromissos + set de dias bloqueados ──
|
||||
@@ -3232,6 +3307,22 @@ onMounted(async () => {
|
||||
:deep(.mini-day-blocked) { background: color-mix(in srgb,#ef4444 20%,transparent) !important; border-radius: 4px; }
|
||||
:deep(.mini-day-work) { }
|
||||
:deep(.mini-day-off) { opacity: 0.45; }
|
||||
:deep(.p-disabled.mini-day-work) { background: color-mix(in srgb, #9ca3af 18%, transparent) !important; opacity: 0.6; }
|
||||
|
||||
/* Semana atual — faixa de fundo contínua seg→dom */
|
||||
:deep(.mini-week-hl) { background: color-mix(in srgb, var(--primary-color, #6366f1) 12%, transparent) !important; border-radius: 0 !important; }
|
||||
:deep(.mini-week-hl--start) { border-radius: 6px 0 0 6px !important; }
|
||||
:deep(.mini-week-hl--end) { border-radius: 0 6px 6px 0 !important; }
|
||||
|
||||
/* Hoje — cartão com borda + sombra */
|
||||
:deep(.mini-day-today) {
|
||||
background: color-mix(in srgb, var(--primary-color, #6366f1) 80%, #00000000) !important;
|
||||
border: 1px solid var(--surface-border) !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06) !important;
|
||||
border-radius: 6px !important;
|
||||
color: #ffffff !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style>
|
||||
|
||||
Reference in New Issue
Block a user