improvement: v1 standalone treatment/case creation made possible.

This commit is contained in:
2026-08-19 15:05:58 +03:30
parent e5c39c6b7e
commit 8bfa8c88fe
39 changed files with 3296 additions and 387 deletions

View File

@@ -3,7 +3,7 @@ import {
Injectable,
} from '@nestjs/common';
import { AppException, ErrorCode } from '../../common/errors';
import { CatalogEntityKind, LabCaseActivityType, LabTaskStatus, Prisma, UserNotificationType } from '@prisma/client';
import { CatalogEntityKind, LabCaseActivityType, LabCaseOrigin, LabTaskStatus, Prisma, UserNotificationType } from '@prisma/client';
import { PrismaService } from '../../../prisma/prisma.service';
import { normalizeMobile } from '../../common/phone';
import {
@@ -23,6 +23,7 @@ const taskListInclude = {
labCase: {
include: {
tasks: { select: { status: true } },
partnerClinic: { select: { id: true, name: true } },
treatment: {
include: {
organization: { select: { id: true, name: true } },
@@ -110,8 +111,7 @@ export class TasksService {
const labCase = await this.prisma.labCase.findFirst({
where: {
id: labCaseId,
sentAt: { not: null },
sends: { some: { organizationId } },
...this.visibleStartedToLabWhere(organizationId),
},
select: { id: true },
});
@@ -129,6 +129,7 @@ export class TasksService {
include: taskListInclude,
orderBy: [
{ treatmentDetailId: 'asc' },
{ sourceKey: 'asc' },
{ prosthesisTypeCode: 'asc' },
{ stepOrder: 'asc' },
{ id: 'asc' },
@@ -164,17 +165,15 @@ export class TasksService {
const target = await this.prisma.labCaseTask.findFirst({
where: {
id: query.taskId,
labCase: {
sentAt: { not: null },
sends: { some: { organizationId: labOrganizationId } },
},
labCase: this.visibleStartedToLabWhere(labOrganizationId),
},
include: {
labCase: { select: { sentAt: true } },
labCase: { select: { sentAt: true, startedAt: true } },
},
});
if (!target?.labCase.sentAt) {
const effectiveAt = target?.labCase.sentAt ?? target?.labCase.startedAt;
if (!target || !effectiveAt) {
throw new AppException(ErrorCode.CASE_TASK_NOT_FOUND, HttpStatus.NOT_FOUND);
}
@@ -194,9 +193,9 @@ export class TasksService {
where,
listQuery,
{
sentAt: target.labCase.sentAt,
sentAt: effectiveAt,
labCaseId: target.labCaseId,
treatmentDetailId: target.treatmentDetailId,
sourceKey: target.sourceKey,
prosthesisTypeCode: target.prosthesisTypeCode,
stepOrder: target.stepOrder,
id: target.id,
@@ -225,10 +224,7 @@ export class TasksService {
const task = await this.prisma.labCaseTask.findFirst({
where: {
id: taskId,
labCase: {
sentAt: { not: null },
sends: { some: { organizationId: labOrganizationId } },
},
labCase: this.visibleStartedToLabWhere(labOrganizationId),
},
include: taskListInclude,
});
@@ -366,11 +362,9 @@ export class TasksService {
await this.assertCanReadTasks(actorUserId, labOrganizationId);
const rows = await this.prisma.labCase.findMany({
where: {
sentAt: { not: null },
sends: { some: { organizationId: labOrganizationId } },
},
where: this.visibleStartedToLabWhere(labOrganizationId),
select: {
partnerClinic: { select: { id: true, name: true } },
treatment: {
select: {
organization: { select: { id: true, name: true } },
@@ -381,7 +375,12 @@ export class TasksService {
const clinicsById = new Map<string, { id: string; name: string }>();
for (const row of rows) {
clinicsById.set(row.treatment.organization.id, row.treatment.organization);
if (row.treatment?.organization) {
clinicsById.set(row.treatment.organization.id, row.treatment.organization);
}
if (row.partnerClinic) {
clinicsById.set(row.partnerClinic.id, row.partnerClinic);
}
}
const locale = normalizeCatalogLocale(localeInput);
@@ -442,12 +441,33 @@ export class TasksService {
}
const labCaseScope: Prisma.LabCaseWhereInput = {
sentAt: sentAtFilter,
sends: { some: { organizationId: labOrganizationId } },
...(query.clinicOrganizationId
? { treatment: { organizationId: query.clinicOrganizationId } }
: {}),
...(query.q?.trim() ? { treatment: this.buildSearchWhere(query.q.trim()) } : {}),
AND: [
this.visibleStartedToLabWhere(labOrganizationId),
...(query.sentFrom || query.sentTo
? [
{
OR: [
{ sentAt: sentAtFilter },
{
origin: LabCaseOrigin.LAB_INTERNAL,
startedAt: sentAtFilter,
},
],
} satisfies Prisma.LabCaseWhereInput,
]
: []),
...(query.clinicOrganizationId
? [
{
OR: [
{ treatment: { organizationId: query.clinicOrganizationId } },
{ partnerClinicOrganizationId: query.clinicOrganizationId },
],
} satisfies Prisma.LabCaseWhereInput,
]
: []),
...(query.q?.trim() ? [this.buildTaskSearchWhere(query.q.trim())] : []),
],
};
const base: Prisma.LabCaseTaskWhereInput = {
@@ -475,7 +495,7 @@ export class TasksService {
}
const completedGroups = await this.prisma.labCaseTask.groupBy({
by: ['labCaseId', 'treatmentDetailId', 'prosthesisTypeCode'],
by: ['labCaseId', 'sourceKey', 'prosthesisTypeCode'],
where: {
workflowStepCode: stepCompleted,
status: LabTaskStatus.COMPLETED,
@@ -493,7 +513,7 @@ export class TasksService {
{
OR: completedGroups.map((group) => ({
labCaseId: group.labCaseId,
treatmentDetailId: group.treatmentDetailId,
sourceKey: group.sourceKey,
prosthesisTypeCode: group.prosthesisTypeCode,
})),
},
@@ -529,7 +549,7 @@ export class TasksService {
target: {
sentAt: Date;
labCaseId: string;
treatmentDetailId: string;
sourceKey: string;
prosthesisTypeCode: string;
stepOrder: number;
id: string;
@@ -543,7 +563,11 @@ export class TasksService {
}
const sentAt = target.sentAt;
const sameSentAt = { labCase: { sentAt } };
const sameSentAt = {
labCase: {
OR: [{ sentAt }, { AND: [{ sentAt: null }, { startedAt: sentAt }] }],
},
};
const tupleBefore: Prisma.LabCaseTaskWhereInput[] = [
{
AND: [sameSentAt, { labCaseId: { lt: target.labCaseId } }],
@@ -552,14 +576,14 @@ export class TasksService {
AND: [
sameSentAt,
{ labCaseId: target.labCaseId },
{ treatmentDetailId: { lt: target.treatmentDetailId } },
{ sourceKey: { lt: target.sourceKey } },
],
},
{
AND: [
sameSentAt,
{ labCaseId: target.labCaseId },
{ treatmentDetailId: target.treatmentDetailId },
{ sourceKey: target.sourceKey },
{ prosthesisTypeCode: { lt: target.prosthesisTypeCode } },
],
},
@@ -567,7 +591,7 @@ export class TasksService {
AND: [
sameSentAt,
{ labCaseId: target.labCaseId },
{ treatmentDetailId: target.treatmentDetailId },
{ sourceKey: target.sourceKey },
{ prosthesisTypeCode: target.prosthesisTypeCode },
{ stepOrder: { lt: target.stepOrder } },
],
@@ -576,7 +600,7 @@ export class TasksService {
AND: [
sameSentAt,
{ labCaseId: target.labCaseId },
{ treatmentDetailId: target.treatmentDetailId },
{ sourceKey: target.sourceKey },
{ prosthesisTypeCode: target.prosthesisTypeCode },
{ stepOrder: target.stepOrder },
{ id: { lt: target.id } },
@@ -586,8 +610,16 @@ export class TasksService {
const sentAtBefore: Prisma.LabCaseTaskWhereInput =
dir === 'desc'
? { labCase: { sentAt: { gt: sentAt } } }
: { labCase: { sentAt: { lt: sentAt } } };
? {
labCase: {
OR: [{ sentAt: { gt: sentAt } }, { startedAt: { gt: sentAt } }],
},
}
: {
labCase: {
OR: [{ sentAt: { lt: sentAt } }, { startedAt: { lt: sentAt } }],
},
};
return this.prisma.labCaseTask.count({
where: {
@@ -596,21 +628,49 @@ export class TasksService {
});
}
private buildSearchWhere(q: string): Prisma.TreatmentWhereInput {
const orConditions: Prisma.PatientWhereInput[] = [
{ firstName: { contains: q, mode: 'insensitive' } },
{ lastName: { contains: q, mode: 'insensitive' } },
private visibleStartedToLabWhere(labOrganizationId: string): Prisma.LabCaseWhereInput {
return {
OR: [
{
sentAt: { not: null },
sends: { some: { organizationId: labOrganizationId } },
},
{
origin: LabCaseOrigin.LAB_INTERNAL,
destinationOrganizationId: labOrganizationId,
startedAt: { not: null },
},
],
};
}
private buildTaskSearchWhere(q: string): Prisma.LabCaseWhereInput {
const orConditions: Prisma.LabCaseWhereInput[] = [
{
treatment: {
patient: {
OR: [
{ firstName: { contains: q, mode: 'insensitive' } },
{ lastName: { contains: q, mode: 'insensitive' } },
],
},
},
},
{
treatment: {
organization: { name: { contains: q, mode: 'insensitive' } },
},
},
{ referringClinicName: { contains: q, mode: 'insensitive' } },
{ referringDentistName: { contains: q, mode: 'insensitive' } },
{ patientDisplayName: { contains: q, mode: 'insensitive' } },
{ patientDisplayMobile: { contains: q, mode: 'insensitive' } },
];
const normalized = normalizeMobile(q);
if (normalized) {
orConditions.push({ mobile: normalized });
orConditions.push({ treatment: { patient: { mobile: normalized } } });
}
return {
OR: [
{ patient: { OR: orConditions } },
{ organization: { name: { contains: q, mode: 'insensitive' } } },
],
};
return { OR: orConditions };
}
private buildOrderBy(query: ListLabTasksDto): Prisma.LabCaseTaskOrderByWithRelationInput[] {
@@ -628,6 +688,7 @@ export class TasksService {
break;
case 'clinic':
orderBy = [
{ labCase: { referringClinicName: dir } },
{ labCase: { treatment: { organization: { name: dir } } } },
{ createdAt: 'desc' },
...stepTiebreakers,
@@ -635,6 +696,7 @@ export class TasksService {
break;
case 'patient':
orderBy = [
{ labCase: { patientDisplayName: dir } },
{ labCase: { treatment: { patient: { lastName: dir } } } },
{ labCase: { treatment: { patient: { firstName: dir } } } },
...stepTiebreakers,
@@ -661,9 +723,10 @@ export class TasksService {
case 'dueDate':
orderBy = [
{ labCase: { dueDate: dir } },
{ labCase: { startedAt: 'desc' } },
{ labCase: { sentAt: 'desc' } },
{ labCaseId: 'asc' },
{ treatmentDetailId: 'asc' },
{ sourceKey: 'asc' },
{ prosthesisTypeCode: 'asc' },
{ stepOrder: 'asc' },
{ id: 'asc' },
@@ -672,9 +735,10 @@ export class TasksService {
case 'date':
default:
orderBy = [
{ labCase: { startedAt: dir } },
{ labCase: { sentAt: dir } },
{ labCaseId: 'asc' },
{ treatmentDetailId: 'asc' },
{ sourceKey: 'asc' },
{ prosthesisTypeCode: 'asc' },
{ stepOrder: 'asc' },
{ id: 'asc' },
@@ -693,10 +757,24 @@ export class TasksService {
task: Prisma.LabCaseTaskGetPayload<{ include: typeof taskListInclude }>,
prosthesisLabels: Map<string, string>,
) {
const clinic =
task.labCase.treatment?.organization ??
task.labCase.partnerClinic ?? {
id: '',
name: task.labCase.referringClinicName?.trim() || '',
};
const displayName = task.labCase.patientDisplayName?.trim() || '';
const [firstName, ...rest] = displayName.split(/\s+/);
const patient = task.labCase.treatment?.patient ?? {
id: '',
firstName: firstName || displayName || clinic.name || '—',
lastName: rest.join(' '),
};
return {
id: task.id,
labCaseId: task.labCaseId,
treatmentDetailId: task.treatmentDetailId,
treatmentDetailId: task.treatmentDetailId ?? task.sourceKey,
teeth: normalizeTaskTeeth(task.teeth),
treatmentType: task.treatmentType,
prosthesisTypeCode: task.prosthesisTypeCode,
@@ -718,13 +796,9 @@ export class TasksService {
? { id: task.lastStatusChangedBy.id, name: task.lastStatusChangedBy.name }
: null,
createdAt: task.createdAt.toISOString(),
caseSentAt: task.labCase.sentAt?.toISOString() ?? null,
clinic: task.labCase.treatment.organization,
patient: {
id: task.labCase.treatment.patient.id,
firstName: task.labCase.treatment.patient.firstName,
lastName: task.labCase.treatment.patient.lastName,
},
caseSentAt: (task.labCase.sentAt ?? task.labCase.startedAt)?.toISOString() ?? null,
clinic,
patient,
};
}