-- ========================================================================== -- Agencia PSI — Migracao: Atribuicao de conversa a terapeuta (CRM Grupo 3.2) -- ========================================================================== -- Criado por: Leonardo Nohama -- Data: 2026-04-21 · Sao Carlos/SP — Brasil -- -- Uma linha por (tenant_id, thread_key) — UPSERT em cada reatribuicao. -- Historico (quem atribuiu pra quem e quando) pode ser adicionado depois -- via trigger INSERT em conversation_assignment_history se virar requisito. -- -- thread_key segue o padrao de conversation_threads: -- - '' → thread de paciente conhecido -- - 'anon:' → thread de numero nao identificado -- -- RLS: -- - SELECT: qualquer membro ativo do tenant -- - INSERT/UPDATE: qualquer membro ativo do tenant (self-assign ou delegar) -- - DELETE: nao permitido (unassign = UPDATE assigned_to=NULL) -- ========================================================================== CREATE TABLE IF NOT EXISTS public.conversation_assignments ( tenant_id UUID NOT NULL REFERENCES public.tenants(id) ON DELETE CASCADE, thread_key TEXT NOT NULL, patient_id UUID REFERENCES public.patients(id) ON DELETE SET NULL, contact_number TEXT, assigned_to UUID REFERENCES auth.users(id) ON DELETE SET NULL, assigned_by UUID NOT NULL REFERENCES auth.users(id) ON DELETE SET NULL, assigned_at TIMESTAMPTZ NOT NULL DEFAULT now(), created_at TIMESTAMPTZ NOT NULL DEFAULT now(), updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (tenant_id, thread_key) ); CREATE INDEX IF NOT EXISTS idx_conv_assign_tenant_user ON public.conversation_assignments (tenant_id, assigned_to) WHERE assigned_to IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_conv_assign_patient ON public.conversation_assignments (patient_id) WHERE patient_id IS NOT NULL; -- Trigger de updated_at DROP TRIGGER IF EXISTS trg_conv_assign_updated_at ON public.conversation_assignments; CREATE TRIGGER trg_conv_assign_updated_at BEFORE UPDATE ON public.conversation_assignments FOR EACH ROW EXECUTE FUNCTION public.set_updated_at(); COMMENT ON TABLE public.conversation_assignments IS 'Atribuicao de threads de conversa a membros do tenant. Uma linha por (tenant_id, thread_key). assigned_to=NULL significa nao atribuida.'; -- -------------------------------------------------------------------------- -- RLS -- -------------------------------------------------------------------------- ALTER TABLE public.conversation_assignments ENABLE ROW LEVEL SECURITY; -- SELECT: qualquer membro ativo do tenant OU saas_admin DROP POLICY IF EXISTS "conv_assign: select tenant" ON public.conversation_assignments; CREATE POLICY "conv_assign: select tenant" ON public.conversation_assignments FOR SELECT TO authenticated USING ( public.is_saas_admin() OR EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = conversation_assignments.tenant_id AND tm.status = 'active' ) ); -- INSERT: membro ativo do tenant. assigned_by deve ser o proprio user. -- assigned_to deve ser membro ativo do mesmo tenant (ou NULL). DROP POLICY IF EXISTS "conv_assign: insert tenant" ON public.conversation_assignments; CREATE POLICY "conv_assign: insert tenant" ON public.conversation_assignments FOR INSERT TO authenticated WITH CHECK ( assigned_by = auth.uid() AND EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = conversation_assignments.tenant_id AND tm.status = 'active' ) AND ( assigned_to IS NULL OR EXISTS ( SELECT 1 FROM public.tenant_members tm2 WHERE tm2.user_id = conversation_assignments.assigned_to AND tm2.tenant_id = conversation_assignments.tenant_id AND tm2.status = 'active' ) ) ); -- UPDATE: membro ativo do tenant pode reatribuir. Mesma validacao pra assigned_to. DROP POLICY IF EXISTS "conv_assign: update tenant" ON public.conversation_assignments; CREATE POLICY "conv_assign: update tenant" ON public.conversation_assignments FOR UPDATE TO authenticated USING ( EXISTS ( SELECT 1 FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.tenant_id = conversation_assignments.tenant_id AND tm.status = 'active' ) ) WITH CHECK ( assigned_by = auth.uid() AND ( assigned_to IS NULL OR EXISTS ( SELECT 1 FROM public.tenant_members tm2 WHERE tm2.user_id = conversation_assignments.assigned_to AND tm2.tenant_id = conversation_assignments.tenant_id AND tm2.status = 'active' ) ) ); -- DELETE: bloqueado (unassign = UPDATE assigned_to=NULL) -- -------------------------------------------------------------------------- -- Atualiza view conversation_threads pra incluir assignment -- -------------------------------------------------------------------------- DROP VIEW IF EXISTS public.conversation_threads CASCADE; CREATE VIEW public.conversation_threads WITH (security_invoker = true) AS WITH base AS ( SELECT cm.id, cm.tenant_id, cm.patient_id, cm.channel, cm.body, cm.direction, cm.kanban_status, cm.read_at, cm.created_at, CASE WHEN cm.direction = 'inbound' THEN cm.from_number ELSE cm.to_number END AS contact_number, COALESCE(cm.patient_id::text, 'anon:' || COALESCE( CASE WHEN cm.direction = 'inbound' THEN cm.from_number ELSE cm.to_number END, 'unknown' )) AS thread_key FROM public.conversation_messages cm ), latest AS ( SELECT DISTINCT ON (tenant_id, thread_key) tenant_id, thread_key, patient_id, channel, contact_number, body AS last_message_body, direction AS last_message_direction, kanban_status, created_at AS last_message_at FROM base ORDER BY tenant_id, thread_key, created_at DESC ), counts AS ( SELECT tenant_id, thread_key, COUNT(*) AS message_count, COUNT(*) FILTER (WHERE direction = 'inbound' AND read_at IS NULL) AS unread_count FROM base GROUP BY tenant_id, thread_key ) SELECT l.tenant_id, l.thread_key, l.patient_id, p.nome_completo AS patient_name, l.contact_number, l.channel, c.message_count, c.unread_count, l.last_message_at, l.last_message_body, l.last_message_direction, l.kanban_status, ca.assigned_to, ca.assigned_at FROM latest l JOIN counts c ON c.tenant_id = l.tenant_id AND c.thread_key = l.thread_key LEFT JOIN public.patients p ON p.id = l.patient_id LEFT JOIN public.conversation_assignments ca ON ca.tenant_id = l.tenant_id AND ca.thread_key = l.thread_key; COMMENT ON VIEW public.conversation_threads IS 'Agregado de conversas por paciente ou numero anonimo. Base do Kanban. Inclui assignment atual do thread.'; GRANT SELECT ON public.conversation_threads TO authenticated; -- ========================================================================== -- FIM DA MIGRACAO -- ==========================================================================