F6.2 Lote C: split de notifications (tenant-local + notifications_sistema)
DB (supabase_admin): - public.notifications_sistema (cross-tenant SaaS->tenant: suporte, billing; vazio hoje, future-proof) + RLS owner_id + realtime + notify_user_sistema() - notify_on_session_status, fanout_inbound_message_to_notifications, cancel_notifications_on_opt_out/session_cancel reescritos schema-aware (search_path dinamico; notifications/notification_queue no schema; tenant_members/patients global/schema) - notify_on_intake/scheduling disparam em tabelas PUBLIC (F1b) -> roteiam pro schema via tenant_schema_for + EXECUTE format - cancel_patient_pending_notifications: notification_queue unqualified (herda search_path do trigger chamador) - detach dos 4 notif-triggers tenant de public; attach_notif_triggers recria 5 notif triggers/schema - smoke: msg inbound -> notification no schema, destinatario correto Frontend (notificationStore.js): load le das 2 fontes (tenantDb + public. notifications_sistema), merge por created_at, campo _origem; realtime 2 canais; markRead/markAllRead/archive roteiam por _origem Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,266 @@
|
||||
-- =============================================================================
|
||||
-- F6.2 Lote C — split de notifications (tenant-local vs SaaS cross-tenant)
|
||||
--
|
||||
-- ⚠️ APLICAR COMO supabase_admin (CREATE OR REPLACE de funções owned por
|
||||
-- postgres E supabase_admin; superuser preserva o owner):
|
||||
-- docker exec -i -e PGPASSWORD=postgres supabase_db_agenciapsi-primesakai \
|
||||
-- psql -U supabase_admin -h 127.0.0.1 -d postgres -v ON_ERROR_STOP=1 \
|
||||
-- < database-novo/manual/f6_2c_notifications_split.supabase_admin.sql
|
||||
--
|
||||
-- Neste projeto, TODAS as notificações atuais (inbound_message, session_status,
|
||||
-- system_alert, new_patient) são tenant-LOCAIS (avisos cross-tenant do SaaS
|
||||
-- vivem em global_notices). Então:
|
||||
-- * notifications continua tenant-local → já vive no schema do tenant (F6.1)
|
||||
-- * public.notifications_sistema é criado como o canal SaaS→tenant / dev
|
||||
-- cross-tenant (vazio hoje; pronto pro futuro: suporte, billing, etc.)
|
||||
-- Triggers de notif reescritos schema-aware; os que disparam em tabelas PUBLIC
|
||||
-- (notify_on_intake, notify_on_scheduling) roteiam pro schema via EXECUTE format.
|
||||
-- =============================================================================
|
||||
|
||||
BEGIN;
|
||||
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
-- 1) notifications_sistema (GLOBAL, cross-tenant)
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
CREATE TABLE IF NOT EXISTS public.notifications_sistema (
|
||||
id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
owner_id uuid NOT NULL, -- destinatário (user do tenant OU dev)
|
||||
tenant_id uuid REFERENCES public.tenants(id) ON DELETE CASCADE, -- contexto (nullable: alerta global)
|
||||
type text NOT NULL,
|
||||
ref_id uuid,
|
||||
ref_table text,
|
||||
payload jsonb,
|
||||
read_at timestamptz,
|
||||
archived boolean NOT NULL DEFAULT false,
|
||||
created_at timestamptz NOT NULL DEFAULT now()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS notif_sistema_owner_idx ON public.notifications_sistema (owner_id, created_at DESC) WHERE archived = false;
|
||||
|
||||
ALTER TABLE public.notifications_sistema ENABLE ROW LEVEL SECURITY;
|
||||
DROP POLICY IF EXISTS notif_sistema_owner ON public.notifications_sistema;
|
||||
CREATE POLICY notif_sistema_owner ON public.notifications_sistema
|
||||
FOR ALL TO authenticated USING (owner_id = auth.uid()) WITH CHECK (owner_id = auth.uid());
|
||||
|
||||
-- realtime
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_publication_tables WHERE pubname='supabase_realtime' AND schemaname='public' AND tablename='notifications_sistema') THEN
|
||||
ALTER PUBLICATION supabase_realtime ADD TABLE public.notifications_sistema;
|
||||
END IF;
|
||||
END $$;
|
||||
|
||||
-- helper pro futuro: emite notificação cross-tenant (dev/SaaS -> destinatário)
|
||||
CREATE OR REPLACE FUNCTION public.notify_user_sistema(
|
||||
p_owner_id uuid, p_type text, p_payload jsonb,
|
||||
p_tenant_id uuid DEFAULT NULL, p_ref_id uuid DEFAULT NULL, p_ref_table text DEFAULT NULL)
|
||||
RETURNS uuid LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE v_id uuid;
|
||||
BEGIN
|
||||
INSERT INTO public.notifications_sistema (owner_id, tenant_id, type, ref_id, ref_table, payload)
|
||||
VALUES (p_owner_id, p_tenant_id, p_type, p_ref_id, p_ref_table, p_payload)
|
||||
RETURNING id INTO v_id;
|
||||
RETURN v_id;
|
||||
END $$;
|
||||
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
-- 2) Rewrites dos triggers de notif (tenant-local) — schema-aware
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.notify_on_session_status()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE v_nome text;
|
||||
BEGIN
|
||||
IF NEW.status IN ('faltou','cancelado') AND OLD.status IS DISTINCT FROM NEW.status THEN
|
||||
PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
|
||||
SELECT nome_completo INTO v_nome FROM patients WHERE id = NEW.patient_id LIMIT 1;
|
||||
INSERT INTO notifications (owner_id, type, ref_id, ref_table, payload)
|
||||
VALUES (NEW.owner_id, 'session_status', NEW.id, 'agenda_eventos',
|
||||
jsonb_build_object(
|
||||
'title', CASE WHEN NEW.status='faltou' THEN 'Paciente faltou' ELSE 'Sessão cancelada' END,
|
||||
'detail', COALESCE(v_nome,'Paciente') || ' — ' || to_char(NEW.inicio_em,'DD/MM HH24:MI'),
|
||||
'deeplink', '/therapist/agenda',
|
||||
'avatar_initials', upper(left(COALESCE(v_nome,'?'),2))));
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.fanout_inbound_message_to_notifications()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE
|
||||
v_target_user uuid; v_title text; v_detail text; v_initials text; v_deeplink text;
|
||||
v_patient_name text; v_payload jsonb; v_tenant uuid;
|
||||
BEGIN
|
||||
IF NEW.direction <> 'inbound' THEN RETURN NEW; END IF;
|
||||
v_tenant := public.tenant_id_for_schema(TG_TABLE_SCHEMA);
|
||||
PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
|
||||
|
||||
IF NEW.patient_id IS NOT NULL THEN
|
||||
SELECT nome_completo INTO v_patient_name FROM patients WHERE id = NEW.patient_id;
|
||||
END IF;
|
||||
v_title := COALESCE(v_patient_name, NEW.from_number, 'Desconhecido');
|
||||
v_detail := COALESCE(left(NEW.body, 100), '[mensagem sem texto]');
|
||||
IF v_patient_name IS NOT NULL THEN
|
||||
v_initials := upper(left(v_patient_name,1)) || COALESCE(upper(left(split_part(v_patient_name,' ',2),1)),'');
|
||||
ELSE v_initials := '?'; END IF;
|
||||
v_deeplink := '/admin/conversas';
|
||||
v_payload := jsonb_build_object('title',v_title,'detail',v_detail,'avatar_initials',v_initials,
|
||||
'deeplink',v_deeplink,'channel',NEW.channel,'conversation_message_id',NEW.id,
|
||||
'patient_id',NEW.patient_id,'from_number',NEW.from_number);
|
||||
|
||||
-- destinatário: responsável do paciente (tenant_members é GLOBAL)
|
||||
IF NEW.patient_id IS NOT NULL THEN
|
||||
SELECT tm.user_id INTO v_target_user
|
||||
FROM patients p JOIN public.tenant_members tm ON tm.id = p.responsible_member_id
|
||||
WHERE p.id = NEW.patient_id AND tm.status = 'active' LIMIT 1;
|
||||
IF v_target_user IS NOT NULL THEN
|
||||
INSERT INTO notifications (owner_id, type, ref_id, ref_table, payload)
|
||||
VALUES (v_target_user, 'inbound_message', NULL, 'conversation_messages', v_payload);
|
||||
RETURN NEW;
|
||||
END IF;
|
||||
END IF;
|
||||
-- fallback: fan-out pros admins/therapists ativos do tenant (global)
|
||||
INSERT INTO notifications (owner_id, type, ref_id, ref_table, payload)
|
||||
SELECT tm.user_id, 'inbound_message', NULL, 'conversation_messages', v_payload
|
||||
FROM public.tenant_members tm
|
||||
WHERE tm.tenant_id = v_tenant AND tm.status = 'active'
|
||||
AND tm.role IN ('clinic_admin','tenant_admin','therapist');
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
-- helper de cancelamento: notification_queue é tenant; herda search_path do trigger chamador
|
||||
CREATE OR REPLACE FUNCTION public.cancel_patient_pending_notifications(p_patient_id uuid, p_channel text DEFAULT NULL, p_evento_id uuid DEFAULT NULL)
|
||||
RETURNS integer LANGUAGE plpgsql SECURITY DEFINER
|
||||
AS $$
|
||||
DECLARE v_canceled integer;
|
||||
BEGIN
|
||||
UPDATE notification_queue SET status='cancelado', updated_at=now()
|
||||
WHERE patient_id = p_patient_id AND status IN ('pendente','processando')
|
||||
AND (p_channel IS NULL OR channel = p_channel)
|
||||
AND (p_evento_id IS NULL OR agenda_evento_id = p_evento_id);
|
||||
GET DIAGNOSTICS v_canceled = ROW_COUNT;
|
||||
RETURN v_canceled;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.cancel_notifications_on_opt_out()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
BEGIN
|
||||
PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
|
||||
IF OLD.whatsapp_opt_in = true AND NEW.whatsapp_opt_in = false THEN
|
||||
PERFORM public.cancel_patient_pending_notifications(NEW.patient_id, 'whatsapp');
|
||||
END IF;
|
||||
IF OLD.email_opt_in = true AND NEW.email_opt_in = false THEN
|
||||
PERFORM public.cancel_patient_pending_notifications(NEW.patient_id, 'email');
|
||||
END IF;
|
||||
IF OLD.sms_opt_in = true AND NEW.sms_opt_in = false THEN
|
||||
PERFORM public.cancel_patient_pending_notifications(NEW.patient_id, 'sms');
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.cancel_notifications_on_session_cancel()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
BEGIN
|
||||
IF NEW.status = 'cancelado' AND OLD.status <> 'cancelado' THEN
|
||||
PERFORM set_config('search_path', TG_TABLE_SCHEMA || ',public,pg_temp', true);
|
||||
PERFORM public.cancel_patient_pending_notifications(NEW.patient_id, NULL, NEW.id);
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
-- 3) Triggers que disparam em tabelas PUBLIC (intake/scheduling, F1b) —
|
||||
-- roteiam a notificação pro schema do tenant via EXECUTE format
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.notify_on_intake()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE v_schema text;
|
||||
BEGIN
|
||||
IF NEW.status = 'new' THEN
|
||||
v_schema := public.tenant_schema_for(NEW.tenant_id);
|
||||
IF v_schema IS NULL THEN RETURN NEW; END IF;
|
||||
EXECUTE format('INSERT INTO %I.notifications (owner_id, type, ref_id, ref_table, payload) VALUES ($1,$2,$3,$4,$5)', v_schema)
|
||||
USING NEW.owner_id, 'new_patient', NEW.id, 'patient_intake_requests',
|
||||
jsonb_build_object('title','Novo cadastro externo','detail',COALESCE(NEW.nome_completo,'Paciente'),
|
||||
'deeplink','/therapist/patients/cadastro/recebidos','avatar_initials',upper(left(COALESCE(NEW.nome_completo,'?'),2)));
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.notify_on_scheduling()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE v_schema text;
|
||||
BEGIN
|
||||
IF NEW.status = 'pendente' THEN
|
||||
v_schema := public.tenant_schema_for(NEW.tenant_id);
|
||||
IF v_schema IS NULL THEN RETURN NEW; END IF;
|
||||
EXECUTE format('INSERT INTO %I.notifications (owner_id, type, ref_id, ref_table, payload) VALUES ($1,$2,$3,$4,$5)', v_schema)
|
||||
USING NEW.owner_id, 'new_scheduling', NEW.id, 'agendador_solicitacoes',
|
||||
jsonb_build_object('title','Nova solicitação de agendamento',
|
||||
'detail', COALESCE(NEW.paciente_nome,'Paciente') || ' ' || COALESCE(NEW.paciente_sobrenome,'') || ' — ' || COALESCE(NEW.tipo,''),
|
||||
'deeplink','/therapist/agendamentos-recebidos',
|
||||
'avatar_initials', upper(left(COALESCE(NEW.paciente_nome,'?'),1) || left(COALESCE(NEW.paciente_sobrenome,''),1)));
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END $$;
|
||||
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
-- 4) Detach dos notif-triggers tenant de public + attach nos schemas (estende
|
||||
-- attach_schema_aware_triggers com os 5 triggers de notif tenant)
|
||||
-- ───────────────────────────────────────────────────────────────────────────
|
||||
DO $$
|
||||
DECLARE
|
||||
aware text[] := ARRAY['notify_on_session_status','fanout_inbound_message_to_notifications',
|
||||
'cancel_notifications_on_opt_out','cancel_notifications_on_session_cancel'];
|
||||
r record;
|
||||
BEGIN
|
||||
FOR r IN
|
||||
SELECT c.relname AS tab, t.tgname FROM pg_trigger t JOIN pg_class c ON c.oid=t.tgrelid
|
||||
JOIN pg_namespace n ON n.oid=c.relnamespace JOIN pg_proc p ON p.oid=t.tgfoid
|
||||
WHERE n.nspname='public' AND NOT t.tgisinternal AND p.proname = ANY(aware)
|
||||
AND c.relname IN (SELECT table_name FROM information_schema.tables WHERE table_schema='_tenant_template' AND table_type='BASE TABLE')
|
||||
LOOP
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON public.%I', r.tgname, r.tab);
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.attach_notif_triggers(p_schema text)
|
||||
RETURNS int LANGUAGE plpgsql SECURITY DEFINER SET search_path TO 'public','pg_temp'
|
||||
AS $$
|
||||
DECLARE
|
||||
specs jsonb := jsonb_build_array(
|
||||
jsonb_build_object('tab','agenda_eventos','name','trg_notify_on_session_status','spec','AFTER UPDATE OF status ON __T__ FOR EACH ROW EXECUTE FUNCTION public.notify_on_session_status()'),
|
||||
jsonb_build_object('tab','agenda_eventos','name','trg_cancel_notifs_on_session_cancel','spec','AFTER UPDATE ON __T__ FOR EACH ROW WHEN (new.status IS DISTINCT FROM old.status) EXECUTE FUNCTION public.cancel_notifications_on_session_cancel()'),
|
||||
jsonb_build_object('tab','agenda_eventos','name','trg_agenda_status_notify','spec','AFTER UPDATE OF status ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fn_notify_agenda_status_change()'),
|
||||
jsonb_build_object('tab','conversation_messages','name','trg_fanout_inbound_to_notifications','spec','AFTER INSERT ON __T__ FOR EACH ROW EXECUTE FUNCTION public.fanout_inbound_message_to_notifications()'),
|
||||
jsonb_build_object('tab','notification_preferences','name','trg_cancel_notifs_on_opt_out','spec','AFTER UPDATE ON __T__ FOR EACH ROW EXECUTE FUNCTION public.cancel_notifications_on_opt_out()')
|
||||
);
|
||||
el jsonb; v_count int := 0; v_target text;
|
||||
BEGIN
|
||||
IF p_schema NOT LIKE 'tenant\_%' THEN RAISE EXCEPTION 'schema inválido %', p_schema; END IF;
|
||||
FOR el IN SELECT * FROM jsonb_array_elements(specs) LOOP
|
||||
IF NOT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema=p_schema AND table_name=(el->>'tab')) THEN CONTINUE; END IF;
|
||||
v_target := format('%I.%I', p_schema, el->>'tab');
|
||||
EXECUTE format('DROP TRIGGER IF EXISTS %I ON %s', el->>'name', v_target);
|
||||
EXECUTE 'CREATE TRIGGER ' || quote_ident(el->>'name') || ' ' || replace(el->>'spec','__T__',v_target);
|
||||
v_count := v_count + 1;
|
||||
END LOOP;
|
||||
RETURN v_count;
|
||||
END $$;
|
||||
|
||||
DO $$
|
||||
DECLARE r record; v int;
|
||||
BEGIN
|
||||
FOR r IN SELECT schema_name FROM public.tenant_schemas ORDER BY schema_name LOOP
|
||||
v := public.attach_notif_triggers(r.schema_name);
|
||||
RAISE NOTICE 'F6.2C %: % notif triggers', r.schema_name, v;
|
||||
END LOOP;
|
||||
END $$;
|
||||
|
||||
COMMIT;
|
||||
@@ -107,83 +107,106 @@ export const useNotificationStore = defineStore('notifications', {
|
||||
},
|
||||
|
||||
actions: {
|
||||
// schema-per-tenant: notificações vêm de DUAS fontes —
|
||||
// • tenant_<slug>.notifications (locais: agenda, conversas, pacientes)
|
||||
// • public.notifications_sistema (cross-tenant: avisos do SaaS, suporte)
|
||||
// Cada item carrega _origem ('tenant' | 'sistema') p/ rotear markRead/archive.
|
||||
async load(ownerId) {
|
||||
const { data, error } = await tenantDb().from('notifications').select('*').eq('owner_id', ownerId).eq('archived', false).order('created_at', { ascending: false }).limit(50);
|
||||
const [localRes, sistemaRes] = await Promise.all([
|
||||
tenantDb().from('notifications').select('*').eq('owner_id', ownerId).eq('archived', false).order('created_at', { ascending: false }).limit(50),
|
||||
supabase.from('notifications_sistema').select('*').eq('owner_id', ownerId).eq('archived', false).order('created_at', { ascending: false }).limit(50)
|
||||
]);
|
||||
|
||||
if (error) {
|
||||
console.error('[notificationStore] load error:', error.message);
|
||||
return;
|
||||
}
|
||||
if (localRes.error) console.error('[notificationStore] load tenant error:', localRes.error.message);
|
||||
if (sistemaRes.error) console.error('[notificationStore] load sistema error:', sistemaRes.error.message);
|
||||
|
||||
this.items = data || [];
|
||||
const local = (localRes.data || []).map((n) => ({ ...n, _origem: 'tenant' }));
|
||||
const sistema = (sistemaRes.data || []).map((n) => ({ ...n, _origem: 'sistema' }));
|
||||
|
||||
this.items = [...local, ...sistema]
|
||||
.sort((a, b) => new Date(b.created_at) - new Date(a.created_at))
|
||||
.slice(0, 50);
|
||||
},
|
||||
|
||||
subscribeRealtime(ownerId, onInsert = null) {
|
||||
if (this._channel) return;
|
||||
const tenantSchema = useTenantStore().activeTenantSchema;
|
||||
if (!tenantSchema) return;
|
||||
|
||||
const channel = supabase
|
||||
.channel(`notifications:${ownerId}`)
|
||||
.on(
|
||||
'postgres_changes',
|
||||
{
|
||||
event: 'INSERT',
|
||||
schema: tenantSchema,
|
||||
table: 'notifications',
|
||||
filter: `owner_id=eq.${ownerId}`
|
||||
},
|
||||
(payload) => {
|
||||
this.items.unshift(payload.new);
|
||||
if (typeof onInsert === 'function') {
|
||||
try { onInsert(payload.new); } catch { /* ignore */ }
|
||||
}
|
||||
}
|
||||
)
|
||||
.subscribe();
|
||||
const onIns = (origem) => (payload) => {
|
||||
const item = { ...payload.new, _origem: origem };
|
||||
this.items.unshift(item);
|
||||
this.items.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
|
||||
if (typeof onInsert === 'function') {
|
||||
try { onInsert(item); } catch { /* ignore */ }
|
||||
}
|
||||
};
|
||||
|
||||
const channel = supabase.channel(`notifications:${ownerId}`);
|
||||
if (tenantSchema) {
|
||||
channel.on('postgres_changes',
|
||||
{ event: 'INSERT', schema: tenantSchema, table: 'notifications', filter: `owner_id=eq.${ownerId}` },
|
||||
onIns('tenant'));
|
||||
}
|
||||
// canal cross-tenant (avisos do SaaS) — sempre escuta public
|
||||
channel.on('postgres_changes',
|
||||
{ event: 'INSERT', schema: 'public', table: 'notifications_sistema', filter: `owner_id=eq.${ownerId}` },
|
||||
onIns('sistema'));
|
||||
|
||||
channel.subscribe();
|
||||
this._channel = channel;
|
||||
},
|
||||
|
||||
// roteia pra fonte certa conforme _origem do item
|
||||
_sourceFor(item) {
|
||||
return item?._origem === 'sistema'
|
||||
? supabase.from('notifications_sistema')
|
||||
: tenantDb().from('notifications');
|
||||
},
|
||||
|
||||
async markRead(id) {
|
||||
const item = this.items.find((n) => n.id === id);
|
||||
if (!item) return;
|
||||
const now = new Date().toISOString();
|
||||
const { error } = await tenantDb().from('notifications').update({ read_at: now }).eq('id', id);
|
||||
const { error } = await this._sourceFor(item).update({ read_at: now }).eq('id', id);
|
||||
|
||||
if (error) {
|
||||
console.error('[notificationStore] markRead error:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
const item = this.items.find((n) => n.id === id);
|
||||
if (item) item.read_at = now;
|
||||
item.read_at = now;
|
||||
},
|
||||
|
||||
async markAllRead() {
|
||||
const unreadIds = this.items.filter((n) => !n.read_at && !n.archived).map((n) => n.id);
|
||||
|
||||
if (!unreadIds.length) return;
|
||||
|
||||
const unread = this.items.filter((n) => !n.read_at && !n.archived);
|
||||
if (!unread.length) return;
|
||||
const now = new Date().toISOString();
|
||||
const { error } = await tenantDb().from('notifications').update({ read_at: now }).in('id', unreadIds);
|
||||
|
||||
if (error) {
|
||||
console.error('[notificationStore] markAllRead error:', error.message);
|
||||
const tenantIds = unread.filter((n) => n._origem !== 'sistema').map((n) => n.id);
|
||||
const sistemaIds = unread.filter((n) => n._origem === 'sistema').map((n) => n.id);
|
||||
|
||||
const ops = [];
|
||||
if (tenantIds.length) ops.push(tenantDb().from('notifications').update({ read_at: now }).in('id', tenantIds));
|
||||
if (sistemaIds.length) ops.push(supabase.from('notifications_sistema').update({ read_at: now }).in('id', sistemaIds));
|
||||
const results = await Promise.all(ops);
|
||||
const err = results.find((r) => r.error);
|
||||
if (err) {
|
||||
console.error('[notificationStore] markAllRead error:', err.error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.items.forEach((n) => {
|
||||
if (unreadIds.includes(n.id)) n.read_at = now;
|
||||
if (!n.read_at && !n.archived) n.read_at = now;
|
||||
});
|
||||
},
|
||||
|
||||
async archive(id) {
|
||||
const { error } = await tenantDb().from('notifications').update({ archived: true }).eq('id', id);
|
||||
const item = this.items.find((n) => n.id === id);
|
||||
if (!item) return;
|
||||
const { error } = await this._sourceFor(item).update({ archived: true }).eq('id', id);
|
||||
|
||||
if (error) {
|
||||
console.error('[notificationStore] archive error:', error.message);
|
||||
return;
|
||||
}
|
||||
|
||||
this.items = this.items.filter((n) => n.id !== id);
|
||||
},
|
||||
|
||||
|
||||
Reference in New Issue
Block a user