-- ═══════════════════════════════════════════════════════════════════════════ -- 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;