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