Prüfung / Lint, Typen, Tests (push) Canceled after 0s
Prüfung / Images bauen (pdf) (push) Canceled after 0s
Prüfung / Images bauen (web) (push) Canceled after 0s
Prüfung / Images bauen (worker) (push) Canceled after 0s
Images / bauen (pdf, docker/pdf.Dockerfile) (push) Canceled after 0s
Images / bauen (web, docker/web.Dockerfile) (push) Canceled after 0s
Images / bauen (worker, docker/worker.Dockerfile) (push) Canceled after 0s
375 Vorkommen in 210 Dateien. Betrifft Kommentare, Oberflaechentexte und die README; an keiner Stelle Bedeutung, nur Zeichensetzung.
190 lines
5.7 KiB
TypeScript
190 lines
5.7 KiB
TypeScript
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<UnreadChat[]> {
|
|
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,
|
|
}))
|
|
}
|