improvement: notification feature polished. feature tabs following the same notification socket emmited data to be updated.

This commit is contained in:
2026-07-18 16:57:13 +03:30
parent 9f6ec193d2
commit 9941dca849
23 changed files with 506 additions and 63 deletions

View File

@@ -41,6 +41,146 @@ export class UserNotificationService {
private readonly realtime: RealtimeEmitter,
) {}
/**
* Lean lab-case snapshot for inbox cards. Used only inside `notify()` so
* inbox list/read never joins patients/orgs/tasks.
*/
private async labCaseInboxPayload(
labCaseId: string,
extra?: Record<string, unknown>,
): Promise<Record<string, unknown>> {
const labCase = await this.prisma.labCase.findUnique({
where: { id: labCaseId },
select: {
id: true,
destinationOrganizationId: true,
treatment: {
select: {
organization: { select: { name: true } },
patient: { select: { firstName: true, lastName: true } },
},
},
toothProsthesis: { select: { prosthesisTypeCode: true } },
tasks: { select: { prosthesisTypeCode: true }, take: 40 },
sends: {
orderBy: { sentAt: 'asc' },
take: 1,
select: { organization: { select: { name: true } } },
},
},
});
if (!labCase?.treatment) {
return { labCaseId, ...(extra ?? {}) };
}
let labName: string | null = labCase.sends[0]?.organization.name ?? null;
if (!labName && labCase.destinationOrganizationId) {
const dest = await this.prisma.organization.findUnique({
where: { id: labCase.destinationOrganizationId },
select: { name: true },
});
labName = dest?.name ?? null;
}
const prosthesisTypeCodes = [
...new Set(
[
...labCase.toothProsthesis.map((row) => row.prosthesisTypeCode),
...labCase.tasks.map((row) => row.prosthesisTypeCode),
].filter(Boolean),
),
];
const patient = labCase.treatment.patient;
const patientName = `${patient.firstName} ${patient.lastName}`.trim();
// Caller ids (taskId, commentId, …) come from `extra`; denormalized display
// fields must win so they are never overwritten by a thin emit payload.
return {
...(extra ?? {}),
labCaseId,
patientName,
clinicName: labCase.treatment.organization.name,
labName,
prosthesisTypeCodes,
};
}
/** Denormalize display fields into payload once at write time. */
private async enrichInboxPayload(
input: FanoutInput,
): Promise<Record<string, unknown> | null> {
const base =
input.payload && typeof input.payload === 'object' && !Array.isArray(input.payload)
? { ...(input.payload as Record<string, unknown>) }
: {};
try {
const labCaseId =
(typeof base.labCaseId === 'string' && base.labCaseId) ||
input.labCaseIdForProviderScope ||
null;
let enriched: Record<string, unknown> = { ...base };
if (labCaseId) {
enriched = await this.labCaseInboxPayload(labCaseId, enriched);
}
const taskId = typeof enriched.taskId === 'string' ? enriched.taskId : null;
if (taskId && typeof enriched.taskName !== 'string') {
const task = await this.prisma.labCaseTask.findUnique({
where: { id: taskId },
select: { stepLabel: true, prosthesisTypeCode: true },
});
if (task) {
enriched = {
...enriched,
taskName: task.stepLabel,
prosthesisTypeCode: task.prosthesisTypeCode,
};
}
}
const fromOrganizationId =
typeof enriched.fromOrganizationId === 'string' ? enriched.fromOrganizationId : null;
if (fromOrganizationId && typeof enriched.fromOrganizationName !== 'string') {
const org = await this.prisma.organization.findUnique({
where: { id: fromOrganizationId },
select: { name: true },
});
if (org) {
enriched = { ...enriched, fromOrganizationName: org.name };
}
}
const membershipId =
typeof enriched.membershipId === 'string' ? enriched.membershipId : null;
if (membershipId && typeof enriched.inviteeName !== 'string') {
const membership = await this.prisma.membership.findUnique({
where: { id: membershipId },
select: { user: { select: { name: true, email: true } } },
});
if (membership?.user) {
enriched = {
...enriched,
inviteeName: membership.user.name,
email:
typeof enriched.email === 'string' && enriched.email
? enriched.email
: membership.user.email,
};
}
}
return Object.keys(enriched).length > 0 ? enriched : null;
} catch {
// Never block inbox write if enrichment fails — store the thin payload.
return Object.keys(base).length > 0 ? base : null;
}
}
async notify(input: FanoutInput): Promise<void> {
const recipientIds = input.recipientUserIds?.length
? [...new Set(input.recipientUserIds.filter((id) => id && id !== input.actorUserId))]
@@ -48,13 +188,15 @@ export class UserNotificationService {
if (recipientIds.length === 0) return;
const payload = await this.enrichInboxPayload(input);
const rows = await this.prisma.userNotification.createManyAndReturn({
data: recipientIds.map((userId) => ({
userId,
organizationId: input.organizationId,
type: input.type,
actorUserId: input.actorUserId ?? null,
payload: (input.payload ?? Prisma.JsonNull) as Prisma.InputJsonValue,
payload: (payload ?? Prisma.JsonNull) as Prisma.InputJsonValue,
href: input.href,
})),
});