From 29ed349cf2bd4835f1c061cea4bf81894830a47c Mon Sep 17 00:00:00 2001 From: Leonardo Date: Wed, 18 Mar 2026 15:47:37 -0300 Subject: [PATCH] Agenda google, avisos globais, feriados + avisos globais, templates de email, configuracoes empresa, preview empresa. --- migrations/agendador_fix_slots.sql | 67 +- .../agenda/components/AgendaEventDialog.vue | 69 ++ src/features/notices/GlobalNoticeBanner.vue | 296 ++++++ src/features/notices/noticeService.js | 116 +++ src/layout/AppLayout.vue | 61 +- src/layout/AppTopbar.vue | 80 +- src/layout/ConfiguracoesPage.vue | 25 +- .../ConfiguracoesEmailTemplatesPage.vue | 850 ++++++++++++++++++ .../ConfiguracoesMinhaEmpresaPage.vue | 706 +++++++++++++++ src/lib/email/emailTemplateConstants.js | 74 ++ src/lib/email/emailTemplateService.js | 335 +++++++ src/lib/email/emailTemplates.reference.js | 706 +++++++++++++++ src/navigation/menus/saas.menu.js | 6 +- src/router/routes.configs.js | 10 + src/router/routes.saas.js | 12 + src/sql-arquivos/global_notices.sql | 139 +++ src/stores/noticeStore.js | 225 +++++ src/utils/googleCalendarLink.js | 58 ++ .../pages/saas/SaasEmailTemplatesPage.vue | 353 ++++++++ src/views/pages/saas/SaasFeriadosPage.vue | 534 ++++++++++- .../pages/saas/SaasGlobalNoticesPage.vue | 685 ++++++++++++++ 21 files changed, 5366 insertions(+), 41 deletions(-) create mode 100644 src/features/notices/GlobalNoticeBanner.vue create mode 100644 src/features/notices/noticeService.js create mode 100644 src/layout/configuracoes/ConfiguracoesEmailTemplatesPage.vue create mode 100644 src/layout/configuracoes/ConfiguracoesMinhaEmpresaPage.vue create mode 100644 src/lib/email/emailTemplateConstants.js create mode 100644 src/lib/email/emailTemplateService.js create mode 100644 src/lib/email/emailTemplates.reference.js create mode 100644 src/sql-arquivos/global_notices.sql create mode 100644 src/stores/noticeStore.js create mode 100644 src/utils/googleCalendarLink.js create mode 100644 src/views/pages/saas/SaasEmailTemplatesPage.vue create mode 100644 src/views/pages/saas/SaasGlobalNoticesPage.vue diff --git a/migrations/agendador_fix_slots.sql b/migrations/agendador_fix_slots.sql index e2d4e6e..a9ce990 100644 --- a/migrations/agendador_fix_slots.sql +++ b/migrations/agendador_fix_slots.sql @@ -1,7 +1,8 @@ -- ═══════════════════════════════════════════════════════════════════════════ -- FIX: agendador_slots_disponiveis + agendador_dias_disponiveis -- Usa agenda_online_slots como fonte de slots --- Cruzamento com: agenda_eventos, recurrence_rules/exceptions, agendador_solicitacoes +-- Cruzamento com: agenda_eventos, recurrence_rules/exceptions, +-- agendador_solicitacoes, agenda_bloqueios (feriados/bloqueios) -- Execute no Supabase SQL Editor -- ═══════════════════════════════════════════════════════════════════════════ @@ -42,6 +43,22 @@ BEGIN v_agora := now(); v_db_dow := extract(dow from p_data::timestamp)::int; + -- ── Dia inteiro bloqueado? (agenda_bloqueios sem hora) ─────────────────── + -- Se sim, não há nenhum slot disponível — retorna vazio. + IF EXISTS ( + SELECT 1 FROM public.agenda_bloqueios b + WHERE b.owner_id = v_owner_id + AND b.data_inicio <= p_data + AND COALESCE(b.data_fim, p_data) >= p_data + AND b.hora_inicio IS NULL -- bloqueio de dia inteiro + AND ( + (NOT b.recorrente) + OR (b.recorrente AND b.dia_semana = v_db_dow) + ) + ) THEN + RETURN; + END IF; + FOR v_slot IN SELECT s.time FROM public.agenda_online_slots s @@ -60,6 +77,23 @@ BEGIN v_ocupado := true; END IF; + -- ── Bloqueio de horário específico (agenda_bloqueios com hora) ─────────── + IF NOT v_ocupado THEN + SELECT EXISTS ( + SELECT 1 FROM public.agenda_bloqueios b + WHERE b.owner_id = v_owner_id + AND b.data_inicio <= p_data + AND COALESCE(b.data_fim, p_data) >= p_data + AND b.hora_inicio IS NOT NULL + AND b.hora_inicio < v_slot_fim + AND b.hora_fim > v_slot + AND ( + (NOT b.recorrente) + OR (b.recorrente AND b.dia_semana = v_db_dow) + ) + ) INTO v_ocupado; + END IF; + -- ── Eventos avulsos internos (agenda_eventos) ──────────────────────────── IF NOT v_ocupado THEN SELECT EXISTS ( @@ -73,7 +107,6 @@ BEGIN END IF; -- ── Recorrências ativas (recurrence_rules) ─────────────────────────────── - -- Loop explícito para evitar erros de tipo no cálculo do ciclo semanal IF NOT v_ocupado THEN FOR v_rule IN SELECT @@ -92,16 +125,12 @@ BEGIN AND r.start_time::time < v_slot_fim AND r.end_time::time > v_slot LOOP - -- Calcula a primeira ocorrência do dia-da-semana a partir do start_date v_rule_start_dow := extract(dow from v_rule.start_date)::int; v_first_occ := v_rule.start_date + (((v_db_dow - v_rule_start_dow + 7) % 7))::int; v_day_diff := (p_data - v_first_occ)::int; - -- Ocorrência válida: diff >= 0 e divisível pelo ciclo semanal IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN - - -- Verifica se há exceção para esta data v_ex_type := NULL; SELECT ex.type INTO v_ex_type FROM public.recurrence_exceptions ex @@ -109,21 +138,19 @@ BEGIN AND ex.original_date = p_data LIMIT 1; - -- Sem exceção, ou exceção que não cancela → bloqueia o slot IF v_ex_type IS NULL OR v_ex_type NOT IN ( 'cancel_session', 'patient_missed', 'therapist_canceled', 'holiday_block', 'reschedule_session' ) THEN v_ocupado := true; - EXIT; -- já basta uma regra que conflite + EXIT; END IF; - END IF; END LOOP; END IF; - -- ── Recorrências remarcadas para este dia (reschedule → new_date = p_data) ─ + -- ── Recorrências remarcadas para este dia ──────────────────────────────── IF NOT v_ocupado THEN SELECT EXISTS ( SELECT 1 @@ -180,6 +207,7 @@ DECLARE v_data_fim date; v_db_dow int; v_tem_slot boolean; + v_bloqueado boolean; BEGIN SELECT c.owner_id, c.antecedencia_minima_horas INTO v_owner_id, v_antecedencia @@ -197,6 +225,25 @@ BEGIN WHILE v_data <= v_data_fim LOOP v_db_dow := extract(dow from v_data::timestamp)::int; + -- ── Dia inteiro bloqueado? (agenda_bloqueios) ───────────────────────── + SELECT EXISTS ( + SELECT 1 FROM public.agenda_bloqueios b + WHERE b.owner_id = v_owner_id + AND b.data_inicio <= v_data + AND COALESCE(b.data_fim, v_data) >= v_data + AND b.hora_inicio IS NULL -- bloqueio de dia inteiro + AND ( + (NOT b.recorrente) + OR (b.recorrente AND b.dia_semana = v_db_dow) + ) + ) INTO v_bloqueado; + + IF v_bloqueado THEN + v_data := v_data + 1; + CONTINUE; + END IF; + + -- ── Tem slots disponíveis no dia? ───────────────────────────────────── SELECT EXISTS ( SELECT 1 FROM public.agenda_online_slots s WHERE s.owner_id = v_owner_id diff --git a/src/features/agenda/components/AgendaEventDialog.vue b/src/features/agenda/components/AgendaEventDialog.vue index 74feeb8..b9443aa 100644 --- a/src/features/agenda/components/AgendaEventDialog.vue +++ b/src/features/agenda/components/AgendaEventDialog.vue @@ -1089,6 +1089,24 @@ v-tooltip.bottom="'Remover'" @click="onDelete" /> + + + + + Google Agenda +