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:
@@ -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