improvement: v1 standalone treatment/case creation made possible.
This commit is contained in:
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user