improvement: notification feature polished. feature tabs following the same notification socket emmited data to be updated.
This commit is contained in:
@@ -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,
|
||||
})),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user