/* |-------------------------------------------------------------------------- | Agência PSI |-------------------------------------------------------------------------- | Arquivo: src/composables/useConversationOptouts.js | Data: 2026-04-21 | | Gerencia opt-outs do CRM WhatsApp (LGPD Art. 18 Sec.2). |-------------------------------------------------------------------------- */ import { ref, computed } from 'vue'; import { supabase } from '@/lib/supabase/client'; import { tenantDb } from '@/lib/supabase/tenantClient'; import { useTenantStore } from '@/stores/tenantStore'; function normalizePhoneBR(raw) { if (!raw) return ''; const digits = String(raw).replace(/\D/g, ''); // Sem DDI 55 → agrega if (digits.length === 10 || digits.length === 11) return '55' + digits; return digits; } export function useConversationOptouts() { const tenantStore = useTenantStore(); const optouts = ref([]); const keywords = ref([]); const loading = ref(false); const saving = ref(false); const activeOptouts = computed(() => optouts.value.filter((o) => !o.opted_back_in_at)); const historyOptouts = computed(() => optouts.value.filter((o) => o.opted_back_in_at)); async function load() { const tenantId = tenantStore.activeTenantId; if (!tenantId) return; loading.value = true; try { const [optsRes, kwsRes] = await Promise.all([ tenantDb().from('conversation_optouts') .select('id, phone, patient_id, source, keyword_matched, original_message, notes, opted_out_at, opted_back_in_at, blocked_by') .order('opted_out_at', { ascending: false }), tenantDb().from('conversation_optout_keywords') .select('id, keyword, enabled, is_system') .order('is_system', { ascending: false }) .order('keyword', { ascending: true }) ]); optouts.value = optsRes.data || []; keywords.value = kwsRes.data || []; // Enriquece com nome do paciente const patIds = [...new Set(optouts.value.map((o) => o.patient_id).filter(Boolean))]; if (patIds.length) { const { data: pats } = await tenantDb().from('patients').select('id, nome_completo').in('id', patIds); const patMap = Object.fromEntries((pats || []).map((p) => [p.id, p.nome_completo])); optouts.value = optouts.value.map((o) => ({ ...o, _patient_name: patMap[o.patient_id] || null })); } } catch (e) { console.error('[useConversationOptouts] load:', e?.message); } finally { loading.value = false; } } async function addManual({ phone, patientId = null, notes = null }) { const tenantId = tenantStore.activeTenantId; const cleanPhone = normalizePhoneBR(phone); if (!tenantId || !cleanPhone) return { ok: false, error: 'invalid_params' }; if (!/^\d{6,15}$/.test(cleanPhone)) return { ok: false, error: 'invalid_phone_format' }; saving.value = true; try { const { data: authData } = await supabase.auth.getUser(); const userId = authData?.user?.id; // Verifica se já existe ativo const { data: existing } = await tenantDb().from('conversation_optouts') .select('id') .eq('phone', cleanPhone) .is('opted_back_in_at', null) .maybeSingle(); if (existing) return { ok: false, error: 'already_opted_out' }; const { data, error } = await tenantDb().from('conversation_optouts') .insert({ phone: cleanPhone, patient_id: patientId, source: 'manual', notes, blocked_by: userId }) .select('id, phone, patient_id, source, notes, opted_out_at, blocked_by') .single(); if (error) throw error; optouts.value = [{ ...data, _patient_name: null }, ...optouts.value]; return { ok: true }; } catch (e) { return { ok: false, error: e?.message || 'add_failed' }; } finally { saving.value = false; } } async function restore(id) { if (!id) return { ok: false, error: 'invalid_id' }; saving.value = true; try { const now = new Date().toISOString(); const { error } = await tenantDb().from('conversation_optouts') .update({ opted_back_in_at: now }) .eq('id', id); if (error) throw error; const item = optouts.value.find((o) => o.id === id); if (item) item.opted_back_in_at = now; return { ok: true }; } catch (e) { return { ok: false, error: e?.message || 'restore_failed' }; } finally { saving.value = false; } } async function addKeyword(keyword) { const tenantId = tenantStore.activeTenantId; const clean = String(keyword || '').trim().slice(0, 100); if (!tenantId || !clean) return { ok: false, error: 'invalid_params' }; saving.value = true; try { const { data, error } = await tenantDb().from('conversation_optout_keywords') .insert({ keyword: clean, is_system: false, enabled: true }) .select('id, keyword, enabled, is_system') .single(); if (error) throw error; keywords.value = [...keywords.value, data]; return { ok: true }; } catch (e) { return { ok: false, error: e?.message || 'add_keyword_failed' }; } finally { saving.value = false; } } async function toggleKeyword(id, enabled) { saving.value = true; try { const { error } = await tenantDb().from('conversation_optout_keywords') .update({ enabled }) .eq('id', id); if (error) throw error; const item = keywords.value.find((k) => k.id === id); if (item) item.enabled = enabled; return { ok: true }; } catch (e) { return { ok: false, error: e?.message || 'toggle_failed' }; } finally { saving.value = false; } } async function deleteKeyword(id) { saving.value = true; try { const { error } = await tenantDb().from('conversation_optout_keywords') .delete() .eq('id', id); if (error) throw error; keywords.value = keywords.value.filter((k) => k.id !== id); return { ok: true }; } catch (e) { return { ok: false, error: e?.message || 'delete_failed' }; } finally { saving.value = false; } } return { optouts, keywords, activeOptouts, historyOptouts, loading, saving, load, addManual, restore, addKeyword, toggleKeyword, deleteKeyword }; }