220 lines
7.8 KiB
PL/PgSQL
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;
|