Correcao Sidebar Classico e Rail, Correcao Layout, Ajuste de Breakpoint para Tailwind, Ajuste AppTopbar, Ajuste Menu PopOver, Recriado Paleta de Cores, Inserido algumas animações leves, Reajuste Cor items NOVOS da tabela, Drawer Ajuda Corrigido no Logout, Whatsapp, sms, email, recursos extras

This commit is contained in:
Leonardo
2026-03-24 21:26:58 -03:00
parent a89d1f5560
commit 53a4980396
453 changed files with 121427 additions and 174407 deletions

View File

@@ -0,0 +1,650 @@
-- =============================================================================
-- AgenciaPsi — Functions — Agenda
-- =============================================================================
-- agenda_cfg_sync, agendador_dias_disponiveis, agendador_gerar_slug,
-- agendador_slots_disponiveis, cancel_recurrence_from,
-- cancelar_eventos_serie, fn_agenda_regras_semanais_no_overlap,
-- split_recurrence_at, sync_busy_mirror, set_updated_at_recurrence
-- =============================================================================
CREATE FUNCTION public.agenda_cfg_sync() RETURNS trigger
LANGUAGE plpgsql
AS $$
begin
if new.agenda_view_mode = 'custom' then
new.usar_horario_admin_custom := true;
new.admin_inicio_visualizacao := new.agenda_custom_start;
new.admin_fim_visualizacao := new.agenda_custom_end;
else
new.usar_horario_admin_custom := false;
end if;
return new;
end;
$$;
ALTER FUNCTION public.agenda_cfg_sync() OWNER TO supabase_admin;
--
-- Name: agendador_dias_disponiveis(text, integer, integer); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) RETURNS TABLE(data date, tem_slots boolean)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_owner_id uuid;
v_antecedencia int;
v_agora timestamptz;
v_data date;
v_data_inicio date;
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
FROM public.agendador_configuracoes c
WHERE c.link_slug = p_slug AND c.ativo = true
LIMIT 1;
IF v_owner_id IS NULL THEN RETURN; END IF;
v_agora := now();
v_data_inicio := make_date(p_ano, p_mes, 1);
v_data_fim := (v_data_inicio + interval '1 month' - interval '1 day')::date;
v_data := v_data_inicio;
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
AND s.weekday = v_db_dow
AND s.enabled = true
AND (v_data::text || ' ' || s.time::text)::timestamp
AT TIME ZONE 'America/Sao_Paulo'
>= v_agora + (v_antecedencia || ' hours')::interval
) INTO v_tem_slot;
IF v_tem_slot THEN
data := v_data;
tem_slots := true;
RETURN NEXT;
END IF;
v_data := v_data + 1;
END LOOP;
END;
$$;
ALTER FUNCTION public.agendador_dias_disponiveis(p_slug text, p_ano integer, p_mes integer) OWNER TO supabase_admin;
--
-- Name: agendador_gerar_slug(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_slug text;
v_exists boolean;
BEGIN
-- só gera se ativou e não tem slug ainda
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
LOOP
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
SELECT EXISTS (
SELECT 1 FROM public.agendador_configuracoes
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
) INTO v_exists;
EXIT WHEN NOT v_exists;
END LOOP;
NEW.link_slug := v_slug;
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;
--
-- Name: agendador_slots_disponiveis(text, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
CREATE FUNCTION public.agendador_gerar_slug() RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
v_slug text;
v_exists boolean;
BEGIN
-- só gera se ativou e não tem slug ainda
IF NEW.ativo = true AND (NEW.link_slug IS NULL OR NEW.link_slug = '') THEN
LOOP
v_slug := lower(substring(replace(gen_random_uuid()::text, '-', ''), 1, 8));
SELECT EXISTS (
SELECT 1 FROM public.agendador_configuracoes
WHERE link_slug = v_slug AND owner_id <> NEW.owner_id
) INTO v_exists;
EXIT WHEN NOT v_exists;
END LOOP;
NEW.link_slug := v_slug;
END IF;
RETURN NEW;
END;
$$;
ALTER FUNCTION public.agendador_gerar_slug() OWNER TO supabase_admin;
--
-- Name: agendador_slots_disponiveis(text, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) RETURNS TABLE(hora time without time zone, disponivel boolean)
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_owner_id uuid;
v_duracao int;
v_antecedencia int;
v_agora timestamptz;
v_db_dow int;
v_slot time;
v_slot_fim time;
v_slot_ts timestamptz;
v_ocupado boolean;
-- loop de recorrências
v_rule RECORD;
v_rule_start_dow int;
v_first_occ date;
v_day_diff int;
v_ex_type text;
BEGIN
SELECT c.owner_id, c.duracao_sessao_min, c.antecedencia_minima_horas
INTO v_owner_id, v_duracao, v_antecedencia
FROM public.agendador_configuracoes c
WHERE c.link_slug = p_slug AND c.ativo = true
LIMIT 1;
IF v_owner_id IS NULL THEN RETURN; END IF;
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
WHERE s.owner_id = v_owner_id
AND s.weekday = v_db_dow
AND s.enabled = true
ORDER BY s.time
LOOP
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
v_ocupado := false;
-- ── Antecedência mínima ──────────────────────────────────────────────────
v_slot_ts := (p_data::text || ' ' || v_slot::text)::timestamp
AT TIME ZONE 'America/Sao_Paulo';
IF v_slot_ts < v_agora + (v_antecedencia || ' hours')::interval THEN
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 (
SELECT 1 FROM public.agenda_eventos e
WHERE e.owner_id = v_owner_id
AND e.status::text NOT IN ('cancelado', 'faltou')
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::date = p_data
AND (e.inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
AND (e.fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
) INTO v_ocupado;
END IF;
-- ── Recorrências ativas (recurrence_rules) ───────────────────────────────
IF NOT v_ocupado THEN
FOR v_rule IN
SELECT
r.id,
r.start_date::date AS start_date,
r.end_date::date AS end_date,
r.start_time::time AS start_time,
r.end_time::time AS end_time,
COALESCE(r.interval, 1)::int AS interval
FROM public.recurrence_rules r
WHERE r.owner_id = v_owner_id
AND r.status = 'ativo'
AND p_data >= r.start_date::date
AND (r.end_date IS NULL OR p_data <= r.end_date::date)
AND v_db_dow = ANY(r.weekdays)
AND r.start_time::time < v_slot_fim
AND r.end_time::time > v_slot
LOOP
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;
IF v_day_diff >= 0 AND v_day_diff % (7 * v_rule.interval) = 0 THEN
v_ex_type := NULL;
SELECT ex.type INTO v_ex_type
FROM public.recurrence_exceptions ex
WHERE ex.recurrence_id = v_rule.id
AND ex.original_date = p_data
LIMIT 1;
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;
END IF;
END IF;
END LOOP;
END IF;
-- ── Recorrências remarcadas para este dia ────────────────────────────────
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1
FROM public.recurrence_exceptions ex
JOIN public.recurrence_rules r ON r.id = ex.recurrence_id
WHERE r.owner_id = v_owner_id
AND r.status = 'ativo'
AND ex.type = 'reschedule_session'
AND ex.new_date = p_data
AND COALESCE(ex.new_start_time, r.start_time)::time < v_slot_fim
AND COALESCE(ex.new_end_time, r.end_time)::time > v_slot
) INTO v_ocupado;
END IF;
-- ── Solicitações públicas pendentes ──────────────────────────────────────
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1 FROM public.agendador_solicitacoes sol
WHERE sol.owner_id = v_owner_id
AND sol.status = 'pendente'
AND sol.data_solicitada = p_data
AND sol.hora_solicitada = v_slot
AND (sol.reservado_ate IS NULL OR sol.reservado_ate > v_agora)
) INTO v_ocupado;
END IF;
hora := v_slot;
disponivel := NOT v_ocupado;
RETURN NEXT;
END LOOP;
END;
$$;
ALTER FUNCTION public.agendador_slots_disponiveis(p_slug text, p_data date) OWNER TO supabase_admin;
--
-- Name: auto_create_financial_record_from_session(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.auto_create_financial_record_from_session() RETURNS trigger
CREATE FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) RETURNS void
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
BEGIN
UPDATE public.recurrence_rules
SET
end_date = p_from_date - INTERVAL '1 day',
open_ended = false,
status = CASE
WHEN p_from_date <= start_date THEN 'cancelado'
ELSE status
END,
updated_at = now()
WHERE id = p_recurrence_id;
END;
$$;
ALTER FUNCTION public.cancel_recurrence_from(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;
--
-- Name: cancel_subscription(uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.cancel_subscription(p_subscription_id uuid) RETURNS public.subscriptions
CREATE FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone DEFAULT now()) RETURNS integer
LANGUAGE plpgsql SECURITY DEFINER
AS $$
DECLARE
v_count integer;
BEGIN
UPDATE public.agenda_eventos
SET status = 'cancelado',
updated_at = now()
WHERE serie_id = p_serie_id
AND inicio_em >= p_a_partir_de
AND status NOT IN ('realizado', 'cancelado');
GET DIAGNOSTICS v_count = ROW_COUNT;
RETURN v_count;
END;
$$;
ALTER FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) OWNER TO supabase_admin;
--
-- Name: FUNCTION cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone); Type: COMMENT; Schema: public; Owner: supabase_admin
--
COMMENT ON FUNCTION public.cancelar_eventos_serie(p_serie_id uuid, p_a_partir_de timestamp with time zone) IS 'Cancela todos os eventos futuros de uma série a partir de p_a_partir_de (inclusive).
Não cancela eventos realizados.';
--
-- Name: change_subscription_plan(uuid, uuid); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.change_subscription_plan(p_subscription_id uuid, p_new_plan_id uuid) RETURNS public.subscriptions
CREATE FUNCTION public.fn_agenda_regras_semanais_no_overlap() RETURNS trigger
LANGUAGE plpgsql
AS $$
declare
v_count int;
begin
if new.ativo is false then
return new;
end if;
select count(*) into v_count
from public.agenda_regras_semanais r
where r.owner_id = new.owner_id
and r.dia_semana = new.dia_semana
and r.ativo is true
and (tg_op = 'INSERT' or r.id <> new.id)
and (new.hora_inicio < r.hora_fim and new.hora_fim > r.hora_inicio);
if v_count > 0 then
raise exception 'Janela sobreposta: existe uma regra ativa nesse intervalo.';
end if;
return new;
end;
$$;
ALTER FUNCTION public.fn_agenda_regras_semanais_no_overlap() OWNER TO supabase_admin;
--
-- Name: get_financial_report(uuid, date, date, text); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.get_financial_report(p_owner_id uuid, p_start_date date, p_end_date date, p_group_by text DEFAULT 'month'::text) RETURNS TABLE(group_key text, group_label text, total_receitas numeric, total_despesas numeric, saldo numeric, total_pendente numeric, total_overdue numeric, count_records bigint)
CREATE FUNCTION public.set_updated_at_recurrence() RETURNS trigger
LANGUAGE plpgsql
AS $$
BEGIN NEW.updated_at = now(); RETURN NEW; END;
$$;
ALTER FUNCTION public.set_updated_at_recurrence() OWNER TO supabase_admin;
--
-- Name: split_recurrence_at(uuid, date); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) RETURNS uuid
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
DECLARE
v_old public.recurrence_rules;
v_new_id uuid;
BEGIN
-- busca a regra original
SELECT * INTO v_old
FROM public.recurrence_rules
WHERE id = p_recurrence_id;
IF NOT FOUND THEN
RAISE EXCEPTION 'recurrence_rule % não encontrada', p_recurrence_id;
END IF;
-- encerra a regra antiga na data anterior
UPDATE public.recurrence_rules
SET
end_date = p_from_date - INTERVAL '1 day',
open_ended = false,
updated_at = now()
WHERE id = p_recurrence_id;
-- cria nova regra a partir de p_from_date
INSERT INTO public.recurrence_rules (
tenant_id, owner_id, therapist_id, patient_id,
determined_commitment_id, type, interval, weekdays,
start_time, end_time, timezone, duration_min,
start_date, end_date, max_occurrences, open_ended,
modalidade, titulo_custom, observacoes, extra_fields, status
)
SELECT
tenant_id, owner_id, therapist_id, patient_id,
determined_commitment_id, type, interval, weekdays,
start_time, end_time, timezone, duration_min,
p_from_date, v_old.end_date, v_old.max_occurrences, v_old.open_ended,
modalidade, titulo_custom, observacoes, extra_fields, status
FROM public.recurrence_rules
WHERE id = p_recurrence_id
RETURNING id INTO v_new_id;
RETURN v_new_id;
END;
$$;
ALTER FUNCTION public.split_recurrence_at(p_recurrence_id uuid, p_from_date date) OWNER TO supabase_admin;
--
-- Name: subscription_intents_view_insert(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.subscription_intents_view_insert() RETURNS trigger
CREATE FUNCTION public.sync_busy_mirror_agenda_eventos() RETURNS trigger
LANGUAGE plpgsql SECURITY DEFINER
SET search_path TO 'public'
AS $$
declare
clinic_tenant uuid;
is_personal boolean;
should_mirror boolean;
begin
-- Anti-recursão: espelho não espelha
if (tg_op <> 'DELETE') then
if new.mirror_of_event_id is not null then
return new;
end if;
else
if old.mirror_of_event_id is not null then
return old;
end if;
end if;
-- Define se é pessoal e se deve espelhar
if (tg_op = 'DELETE') then
is_personal := (old.tenant_id = old.owner_id);
should_mirror := (old.visibility_scope in ('busy_only','private'));
else
is_personal := (new.tenant_id = new.owner_id);
should_mirror := (new.visibility_scope in ('busy_only','private'));
end if;
-- Se não é pessoal, não faz nada
if not is_personal then
if (tg_op = 'DELETE') then
return old;
end if;
return new;
end if;
-- DELETE: remove espelhos existentes
if (tg_op = 'DELETE') then
delete from public.agenda_eventos e
where e.mirror_of_event_id = old.id
and e.mirror_source = 'personal_busy_mirror';
return old;
end if;
-- INSERT/UPDATE:
-- Se não deve espelhar, remove espelhos e sai
if not should_mirror then
delete from public.agenda_eventos e
where e.mirror_of_event_id = new.id
and e.mirror_source = 'personal_busy_mirror';
return new;
end if;
-- Para cada clínica onde o usuário é therapist active, cria/atualiza o "Ocupado"
for clinic_tenant in
select tm.tenant_id
from public.tenant_members tm
where tm.user_id = new.owner_id
and tm.role = 'therapist'
and tm.status = 'active'
and tm.tenant_id <> new.owner_id
loop
insert into public.agenda_eventos (
tenant_id,
owner_id,
terapeuta_id,
paciente_id,
tipo,
status,
titulo,
observacoes,
inicio_em,
fim_em,
mirror_of_event_id,
mirror_source,
visibility_scope,
created_at,
updated_at
) values (
clinic_tenant,
new.owner_id,
new.owner_id,
null,
'bloqueio'::public.tipo_evento_agenda,
'agendado'::public.status_evento_agenda,
'Ocupado',
null,
new.inicio_em,
new.fim_em,
new.id,
'personal_busy_mirror',
'public',
now(),
now()
)
on conflict (tenant_id, mirror_of_event_id) where mirror_of_event_id is not null
do update set
owner_id = excluded.owner_id,
terapeuta_id = excluded.terapeuta_id,
tipo = excluded.tipo,
status = excluded.status,
titulo = excluded.titulo,
observacoes = excluded.observacoes,
inicio_em = excluded.inicio_em,
fim_em = excluded.fim_em,
updated_at = now();
end loop;
-- Limpa espelhos de clínicas onde o vínculo therapist active não existe mais
delete from public.agenda_eventos e
where e.mirror_of_event_id = new.id
and e.mirror_source = 'personal_busy_mirror'
and not exists (
select 1
from public.tenant_members tm
where tm.user_id = new.owner_id
and tm.role = 'therapist'
and tm.status = 'active'
and tm.tenant_id = e.tenant_id
);
return new;
end;
$$;
ALTER FUNCTION public.sync_busy_mirror_agenda_eventos() OWNER TO supabase_admin;
--
-- Name: sync_overdue_financial_records(); Type: FUNCTION; Schema: public; Owner: supabase_admin
--
CREATE FUNCTION public.sync_overdue_financial_records() RETURNS integer