Files
agenciapsilmno/DBS/2026-03-11/migrations/agendador_publico.sql
2026-03-12 08:58:36 -03:00

220 lines
7.8 KiB
PL/PgSQL

-- ═══════════════════════════════════════════════════════════════════════════
-- Agendador Online — acesso público (anon) + função de slots disponíveis
-- ═══════════════════════════════════════════════════════════════════════════
-- ── 1. Geração automática de slug ──────────────────────────────────────────
-- Cria slug único de 8 chars quando o profissional ativa sem link_personalizado
CREATE OR REPLACE 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;
$$;
DROP TRIGGER IF EXISTS agendador_slug_trigger ON public.agendador_configuracoes;
CREATE TRIGGER agendador_slug_trigger
BEFORE INSERT OR UPDATE ON public.agendador_configuracoes
FOR EACH ROW EXECUTE FUNCTION public.agendador_gerar_slug();
-- ── 2. Políticas públicas (anon) ────────────────────────────────────────────
-- Leitura pública da config pelo slug (só ativo)
DROP POLICY IF EXISTS "agendador_cfg_public_read" ON public.agendador_configuracoes;
CREATE POLICY "agendador_cfg_public_read" ON public.agendador_configuracoes
FOR SELECT TO anon
USING (ativo = true AND link_slug IS NOT NULL);
-- Inserção pública de solicitações (qualquer pessoa pode solicitar)
DROP POLICY IF EXISTS "agendador_sol_public_insert" ON public.agendador_solicitacoes;
CREATE POLICY "agendador_sol_public_insert" ON public.agendador_solicitacoes
FOR INSERT TO anon
WITH CHECK (true);
-- Leitura da própria solicitação (pelo paciente logado)
DROP POLICY IF EXISTS "agendador_sol_patient_read" ON public.agendador_solicitacoes;
CREATE POLICY "agendador_sol_patient_read" ON public.agendador_solicitacoes
FOR SELECT TO authenticated
USING (auth.uid() = user_id OR auth.uid() = owner_id);
-- ── 3. Função: retorna slots disponíveis para uma data ──────────────────────
-- Roda como SECURITY DEFINER (acessa agenda_regras e agenda_eventos sem RLS)
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_reserva int;
v_antecedencia int;
v_dia_semana int; -- 0=dom..6=sab (JS) → convertemos
v_db_dow int; -- 0=dom..6=sab no Postgres (extract dow)
v_inicio time;
v_fim time;
v_slot time;
v_slot_fim time;
v_agora timestamptz;
BEGIN
-- carrega config do agendador
SELECT
c.owner_id,
c.duracao_sessao_min,
c.reserva_horas,
c.antecedencia_minima_horas
INTO v_owner_id, v_duracao, v_reserva, 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; -- 0=dom..6=sab
-- regra semanal para o dia da semana
SELECT hora_inicio, hora_fim
INTO v_inicio, v_fim
FROM public.agenda_regras_semanais
WHERE owner_id = v_owner_id
AND dia_semana = v_db_dow
AND ativo = true
LIMIT 1;
IF v_inicio IS NULL THEN
RETURN; -- profissional não atende nesse dia
END IF;
-- itera slots de v_duracao em v_duracao dentro da jornada
v_slot := v_inicio;
WHILE v_slot + (v_duracao || ' minutes')::interval <= v_fim LOOP
v_slot_fim := v_slot + (v_duracao || ' minutes')::interval;
-- bloco temporário para verificar conflitos
DECLARE
v_ocupado boolean := false;
v_slot_ts timestamptz;
BEGIN
-- antecedência mínima (compara em horário de Brasília)
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;
-- conflito com eventos existentes na agenda
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1 FROM public.agenda_eventos
WHERE owner_id = v_owner_id
AND status::text NOT IN ('cancelado', 'faltou')
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' >= p_data::timestamp
AND inicio_em AT TIME ZONE 'America/Sao_Paulo' < p_data::timestamp + interval '1 day'
AND (inicio_em AT TIME ZONE 'America/Sao_Paulo')::time < v_slot_fim
AND (fim_em AT TIME ZONE 'America/Sao_Paulo')::time > v_slot
) INTO v_ocupado;
END IF;
-- conflito com solicitações pendentes (reservadas)
IF NOT v_ocupado THEN
SELECT EXISTS (
SELECT 1 FROM public.agendador_solicitacoes
WHERE owner_id = v_owner_id
AND status = 'pendente'
AND data_solicitada = p_data
AND hora_solicitada = v_slot
AND (reservado_ate IS NULL OR reservado_ate > v_agora)
) INTO v_ocupado;
END IF;
hora := v_slot;
disponivel := NOT v_ocupado;
RETURN NEXT;
END;
v_slot := v_slot + (v_duracao || ' minutes')::interval;
END LOOP;
END;
$$;
GRANT EXECUTE ON FUNCTION public.agendador_slots_disponiveis(text, date) TO anon, authenticated;
-- ── 4. Função: retorna dias com disponibilidade no mês ─────────────────────
CREATE OR REPLACE FUNCTION public.agendador_dias_disponiveis(
p_slug text,
p_ano int,
p_mes int -- 1-12
)
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_data date;
v_data_inicio date;
v_data_fim date;
v_agora timestamptz;
v_db_dow int;
v_tem_regra 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
-- não oferece dias no passado ou dentro da antecedência mínima
IF v_data::timestamptz + '23:59:59'::interval > v_agora + (v_antecedencia || ' hours')::interval THEN
v_db_dow := extract(dow from v_data::timestamp)::int;
SELECT EXISTS (
SELECT 1 FROM public.agenda_regras_semanais
WHERE owner_id = v_owner_id AND dia_semana = v_db_dow AND ativo = true
) INTO v_tem_regra;
IF v_tem_regra THEN
data := v_data;
tem_slots := true;
RETURN NEXT;
END IF;
END IF;
v_data := v_data + 1;
END LOOP;
END;
$$;
GRANT EXECUTE ON FUNCTION public.agendador_dias_disponiveis(text, int, int) TO anon, authenticated;