import { db } from '@/lib/db' import { visibleCaseWhere } from '@/lib/access/caseFilter' import { visibleMessageWhere } from '@/lib/access/chatFilter' import { visiblePersonnelWhere } from '@/lib/access/personnelFilter' import { listWatched } from '@/lib/cases/watch' import { listRecent } from '@/lib/recent/service' import { WORKING_ROLES } from '@/lib/cases/terms' import type { EffectivePermissions } from '@/lib/auth/permissions' import type { Prisma } from '@afis/db' /** * Was auf der Übersicht steht. * * Fünf Fragen, die man morgens hat: woran war ich dran, woran arbeite ich, was * ist fällig, wo hat jemand geschrieben, was hat sich getan. Jede läuft durch * `visibleCaseWhere` - die Übersicht ist der Ort, an dem eine unachtsame * Abfrage am ehesten eine Akte verrät, die man nicht sehen darf. */ const AKTE = { id: true, caseNumber: true, title: true, status: true, classification: true, updatedAt: true, dueAt: true, } satisfies Prisma.CaseSelect /** Fristen innerhalb dieser Anzahl Tage gelten als „steht an". */ const FRIST_TAGE = 14 export async function loadDashboard(p: EffectivePermissions) { const sichtbar = visibleCaseWhere(p) const jetzt = new Date() const frist = new Date(jetzt.getTime() + FRIST_TAGE * 86_400_000) const [meine, fristen, zuletzt, beobachtet, ungelesen, geoeffnet] = await Promise.all([ db.case.findMany({ where: { AND: [ sichtbar, { participants: { some: { userId: p.userId, role: { in: [...WORKING_ROLES] } } } }, { status: { in: ['DRAFT', 'OPEN', 'REVIEW'] } }, ], }, orderBy: { updatedAt: 'desc' }, take: 10, select: AKTE, }), db.case.findMany({ where: { AND: [ sichtbar, { dueAt: { not: null, lte: frist } }, { status: { notIn: ['CLOSED', 'ARCHIVED', 'SEALED'] } }, ], }, orderBy: { dueAt: 'asc' }, take: 10, select: AKTE, }), db.case.findMany({ where: sichtbar, orderBy: { updatedAt: 'desc' }, take: 8, select: AKTE, }), listWatched(p, 10), ungeleseneChats(p), listRecent(p, 8), ]) return { meine, fristen, zuletzt, beobachtet, ungelesen, geoeffnet } } export type UnreadChat = { scopeType: 'case' | 'personnel' scopeId: string label: string href: string count: number } /** * Wo seit dem letzten Besuch geschrieben wurde. * * Gezählt wird über `ReadMarker`. Nur an Orten, die den Nutzer angehen: Akten, * an denen er beteiligt ist oder die er beobachtet, und Personalakten, denen * er zugeordnet ist. Eine Zählung über alles Sichtbare wäre auf der Übersicht * nur Lärm. Eigene Nachrichten zählen nicht mit. */ async function ungeleseneChats(p: EffectivePermissions): Promise { const marken = await db.readMarker.findMany({ where: { userId: p.userId }, select: { scopeType: true, scopeId: true, lastReadAt: true }, }) const markeVon = new Map(marken.map((m) => [`${m.scopeType}:${m.scopeId}`, m.lastReadAt])) const darfPersonal = p.isAdmin || p.permissions.has('personnel.read') const [meineAkten, personalakten] = await Promise.all([ db.case.findMany({ where: { AND: [ visibleCaseWhere(p), { OR: [ { participants: { some: { userId: p.userId, role: { in: [...WORKING_ROLES] } } } }, { watches: { some: { userId: p.userId } } }, ], }, ], }, select: { id: true, caseNumber: true }, take: 50, }), darfPersonal ? db.personnelFile.findMany({ where: { AND: [ visiblePersonnelWhere(p), { assignments: { some: { userId: p.userId } } }, ], }, select: { id: true, displayName: true }, take: 50, }) : Promise.resolve([]), ]) const orte = [ ...meineAkten.map((akte) => ({ scopeType: 'case' as const, scopeId: akte.id, label: akte.caseNumber, href: `/faelle/${akte.caseNumber}`, wo: { caseId: akte.id } as Prisma.ChatMessageWhereInput, })), ...personalakten.map((akte) => ({ scopeType: 'personnel' as const, scopeId: akte.id, label: akte.displayName, href: `/personalakten/${akte.id}`, wo: { personnelFileId: akte.id } as Prisma.ChatMessageWhereInput, })), ] if (orte.length === 0) return [] // **Eine** Abfrage statt einer je Ort. Vorher war das ein `count` pro Akte // und pro Personalakte - bei hundert Orten hundert Hin- und Rückwege zur // Datenbank, jeder für sich schnell und in der Summe die langsamste Stelle // der Übersicht. Die Lesemarke steckt als Bedingung im ODER-Zweig des // jeweiligen Ortes, gezählt wird über `groupBy`. const zweige = orte.map((ort) => { const seit = markeVon.get(`${ort.scopeType}:${ort.scopeId}`) return seit ? { AND: [ort.wo, { createdAt: { gt: seit } }] } : ort.wo }) const gruppen = await db.chatMessage.groupBy({ by: ['caseId', 'personnelFileId'], where: { AND: [ visibleMessageWhere(p), { withdrawnAt: null }, { authorId: { not: p.userId } }, { OR: zweige }, ], }, _count: { _all: true }, }) const anzahlVon = new Map( gruppen.map((gruppe) => [gruppe.caseId ?? gruppe.personnelFileId ?? '', gruppe._count._all]), ) return orte .map((ort) => ({ ort, count: anzahlVon.get(ort.scopeId) ?? 0 })) .filter((eintrag) => eintrag.count > 0) .sort((a, b) => b.count - a.count) .slice(0, 10) .map(({ ort, count }) => ({ scopeType: ort.scopeType, scopeId: ort.scopeId, label: ort.label, href: ort.href, count, })) }