Agenda, Agendador, Configurações
This commit is contained in:
219
DBS/2026-03-11/migrations/agendador_publico.sql
Normal file
219
DBS/2026-03-11/migrations/agendador_publico.sql
Normal file
@@ -0,0 +1,219 @@
|
||||
-- ═══════════════════════════════════════════════════════════════════════════
|
||||
-- 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;
|
||||
Reference in New Issue
Block a user