269 lines
9.6 KiB
PL/PgSQL
269 lines
9.6 KiB
PL/PgSQL
-- ═══════════════════════════════════════════════════════════════════════════
|
|
-- FIX: agendador_slots_disponiveis + agendador_dias_disponiveis
|
|
-- Usa agenda_online_slots como fonte de slots
|
|
-- Cruzamento com: agenda_eventos, recurrence_rules/exceptions,
|
|
-- agendador_solicitacoes, agenda_bloqueios (feriados/bloqueios)
|
|
-- 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;
|
|
|
|
-- ── 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;
|
|
$$;
|
|
|
|
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;
|
|
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;
|
|
$$;
|
|
|
|
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;
|