Agenda, Agendador, Configurações
This commit is contained in:
221
DBS/2026-03-11/migrations/agendador_fix_slots.sql
Normal file
221
DBS/2026-03-11/migrations/agendador_fix_slots.sql
Normal file
@@ -0,0 +1,221 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- FIX: agendador_slots_disponiveis + agendador_dias_disponiveis
|
||||
-- Usa agenda_online_slots como fonte de slots
|
||||
-- Cruzamento com: agenda_eventos, recurrence_rules/exceptions, agendador_solicitacoes
|
||||
-- Execute no Supabase SQL Editor
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.agendador_slots_disponiveis(
|
||||
p_slug text,
|
||||
p_data date
|
||||
)
|
||||
RETURNS TABLE (hora time, disponivel boolean)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = 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;
|
||||
|
||||
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;
|
||||
|
||||
-- ── 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) ───────────────────────────────
|
||||
-- Loop explícito para evitar erros de tipo no cálculo do ciclo semanal
|
||||
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
|
||||
-- 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
|
||||
WHERE ex.recurrence_id = v_rule.id
|
||||
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
|
||||
END IF;
|
||||
|
||||
END IF;
|
||||
END LOOP;
|
||||
END IF;
|
||||
|
||||
-- ── Recorrências remarcadas para este dia (reschedule → new_date = p_data) ─
|
||||
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;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
|
||||
|
||||
|
||||
-- ── agendador_dias_disponiveis ───────────────────────────────────────────────
|
||||
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
|
||||
p_slug text,
|
||||
p_ano int,
|
||||
p_mes int
|
||||
)
|
||||
RETURNS TABLE (data date, tem_slots boolean)
|
||||
LANGUAGE plpgsql
|
||||
SECURITY DEFINER
|
||||
SET search_path = 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;
|
||||
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;
|
||||
|
||||
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;
|
||||
$$;
|
||||
|
||||
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|
||||
Reference in New Issue
Block a user