-- ============================================================================= -- CRMIA - FULL SCHEMA INIT (CONSOLIDATED) -- ============================================================================= -- -- Generated at: 2025-12-09 -- Purpose: Consolidate all previous migrations into a single file for fresh install. -- -- Includes: -- 1. Base Single-Tenant Schema -- 2. Cockpit Features (Deal Notes, Files, Scripts) -- 3. AI Key Separation -- 4. Expanded System Scripts -- 5. Board Archive Features (Won/Lost configurations) -- -- ============================================================================= -- ============================================================================= -- PART 1: BASE SCHEMA (Originally 20251207120000_schema_v2_single_tenant.sql) -- ============================================================================= -- ----------------------------------------------------------------------------- -- 1. EXTENSÕES -- ----------------------------------------------------------------------------- CREATE EXTENSION IF NOT EXISTS "uuid-ossp" WITH SCHEMA extensions; CREATE EXTENSION IF NOT EXISTS "pgcrypto" WITH SCHEMA extensions; -- ############################################################################# -- PARTE 1: TABELAS PRINCIPAIS -- ############################################################################# -- ----------------------------------------------------------------------------- -- 2. ORGANIZATIONS (Mantida para compatibilidade de dados) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.organizations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, deleted_at TIMESTAMPTZ DEFAULT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.organizations ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 3. PROFILES (Usuários - estende auth.users) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.profiles ( id UUID PRIMARY KEY REFERENCES auth.users(id) ON DELETE CASCADE, email TEXT, name TEXT, avatar TEXT, role TEXT DEFAULT 'user', organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE, first_name TEXT, last_name TEXT, nickname TEXT, phone TEXT, avatar_url TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 4. LIFECYCLE_STAGES (Estágios do funil - GLOBAL) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.lifecycle_stages ( id TEXT PRIMARY KEY, name TEXT NOT NULL, color TEXT NOT NULL, "order" INTEGER NOT NULL, is_default BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.lifecycle_stages ENABLE ROW LEVEL SECURITY; INSERT INTO public.lifecycle_stages (id, name, color, "order", is_default) VALUES ('LEAD', 'Lead', 'bg-blue-500', 0, true), ('MQL', 'MQL', 'bg-yellow-500', 1, true), ('PROSPECT', 'Oportunidade', 'bg-purple-500', 2, true), ('CUSTOMER', 'Cliente', 'bg-green-500', 3, true), ('OTHER', 'Outros / Perdidos', 'bg-slate-500', 4, true) ON CONFLICT (id) DO NOTHING; -- ----------------------------------------------------------------------------- -- 5. CRM_COMPANIES (Empresas dos CLIENTES do CRM) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.crm_companies ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, industry TEXT, website TEXT, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.crm_companies ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 6. BOARDS (Quadros Kanban) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.boards ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, type TEXT DEFAULT 'SALES', is_default BOOLEAN DEFAULT false, template TEXT, linked_lifecycle_stage TEXT, next_board_id UUID REFERENCES public.boards(id), goal_description TEXT, goal_kpi TEXT, goal_target_value TEXT, goal_type TEXT, agent_name TEXT, agent_role TEXT, agent_behavior TEXT, entry_trigger TEXT, automation_suggestions TEXT[], position INTEGER DEFAULT 0, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.boards ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 7. BOARD_STAGES (Colunas dos quadros) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.board_stages ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), board_id UUID REFERENCES public.boards(id) ON DELETE CASCADE, name TEXT NOT NULL, label TEXT, color TEXT, "order" INTEGER NOT NULL, is_default BOOLEAN DEFAULT false, linked_lifecycle_stage TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.board_stages ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 8. CONTACTS (Contatos) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.contacts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, email TEXT, phone TEXT, role TEXT, company_name TEXT, client_company_id UUID REFERENCES public.crm_companies(id), avatar TEXT, notes TEXT, status TEXT DEFAULT 'ACTIVE', stage TEXT DEFAULT 'LEAD', source TEXT, birth_date DATE, last_interaction TIMESTAMPTZ, last_purchase_date DATE, total_value NUMERIC DEFAULT 0, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 9. PRODUCTS (Catálogo de produtos/serviços) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.products ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, description TEXT, price NUMERIC NOT NULL DEFAULT 0, sku TEXT, active BOOLEAN DEFAULT true, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.products ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 10. DEALS (Negócios/Oportunidades) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.deals ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, value NUMERIC DEFAULT 0, probability INTEGER DEFAULT 0, status TEXT, priority TEXT DEFAULT 'medium', board_id UUID REFERENCES public.boards(id), stage_id UUID REFERENCES public.board_stages(id), contact_id UUID REFERENCES public.contacts(id), client_company_id UUID REFERENCES public.crm_companies(id), ai_summary TEXT, loss_reason TEXT, tags TEXT[] DEFAULT '{}', last_stage_change_date TIMESTAMPTZ, custom_fields JSONB DEFAULT '{}', is_won BOOLEAN DEFAULT FALSE, is_lost BOOLEAN DEFAULT FALSE, closed_at TIMESTAMPTZ, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.deals ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 11. DEAL_ITEMS (Produtos vinculados a deals) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.deal_items ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), deal_id UUID REFERENCES public.deals(id) ON DELETE CASCADE, product_id UUID REFERENCES public.products(id), name TEXT NOT NULL, quantity INTEGER NOT NULL DEFAULT 1, price NUMERIC NOT NULL DEFAULT 0, created_at TIMESTAMPTZ DEFAULT NOW(), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.deal_items ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 12. ACTIVITIES (Atividades: tarefas, ligações, reuniões) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.activities ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, description TEXT, type TEXT NOT NULL, date TIMESTAMPTZ NOT NULL, completed BOOLEAN DEFAULT false, deal_id UUID REFERENCES public.deals(id) ON DELETE CASCADE, contact_id UUID REFERENCES public.contacts(id) ON DELETE SET NULL, deleted_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.activities ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 13. TAGS (Sistema de tags) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.tags ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, color TEXT DEFAULT 'bg-gray-500', created_at TIMESTAMPTZ DEFAULT NOW(), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE, UNIQUE(name, organization_id) ); ALTER TABLE public.tags ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 14. CUSTOM_FIELD_DEFINITIONS (Campos personalizados) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.custom_field_definitions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), key TEXT NOT NULL, label TEXT NOT NULL, type TEXT NOT NULL, options TEXT[], entity_type TEXT NOT NULL DEFAULT 'deal', created_at TIMESTAMPTZ DEFAULT NOW(), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE, UNIQUE(key, organization_id) ); ALTER TABLE public.custom_field_definitions ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 15. LEADS (Para importação de leads) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.leads ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), name TEXT NOT NULL, email TEXT, company_name TEXT, role TEXT, source TEXT, status TEXT DEFAULT 'NEW', notes TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), converted_to_contact_id UUID REFERENCES public.contacts(id), owner_id UUID REFERENCES public.profiles(id), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE ); ALTER TABLE public.leads ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 16. USER_SETTINGS (Configurações do usuário) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.user_settings ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE UNIQUE, ai_provider TEXT DEFAULT 'google', ai_api_key TEXT, ai_model TEXT DEFAULT 'gemini-2.5-flash', ai_thinking BOOLEAN DEFAULT true, ai_search BOOLEAN DEFAULT true, ai_anthropic_caching BOOLEAN DEFAULT false, dark_mode BOOLEAN DEFAULT true, default_route TEXT DEFAULT '/boards', active_board_id UUID REFERENCES public.boards(id), inbox_view_mode TEXT DEFAULT 'list', onboarding_completed BOOLEAN DEFAULT false, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.user_settings ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 17. AI_CONVERSATIONS (Histórico de conversas com IA) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.ai_conversations ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE, conversation_key TEXT NOT NULL, messages JSONB DEFAULT '[]', created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, conversation_key) ); ALTER TABLE public.ai_conversations ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 18. AI_DECISIONS (Fila de decisões da IA) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.ai_decisions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE, deal_id UUID REFERENCES public.deals(id) ON DELETE CASCADE, contact_id UUID REFERENCES public.contacts(id) ON DELETE SET NULL, decision_type TEXT NOT NULL, priority TEXT DEFAULT 'medium', title TEXT NOT NULL, description TEXT, suggested_action JSONB, status TEXT DEFAULT 'pending', snoozed_until TIMESTAMPTZ, processed_at TIMESTAMPTZ, ai_reasoning TEXT, confidence_score NUMERIC(3,2), created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.ai_decisions ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 19. AI_AUDIO_NOTES (Notas de áudio transcritas) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.ai_audio_notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE, deal_id UUID REFERENCES public.deals(id) ON DELETE CASCADE, contact_id UUID REFERENCES public.contacts(id) ON DELETE SET NULL, audio_url TEXT, duration_seconds INTEGER, transcription TEXT NOT NULL, sentiment TEXT, next_action JSONB, activity_created_id UUID REFERENCES public.activities(id), created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.ai_audio_notes ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 20. ORGANIZATION_INVITES (Convites para novos usuários) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.organization_invites ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE, email TEXT, role TEXT NOT NULL DEFAULT 'vendedor', token UUID NOT NULL DEFAULT gen_random_uuid(), created_at TIMESTAMPTZ DEFAULT NOW(), expires_at TIMESTAMPTZ, used_at TIMESTAMPTZ, created_by UUID REFERENCES public.profiles(id) ); ALTER TABLE public.organization_invites ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 21. SYSTEM_NOTIFICATIONS (Notificações do sistema) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.system_notifications ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID REFERENCES public.organizations(id) ON DELETE CASCADE, type TEXT NOT NULL, title TEXT NOT NULL, message TEXT NOT NULL, link TEXT, severity TEXT DEFAULT 'medium' CHECK (severity IN ('high', 'medium', 'low')), read_at TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.system_notifications ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 22. AI_SUGGESTION_INTERACTIONS (Track AI suggestion actions) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.ai_suggestion_interactions ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, suggestion_type TEXT NOT NULL CHECK (suggestion_type IN ('UPSELL', 'STALLED', 'BIRTHDAY', 'RESCUE')), entity_type TEXT NOT NULL CHECK (entity_type IN ('deal', 'contact')), entity_id UUID NOT NULL, action TEXT NOT NULL CHECK (action IN ('ACCEPTED', 'DISMISSED', 'SNOOZED')), snoozed_until TIMESTAMPTZ, created_at TIMESTAMPTZ DEFAULT NOW(), UNIQUE(user_id, suggestion_type, entity_id) ); ALTER TABLE public.ai_suggestion_interactions ENABLE ROW LEVEL SECURITY; CREATE INDEX IF NOT EXISTS idx_ai_suggestion_user ON public.ai_suggestion_interactions(user_id); CREATE INDEX IF NOT EXISTS idx_ai_suggestion_entity ON public.ai_suggestion_interactions(entity_type, entity_id); -- ############################################################################# -- PARTE 2: TABELAS DE SEGURANÇA -- ############################################################################# -- ----------------------------------------------------------------------------- -- 23. RATE_LIMITS (Rate limiting para Edge Functions) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.rate_limits ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), identifier TEXT NOT NULL, endpoint TEXT NOT NULL, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() ); CREATE INDEX IF NOT EXISTS idx_rate_limits_lookup ON public.rate_limits (identifier, endpoint, created_at DESC); ALTER TABLE public.rate_limits ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 24. USER_CONSENTS (LGPD Compliance) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.user_consents ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID NOT NULL REFERENCES auth.users(id) ON DELETE CASCADE, consent_type TEXT NOT NULL, version TEXT NOT NULL, consented_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), ip_address TEXT, user_agent TEXT, revoked_at TIMESTAMPTZ, CONSTRAINT valid_consent_type CHECK ( consent_type IN ('terms', 'privacy', 'marketing', 'analytics', 'data_processing', 'AI_CONSENT') ) ); ALTER TABLE public.user_consents ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 25. AUDIT_LOGS (Security Monitoring) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.audit_logs ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, organization_id UUID REFERENCES public.organizations(id) ON DELETE SET NULL, action TEXT NOT NULL, resource_type TEXT NOT NULL, resource_id UUID, details JSONB DEFAULT '{}', ip_address TEXT, user_agent TEXT, created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), severity TEXT NOT NULL DEFAULT 'info', CONSTRAINT valid_severity CHECK (severity IN ('debug', 'info', 'warning', 'error', 'critical')) ); ALTER TABLE public.audit_logs ENABLE ROW LEVEL SECURITY; -- ----------------------------------------------------------------------------- -- 26. SECURITY_ALERTS (Sistema de alertas de segurança) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS security_alerts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), organization_id UUID REFERENCES organizations(id) ON DELETE CASCADE, alert_type VARCHAR(50) NOT NULL, severity VARCHAR(20) NOT NULL DEFAULT 'warning', title VARCHAR(255) NOT NULL, description TEXT, details JSONB, user_id UUID REFERENCES auth.users(id) ON DELETE SET NULL, acknowledged_at TIMESTAMPTZ, acknowledged_by UUID REFERENCES auth.users(id) ON DELETE SET NULL, created_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE security_alerts ENABLE ROW LEVEL SECURITY; -- ############################################################################# -- PARTE 3: STORAGE BUCKETS -- ############################################################################# INSERT INTO storage.buckets (id, name, public) VALUES ('avatars', 'avatars', true) ON CONFLICT (id) DO NOTHING; INSERT INTO storage.buckets (id, name, public) VALUES ('audio-notes', 'audio-notes', false) ON CONFLICT (id) DO NOTHING; -- ############################################################################# -- PARTE 4: FUNÇÕES AUXILIARES (SIMPLIFICADAS) -- ############################################################################# -- Verificar se instância foi inicializada CREATE OR REPLACE FUNCTION public.is_instance_initialized() RETURNS BOOLEAN LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN RETURN EXISTS (SELECT 1 FROM public.organizations LIMIT 1); END; $$; -- Estatísticas do Dashboard (simplificado - sem filtro de tenant) CREATE OR REPLACE FUNCTION public.get_dashboard_stats() RETURNS JSON LANGUAGE plpgsql SECURITY DEFINER AS $$ DECLARE result JSON; BEGIN SELECT json_build_object( 'total_deals', (SELECT COUNT(*) FROM public.deals WHERE deleted_at IS NULL), 'pipeline_value', (SELECT COALESCE(SUM(value), 0) FROM public.deals WHERE is_won = FALSE AND is_lost = FALSE AND deleted_at IS NULL), 'total_contacts', (SELECT COUNT(*) FROM public.contacts WHERE deleted_at IS NULL), 'total_companies', (SELECT COUNT(*) FROM public.crm_companies WHERE deleted_at IS NULL), 'won_deals', (SELECT COUNT(*) FROM public.deals WHERE is_won = TRUE AND deleted_at IS NULL), 'won_value', (SELECT COALESCE(SUM(value), 0) FROM public.deals WHERE is_won = TRUE AND deleted_at IS NULL), 'lost_deals', (SELECT COUNT(*) FROM public.deals WHERE is_lost = TRUE AND deleted_at IS NULL), 'activities_today', (SELECT COUNT(*) FROM public.activities WHERE DATE(date) = CURRENT_DATE AND deleted_at IS NULL) ) INTO result; RETURN result; END; $$; -- Funções para marcar deals como ganho/perdido (simplificadas) CREATE OR REPLACE FUNCTION public.mark_deal_won(deal_id UUID) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN UPDATE public.deals SET is_won = TRUE, is_lost = FALSE, closed_at = NOW(), updated_at = NOW() WHERE id = deal_id; END; $$; CREATE OR REPLACE FUNCTION public.mark_deal_lost(deal_id UUID, reason TEXT DEFAULT NULL) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN UPDATE public.deals SET is_lost = TRUE, is_won = FALSE, loss_reason = COALESCE(reason, loss_reason), closed_at = NOW(), updated_at = NOW() WHERE id = deal_id; END; $$; CREATE OR REPLACE FUNCTION public.reopen_deal(deal_id UUID) RETURNS VOID LANGUAGE plpgsql SECURITY DEFINER AS $$ BEGIN UPDATE public.deals SET is_won = FALSE, is_lost = FALSE, closed_at = NULL, updated_at = NOW() WHERE id = deal_id; END; $$; -- Contagem de contatos por estágio CREATE OR REPLACE FUNCTION get_contact_stage_counts() RETURNS TABLE ( stage TEXT, count BIGINT ) LANGUAGE sql SECURITY DEFINER STABLE AS $$ SELECT stage, COUNT(*)::BIGINT as count FROM contacts WHERE deleted_at IS NULL GROUP BY stage; $$; -- Log de auditoria (simplificado) CREATE OR REPLACE FUNCTION log_audit_event( p_action TEXT, p_resource_type TEXT, p_resource_id UUID DEFAULT NULL, p_details JSONB DEFAULT '{}', p_severity TEXT DEFAULT 'info' ) RETURNS UUID LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE v_user_id UUID; v_log_id UUID; BEGIN v_user_id := auth.uid(); INSERT INTO public.audit_logs ( user_id, action, resource_type, resource_id, details, severity ) VALUES ( v_user_id, p_action, p_resource_type, p_resource_id, p_details, p_severity ) RETURNING id INTO v_log_id; RETURN v_log_id; END; $$; -- Cleanup rate limits CREATE OR REPLACE FUNCTION cleanup_rate_limits(older_than_minutes INTEGER DEFAULT 5) RETURNS INTEGER LANGUAGE plpgsql SECURITY DEFINER SET search_path = public AS $$ DECLARE deleted_count INTEGER; BEGIN DELETE FROM public.rate_limits WHERE created_at < NOW() - (older_than_minutes || ' minutes')::INTERVAL; GET DIAGNOSTICS deleted_count = ROW_COUNT; RETURN deleted_count; END; $$; -- ############################################################################# -- PARTE 5: TRIGGERS (SIMPLIFICADOS) -- ############################################################################# -- Trigger: criar profile quando usuário se cadastra CREATE OR REPLACE FUNCTION public.handle_new_user() RETURNS trigger AS $$ BEGIN INSERT INTO public.profiles (id, email, name, avatar, role, organization_id) VALUES ( new.id, new.email, COALESCE(new.raw_user_meta_data->>'name', new.raw_user_meta_data->>'full_name', split_part(new.email, '@', 1)), new.raw_user_meta_data->>'avatar_url', COALESCE(new.raw_user_meta_data->>'role', 'user'), (new.raw_user_meta_data->>'organization_id')::uuid ); RETURN new; END; $$ LANGUAGE plpgsql SECURITY DEFINER; DROP TRIGGER IF EXISTS on_auth_user_created ON auth.users; CREATE TRIGGER on_auth_user_created AFTER INSERT ON auth.users FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user(); -- Cascade soft delete: boards -> deals CREATE OR REPLACE FUNCTION cascade_soft_delete_deals() RETURNS TRIGGER AS $$ BEGIN IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN UPDATE deals SET deleted_at = NEW.deleted_at WHERE board_id = NEW.id AND deleted_at IS NULL; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS cascade_board_delete ON boards; CREATE TRIGGER cascade_board_delete AFTER UPDATE OF deleted_at ON boards FOR EACH ROW WHEN (NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL) EXECUTE FUNCTION cascade_soft_delete_deals(); -- Cascade soft delete: contacts -> activities CREATE OR REPLACE FUNCTION cascade_soft_delete_activities_by_contact() RETURNS TRIGGER AS $$ BEGIN IF NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL THEN UPDATE activities SET deleted_at = NEW.deleted_at WHERE contact_id = NEW.id AND deleted_at IS NULL; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS cascade_contact_delete ON contacts; CREATE TRIGGER cascade_contact_delete AFTER UPDATE OF deleted_at ON contacts FOR EACH ROW WHEN (NEW.deleted_at IS NOT NULL AND OLD.deleted_at IS NULL) EXECUTE FUNCTION cascade_soft_delete_activities_by_contact(); -- Trigger: prevent duplicate deals (same contact + stage) CREATE OR REPLACE FUNCTION check_deal_duplicate() RETURNS TRIGGER AS $$ DECLARE existing_deal RECORD; BEGIN IF NEW.contact_id IS NULL THEN RETURN NEW; END IF; -- Fix: Check if deal is OPEN via is_won/is_lost flags -- A deal is open if NOT won AND NOT lost SELECT d.id, d.title, bs.label as stage_name INTO existing_deal FROM deals d LEFT JOIN board_stages bs ON d.stage_id = bs.id WHERE d.contact_id = NEW.contact_id AND d.stage_id = NEW.stage_id AND d.deleted_at IS NULL AND d.is_won = FALSE AND d.is_lost = FALSE AND NEW.is_won = FALSE AND NEW.is_lost = FALSE AND (TG_OP = 'INSERT' OR d.id != NEW.id) LIMIT 1; IF FOUND THEN RAISE EXCEPTION 'Já existe um negócio para este contato no estágio "%". Mova o negócio existente ou escolha outro estágio.', COALESCE(existing_deal.stage_name, 'desconhecido') USING ERRCODE = 'unique_violation'; END IF; RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS check_deal_duplicate_trigger ON deals; CREATE TRIGGER check_deal_duplicate_trigger BEFORE INSERT OR UPDATE ON deals FOR EACH ROW EXECUTE FUNCTION check_deal_duplicate(); -- ############################################################################# -- PARTE 6: ROW LEVEL SECURITY POLICIES (SIMPLIFICADAS) -- ############################################################################# -- ORGANIZATIONS (acesso livre para authenticated) DROP POLICY IF EXISTS "authenticated_access" ON public.organizations; CREATE POLICY "authenticated_access" ON public.organizations FOR ALL TO authenticated USING (deleted_at IS NULL) WITH CHECK (true); -- PROFILES DROP POLICY IF EXISTS "profiles_select" ON public.profiles; CREATE POLICY "profiles_select" ON public.profiles FOR SELECT TO authenticated USING (true); DROP POLICY IF EXISTS "profiles_update" ON public.profiles; CREATE POLICY "profiles_update" ON public.profiles FOR UPDATE TO authenticated USING (id = auth.uid()) WITH CHECK (id = auth.uid()); -- USER SETTINGS DROP POLICY IF EXISTS "user_settings_isolate" ON public.user_settings; CREATE POLICY "user_settings_isolate" ON public.user_settings FOR ALL TO authenticated USING (user_id = auth.uid()) WITH CHECK (user_id = auth.uid()); -- ============================================================================= -- PART 2: COCKPIT FEATURES (Originally 20251208000000_cockpit_features.sql) -- ============================================================================= -- ----------------------------------------------------------------------------- -- 1. DEAL_NOTES (Notas por Deal) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.deal_notes ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), deal_id UUID NOT NULL REFERENCES public.deals(id) ON DELETE CASCADE, content TEXT NOT NULL, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL ); ALTER TABLE public.deal_notes ENABLE ROW LEVEL SECURITY; CREATE INDEX IF NOT EXISTS idx_deal_notes_deal ON public.deal_notes(deal_id); CREATE INDEX IF NOT EXISTS idx_deal_notes_created ON public.deal_notes(created_at DESC); -- RLS: Todos usuários autenticados podem ler/escrever (single-tenant) DROP POLICY IF EXISTS "deal_notes_access" ON public.deal_notes; CREATE POLICY "deal_notes_access" ON public.deal_notes FOR ALL TO authenticated USING (true) WITH CHECK (true); -- ----------------------------------------------------------------------------- -- 2. DEAL_FILES (Arquivos por Deal) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.deal_files ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), deal_id UUID NOT NULL REFERENCES public.deals(id) ON DELETE CASCADE, file_name TEXT NOT NULL, file_path TEXT NOT NULL, file_size INTEGER, mime_type TEXT, created_at TIMESTAMPTZ DEFAULT NOW(), created_by UUID REFERENCES public.profiles(id) ON DELETE SET NULL ); ALTER TABLE public.deal_files ENABLE ROW LEVEL SECURITY; CREATE INDEX IF NOT EXISTS idx_deal_files_deal ON public.deal_files(deal_id); -- RLS DROP POLICY IF EXISTS "deal_files_access" ON public.deal_files; CREATE POLICY "deal_files_access" ON public.deal_files FOR ALL TO authenticated USING (true) WITH CHECK (true); -- ----------------------------------------------------------------------------- -- 3. STORAGE BUCKET FOR DEAL FILES -- ----------------------------------------------------------------------------- INSERT INTO storage.buckets (id, name, public, file_size_limit) VALUES ('deal-files', 'deal-files', false, 10485760) -- 10MB limit ON CONFLICT (id) DO UPDATE SET file_size_limit = 10485760; -- Storage policies DROP POLICY IF EXISTS "deal_files_upload" ON storage.objects; CREATE POLICY "deal_files_upload" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'deal-files'); DROP POLICY IF EXISTS "deal_files_read" ON storage.objects; CREATE POLICY "deal_files_read" ON storage.objects FOR SELECT TO authenticated USING (bucket_id = 'deal-files'); DROP POLICY IF EXISTS "deal_files_delete" ON storage.objects; CREATE POLICY "deal_files_delete" ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'deal-files'); -- ----------------------------------------------------------------------------- -- 4. QUICK_SCRIPTS (Templates de Scripts) -- ----------------------------------------------------------------------------- CREATE TABLE IF NOT EXISTS public.quick_scripts ( id UUID PRIMARY KEY DEFAULT gen_random_uuid(), title TEXT NOT NULL, category TEXT NOT NULL CHECK (category IN ('followup', 'objection', 'closing', 'intro', 'rescue', 'other')), template TEXT NOT NULL, icon TEXT DEFAULT 'MessageSquare', is_system BOOLEAN DEFAULT false, -- Scripts do sistema (não podem ser deletados) user_id UUID REFERENCES public.profiles(id) ON DELETE CASCADE, created_at TIMESTAMPTZ DEFAULT NOW(), updated_at TIMESTAMPTZ DEFAULT NOW() ); ALTER TABLE public.quick_scripts ENABLE ROW LEVEL SECURITY; CREATE INDEX IF NOT EXISTS idx_quick_scripts_user ON public.quick_scripts(user_id); CREATE INDEX IF NOT EXISTS idx_quick_scripts_category ON public.quick_scripts(category); -- RLS: Ver scripts do sistema + próprios, criar/editar/deletar apenas próprios DROP POLICY IF EXISTS "quick_scripts_select" ON public.quick_scripts; CREATE POLICY "quick_scripts_select" ON public.quick_scripts FOR SELECT TO authenticated USING (is_system = true OR user_id = auth.uid()); DROP POLICY IF EXISTS "quick_scripts_insert" ON public.quick_scripts; CREATE POLICY "quick_scripts_insert" ON public.quick_scripts FOR INSERT TO authenticated WITH CHECK (user_id = auth.uid() AND is_system = false); DROP POLICY IF EXISTS "quick_scripts_update" ON public.quick_scripts; CREATE POLICY "quick_scripts_update" ON public.quick_scripts FOR UPDATE TO authenticated USING (user_id = auth.uid() AND is_system = false) WITH CHECK (user_id = auth.uid() AND is_system = false); DROP POLICY IF EXISTS "quick_scripts_delete" ON public.quick_scripts; CREATE POLICY "quick_scripts_delete" ON public.quick_scripts FOR DELETE TO authenticated USING (user_id = auth.uid() AND is_system = false); -- ----------------------------------------------------------------------------- -- 5. SEED: Default System Scripts -- ----------------------------------------------------------------------------- INSERT INTO public.quick_scripts (title, category, template, icon, is_system, user_id) VALUES ( 'Follow-up Amigável', 'followup', 'Olá {nome}! 👋 Tudo bem? Estou passando para ver como estão as coisas por aí. Conseguiu avaliar nossa última conversa? Fico à disposição para tirar qualquer dúvida! Abraço!', 'MessageCircle', true, NULL ), ( 'Resposta a Objeção de Preço', 'objection', 'Entendo sua preocupação com o investimento, {nome}. O que muitos dos nossos clientes perceberam é que o retorno vem em [X meses]. Posso te mostrar alguns cases de sucesso similares ao seu negócio?', 'AlertCircle', true, NULL ), ( 'Fechamento Suave', 'closing', '{nome}, com base no que conversamos: ✅ [Benefício 1] ✅ [Benefício 2] ✅ [Benefício 3] Podemos dar o próximo passo? O que falta para fecharmos?', 'Target', true, NULL ), ( 'Primeira Abordagem', 'intro', 'Olá {nome}! 👋 Vi que você [contexto de como chegou]. Sou especialista em [área] e ajudo empresas como a [empresa] a [resultado]. Podemos bater um papo de 15 minutos essa semana?', 'Sparkles', true, NULL ), ( 'Resgate de Deal Parado', 'rescue', 'Oi {nome}, quanto tempo! 😊 Estava revisando meus contatos e lembrei da nossa última conversa sobre [assunto]. Sei que as coisas mudam, mas queria saber se faz sentido retomar de onde paramos. O que acha?', 'RefreshCw', true, NULL ) ON CONFLICT DO NOTHING; -- ----------------------------------------------------------------------------- -- 6. TRIGGER: Auto-update updated_at -- ----------------------------------------------------------------------------- CREATE OR REPLACE FUNCTION update_updated_at_column() RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = NOW(); RETURN NEW; END; $$ LANGUAGE plpgsql; DROP TRIGGER IF EXISTS update_deal_notes_updated_at ON public.deal_notes; CREATE TRIGGER update_deal_notes_updated_at BEFORE UPDATE ON public.deal_notes FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); DROP TRIGGER IF EXISTS update_quick_scripts_updated_at ON public.quick_scripts; CREATE TRIGGER update_quick_scripts_updated_at BEFORE UPDATE ON public.quick_scripts FOR EACH ROW EXECUTE FUNCTION update_updated_at_column(); -- ============================================================================= -- PART 3: AI KEY SEPARATION (Originally 20251208193000_separate_ai_keys.sql) -- ============================================================================= -- Add separate API key columns for each provider ALTER TABLE public.user_settings ADD COLUMN IF NOT EXISTS ai_google_key TEXT DEFAULT NULL, ADD COLUMN IF NOT EXISTS ai_openai_key TEXT DEFAULT NULL, ADD COLUMN IF NOT EXISTS ai_anthropic_key TEXT DEFAULT NULL; -- Keep ai_api_key as a computed/current key for backward compatibility COMMENT ON COLUMN public.user_settings.ai_api_key IS 'Legacy column - use ai__key instead'; COMMENT ON COLUMN public.user_settings.ai_google_key IS 'Google/Gemini API key'; COMMENT ON COLUMN public.user_settings.ai_openai_key IS 'OpenAI API key'; COMMENT ON COLUMN public.user_settings.ai_anthropic_key IS 'Anthropic/Claude API key'; -- ============================================================================= -- PART 4: MORE SYSTEM SCRIPTS (Originally 20251208220000_more_system_scripts.sql) -- ============================================================================= -- Additional system scripts for better coverage across sales scenarios INSERT INTO public.quick_scripts (title, category, template, icon, is_system, user_id) VALUES -- ========== FOLLOW-UP ========== ( 'Follow-up de Reunião', 'followup', 'Oi {nome}! 👋 Foi ótimo falar com você hoje sobre [assunto da reunião]. Conforme combinamos, seguem os próximos passos: 1. [Item 1] 2. [Item 2] Qualquer dúvida, estou à disposição!', 'Calendar', true, NULL ), ( 'Confirmação de Agendamento', 'followup', 'Olá {nome}! 📅 Confirmando nossa reunião: 📆 Data: [dia/mês] ⏰ Horário: [horário] 📍 Local: [link/endereço] Precisa remarcar? Me avise com antecedência! Te espero lá!', 'Clock', true, NULL ), ( 'Follow-up Pós-Proposta', 'followup', '{nome}, tudo certo? Passando para saber se teve a chance de analisar a proposta que enviei. Ficou com alguma dúvida ou quer ajustar algum ponto? Fico à disposição para conversarmos!', 'FileText', true, NULL ), -- ========== OBJEÇÕES ========== ( 'Objeção: Preciso Pensar', 'objection', 'Entendo perfeitamente, {nome}. Me conta: qual é o principal ponto que você gostaria de avaliar melhor? Assim posso te ajudar com mais informações para sua decisão.', 'Brain', true, NULL ), ( 'Objeção: Não é o Momento', 'objection', 'Compreendo, {nome}. O timing é importante mesmo. Só para eu entender melhor: o que mudaria para ser o momento ideal? Assim consigo te ajudar melhor quando fizer sentido.', 'Clock', true, NULL ), ( 'Objeção: Já Tenho Fornecedor', 'objection', 'Faz sentido, {nome}. Ter um fornecedor de confiança é importante. A maioria dos nossos clientes também tinha quando nos conheceram. O que eles descobriram foi [diferencial]. Posso te mostrar como complementamos o que você já tem?', 'Users', true, NULL ), -- ========== FECHAMENTO ========== ( 'Fechamento Urgente', 'closing', '{nome}, lembrete rápido: Nossa condição especial de [oferta] vai até [data]. Depois disso, o investimento volta ao valor normal de [valor]. Quer garantir antes que acabe?', 'Zap', true, NULL ), ( 'Fechamento por Escassez', 'closing', '{nome}, preciso ser sincero: Temos apenas [X vagas/unidades] disponíveis este mês, e já estamos com [Y] confirmadas. Consigo reservar uma para você se confirmarmos até [data]. Faz sentido?', 'AlertTriangle', true, NULL ), -- ========== APRESENTAÇÃO ========== ( 'Pedido de Indicação', 'intro', 'Oi {nome}! Estava pensando: você conhece alguém que também poderia se beneficiar de [solução]? Adoraria ajudar outros profissionais como você. Se tiver alguém em mente, me avisa! 🙏', 'UserPlus', true, NULL ), ( 'Conexão via LinkedIn', 'intro', 'Olá {nome}! Vi seu perfil no LinkedIn e me identifiquei com [algo específico]. Trabalho com [área] e ajudo [público] a [resultado]. Podemos conectar? Adoraria trocar experiências!', 'Linkedin', true, NULL ), -- ========== RESGATE ========== ( 'Reativação de Cliente Antigo', 'rescue', 'Oi {nome}! Saudades! 😊 Faz um tempo que não conversamos e queria saber como estão as coisas por aí. Temos algumas novidades que podem te interessar: [novidade]. Bora colocar o papo em dia?', 'Heart', true, NULL ), ( 'Tentativa Final', 'rescue', '{nome}, Tentei te contatar algumas vezes sem sucesso. Entendo que as coisas podem ter mudado. Se não fizer mais sentido, tudo bem! Só me avisa se devo parar ou se prefere retomar mais pra frente. 🙏', 'Flag', true, NULL ) ON CONFLICT DO NOTHING; -- ============================================================================= -- PART 5: BOARD ARCHIVE FEATURES (Originally 20251209140000_board_archive_features.sql) -- ============================================================================= -- 1. Add Explicit Won/Lost Stage IDs ALTER TABLE boards ADD COLUMN IF NOT EXISTS won_stage_id UUID REFERENCES board_stages(id); ALTER TABLE boards ADD COLUMN IF NOT EXISTS lost_stage_id UUID REFERENCES board_stages(id); -- 2. Add Stay In Stage (Archive) Flags ALTER TABLE boards ADD COLUMN IF NOT EXISTS won_stay_in_stage BOOLEAN DEFAULT FALSE; ALTER TABLE boards ADD COLUMN IF NOT EXISTS lost_stay_in_stage BOOLEAN DEFAULT FALSE; -- Trigger: Sync email from auth.users to public.profiles CREATE OR REPLACE FUNCTION public.handle_user_email_update() RETURNS trigger AS $$ BEGIN UPDATE public.profiles SET email = NEW.email, updated_at = NOW() WHERE id = NEW.id; RETURN NEW; END; $$ LANGUAGE plpgsql SECURITY DEFINER; DROP TRIGGER IF EXISTS on_auth_user_email_updated ON auth.users; CREATE TRIGGER on_auth_user_email_updated AFTER UPDATE OF email ON auth.users FOR EACH ROW WHEN (OLD.email IS DISTINCT FROM NEW.email) EXECUTE FUNCTION public.handle_user_email_update(); -- RLS Policies for Boards (Added via fix) CREATE POLICY "Enable read access for authenticated users" ON public.boards FOR SELECT TO authenticated USING (true); CREATE POLICY "Enable insert access for authenticated users" ON public.boards FOR INSERT TO authenticated WITH CHECK (true); CREATE POLICY "Enable update access for authenticated users" ON public.boards FOR UPDATE TO authenticated USING (true); CREATE POLICY "Enable delete access for authenticated users" ON public.boards FOR DELETE TO authenticated USING (true); -- RLS Policies for Board Stages (Added via fix) CREATE POLICY "Enable read access for authenticated users" ON public.board_stages FOR SELECT TO authenticated USING (true); CREATE POLICY "Enable insert access for authenticated users" ON public.board_stages FOR INSERT TO authenticated WITH CHECK (true); CREATE POLICY "Enable update access for authenticated users" ON public.board_stages FOR UPDATE TO authenticated USING (true); CREATE POLICY "Enable delete access for authenticated users" ON public.board_stages FOR DELETE TO authenticated USING (true); -- RLS Policies for Core Tables (Added via audit fix) -- Lifecycle Stages CREATE POLICY "Enable all access for authenticated users" ON public.lifecycle_stages FOR ALL TO authenticated USING (true); -- CRM Companies CREATE POLICY "Enable all access for authenticated users" ON public.crm_companies FOR ALL TO authenticated USING (true); -- Contacts CREATE POLICY "Enable all access for authenticated users" ON public.contacts FOR ALL TO authenticated USING (true); -- Products CREATE POLICY "Enable all access for authenticated users" ON public.products FOR ALL TO authenticated USING (true); -- Deals CREATE POLICY "Enable all access for authenticated users" ON public.deals FOR ALL TO authenticated USING (true); -- Deal Items CREATE POLICY "Enable all access for authenticated users" ON public.deal_items FOR ALL TO authenticated USING (true); -- Activities CREATE POLICY "Enable all access for authenticated users" ON public.activities FOR ALL TO authenticated USING (true); -- Tags CREATE POLICY "Enable all access for authenticated users" ON public.tags FOR ALL TO authenticated USING (true); -- Custom Field Definitions CREATE POLICY "Enable all access for authenticated users" ON public.custom_field_definitions FOR ALL TO authenticated USING (true); -- Leads CREATE POLICY "Enable all access for authenticated users" ON public.leads FOR ALL TO authenticated USING (true); -- AI Tables CREATE POLICY "Enable all access for authenticated users" ON public.ai_conversations FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON public.ai_decisions FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON public.ai_audio_notes FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON public.ai_suggestion_interactions FOR ALL TO authenticated USING (true); -- System Tables CREATE POLICY "Enable all access for authenticated users" ON public.organization_invites FOR ALL TO authenticated USING (true); -- Allow anon to validate invite tokens (before login) CREATE POLICY "Allow public to validate invite tokens" ON public.organization_invites FOR SELECT TO anon USING (used_at IS NULL); CREATE POLICY "Enable all access for authenticated users" ON public.system_notifications FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON public.rate_limits FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON public.user_consents FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON public.audit_logs FOR ALL TO authenticated USING (true); CREATE POLICY "Enable all access for authenticated users" ON security_alerts FOR ALL TO authenticated USING (true); -- Storage Policies for Avatars (Added via audit fix) DROP POLICY IF EXISTS "avatar_upload" ON storage.objects; CREATE POLICY "avatar_upload" ON storage.objects FOR INSERT TO authenticated WITH CHECK (bucket_id = 'avatars'); DROP POLICY IF EXISTS "avatar_select" ON storage.objects; CREATE POLICY "avatar_select" ON storage.objects FOR SELECT TO public USING (bucket_id = 'avatars'); DROP POLICY IF EXISTS "avatar_update" ON storage.objects; CREATE POLICY "avatar_update" ON storage.objects FOR UPDATE TO authenticated USING (bucket_id = 'avatars'); DROP POLICY IF EXISTS "avatar_delete" ON storage.objects; CREATE POLICY "avatar_delete" ON storage.objects FOR DELETE TO authenticated USING (bucket_id = 'avatars'); -- ============================================================================= -- PART 6: REALTIME CONFIGURATION -- ============================================================================= -- Enable Realtime for all CRM tables -- This allows real-time updates across multiple users DO $$ BEGIN -- Add tables to realtime publication if not already added IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'deals') THEN ALTER PUBLICATION supabase_realtime ADD TABLE deals; END IF; IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'activities') THEN ALTER PUBLICATION supabase_realtime ADD TABLE activities; END IF; IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'contacts') THEN ALTER PUBLICATION supabase_realtime ADD TABLE contacts; END IF; IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'crm_companies') THEN ALTER PUBLICATION supabase_realtime ADD TABLE crm_companies; END IF; IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'board_stages') THEN ALTER PUBLICATION supabase_realtime ADD TABLE board_stages; END IF; IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname = 'supabase_realtime' AND tablename = 'boards') THEN ALTER PUBLICATION supabase_realtime ADD TABLE boards; END IF; END $$; -- ============================================================================= -- PART 7: FUNCTION GRANTS -- ============================================================================= GRANT EXECUTE ON FUNCTION public.is_instance_initialized TO anon; GRANT EXECUTE ON FUNCTION public.is_instance_initialized TO authenticated; GRANT EXECUTE ON FUNCTION public.get_dashboard_stats TO authenticated; GRANT EXECUTE ON FUNCTION public.mark_deal_won TO authenticated; GRANT EXECUTE ON FUNCTION public.mark_deal_lost TO authenticated; GRANT EXECUTE ON FUNCTION public.reopen_deal TO authenticated; GRANT EXECUTE ON FUNCTION log_audit_event TO authenticated; GRANT EXECUTE ON FUNCTION get_contact_stage_counts() TO authenticated;