|
|
|
|
@@ -55,7 +55,7 @@ export class TasksService {
|
|
|
|
|
const limit = Math.min(Math.max(query.limit ?? 50, 1), 100);
|
|
|
|
|
const skip = (page - 1) * limit;
|
|
|
|
|
|
|
|
|
|
const where = this.buildListWhere(labOrganizationId, query);
|
|
|
|
|
const where = await this.buildListWhere(labOrganizationId, query);
|
|
|
|
|
|
|
|
|
|
const [items, total] = await Promise.all([
|
|
|
|
|
this.prisma.labCaseTask.findMany({
|
|
|
|
|
@@ -149,10 +149,60 @@ export class TasksService {
|
|
|
|
|
return { success: true, data: this.mapTaskListItem(updated, prosthesisLabels) };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildListWhere(
|
|
|
|
|
async listFilterOptions(
|
|
|
|
|
labOrganizationId: string,
|
|
|
|
|
actorUserId: string,
|
|
|
|
|
localeInput?: string | null,
|
|
|
|
|
) {
|
|
|
|
|
await this.assertCanReadTasks(actorUserId, labOrganizationId);
|
|
|
|
|
|
|
|
|
|
const rows = await this.prisma.labCase.findMany({
|
|
|
|
|
where: {
|
|
|
|
|
sentAt: { not: null },
|
|
|
|
|
sends: { some: { organizationId: labOrganizationId } },
|
|
|
|
|
},
|
|
|
|
|
select: {
|
|
|
|
|
treatment: {
|
|
|
|
|
select: {
|
|
|
|
|
organization: { select: { id: true, name: true } },
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const clinicsById = new Map<string, { id: string; name: string }>();
|
|
|
|
|
for (const row of rows) {
|
|
|
|
|
clinicsById.set(row.treatment.organization.id, row.treatment.organization);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const locale = normalizeCatalogLocale(localeInput);
|
|
|
|
|
const steps = await this.prisma.labWorkflowStep.findMany({
|
|
|
|
|
orderBy: { sortOrder: 'asc' },
|
|
|
|
|
select: { code: true },
|
|
|
|
|
});
|
|
|
|
|
const stepCodes = steps.map((s) => s.code);
|
|
|
|
|
const stepLabels = await this.catalogLabels.resolveLabels(
|
|
|
|
|
CatalogEntityKind.LAB_WORKFLOW_STEP,
|
|
|
|
|
stepCodes,
|
|
|
|
|
locale,
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
success: true,
|
|
|
|
|
data: {
|
|
|
|
|
clinics: [...clinicsById.values()].sort((a, b) => a.name.localeCompare(b.name)),
|
|
|
|
|
workflowSteps: steps.map((step) => ({
|
|
|
|
|
code: step.code,
|
|
|
|
|
label: stepLabels.get(step.code) ?? step.code,
|
|
|
|
|
})),
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async buildListWhere(
|
|
|
|
|
labOrganizationId: string,
|
|
|
|
|
query: ListLabTasksDto,
|
|
|
|
|
): Prisma.LabCaseTaskWhereInput {
|
|
|
|
|
): Promise<Prisma.LabCaseTaskWhereInput> {
|
|
|
|
|
const sentAtFilter: Prisma.DateTimeNullableFilter = { not: null };
|
|
|
|
|
|
|
|
|
|
if (query.sentFrom) {
|
|
|
|
|
@@ -181,18 +231,47 @@ export class TasksService {
|
|
|
|
|
status = LabTaskStatus.IN_PROGRESS;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
labCase: {
|
|
|
|
|
sentAt: sentAtFilter,
|
|
|
|
|
sends: { some: { organizationId: labOrganizationId } },
|
|
|
|
|
...(query.clinicOrganizationId
|
|
|
|
|
? { treatment: { organizationId: query.clinicOrganizationId } }
|
|
|
|
|
: {}),
|
|
|
|
|
...(query.q?.trim() ? { treatment: this.buildSearchWhere(query.q.trim()) } : {}),
|
|
|
|
|
...(query.important !== undefined ? { isImportant: query.important } : {}),
|
|
|
|
|
},
|
|
|
|
|
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()) } : {}),
|
|
|
|
|
...(query.important !== undefined ? { isImportant: query.important } : {}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const base: Prisma.LabCaseTaskWhereInput = {
|
|
|
|
|
labCase: labCaseScope,
|
|
|
|
|
...(status !== undefined ? { status } : {}),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const stepCompleted = query.stepCompleted?.trim();
|
|
|
|
|
if (!stepCompleted) {
|
|
|
|
|
return base;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const completedGroups = await this.prisma.labCaseTask.groupBy({
|
|
|
|
|
by: ['labCaseId', 'treatmentDetailId', 'prosthesisTypeCode'],
|
|
|
|
|
where: {
|
|
|
|
|
workflowStepCode: stepCompleted,
|
|
|
|
|
status: LabTaskStatus.COMPLETED,
|
|
|
|
|
labCase: labCaseScope,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (completedGroups.length === 0) {
|
|
|
|
|
return { id: { in: [] } };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...base,
|
|
|
|
|
OR: completedGroups.map((group) => ({
|
|
|
|
|
labCaseId: group.labCaseId,
|
|
|
|
|
treatmentDetailId: group.treatmentDetailId,
|
|
|
|
|
prosthesisTypeCode: group.prosthesisTypeCode,
|
|
|
|
|
})),
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private buildSearchWhere(q: string): Prisma.TreatmentWhereInput {
|
|
|
|
|
@@ -214,40 +293,50 @@ export class TasksService {
|
|
|
|
|
|
|
|
|
|
private buildOrderBy(query: ListLabTasksDto): Prisma.LabCaseTaskOrderByWithRelationInput[] {
|
|
|
|
|
const dir = query.sortDir ?? 'desc';
|
|
|
|
|
const stepTiebreakers: Prisma.LabCaseTaskOrderByWithRelationInput[] = [
|
|
|
|
|
{ stepOrder: 'asc' },
|
|
|
|
|
{ id: 'asc' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
switch (query.sortBy) {
|
|
|
|
|
case 'status':
|
|
|
|
|
return [{ status: dir }, { createdAt: 'desc' }, { id: 'asc' }];
|
|
|
|
|
return [{ status: dir }, { createdAt: 'desc' }, ...stepTiebreakers];
|
|
|
|
|
case 'clinic':
|
|
|
|
|
return [
|
|
|
|
|
{ labCase: { treatment: { organization: { name: dir } } } },
|
|
|
|
|
{ createdAt: 'desc' },
|
|
|
|
|
{ id: 'asc' },
|
|
|
|
|
...stepTiebreakers,
|
|
|
|
|
];
|
|
|
|
|
case 'patient':
|
|
|
|
|
return [
|
|
|
|
|
{ labCase: { treatment: { patient: { lastName: dir } } } },
|
|
|
|
|
{ labCase: { treatment: { patient: { firstName: dir } } } },
|
|
|
|
|
{ id: 'asc' },
|
|
|
|
|
...stepTiebreakers,
|
|
|
|
|
];
|
|
|
|
|
case 'important':
|
|
|
|
|
return [{ labCase: { isImportant: dir } }, { createdAt: 'desc' }, { id: 'asc' }];
|
|
|
|
|
return [{ labCase: { isImportant: dir } }, { createdAt: 'desc' }, ...stepTiebreakers];
|
|
|
|
|
case 'prosthesis':
|
|
|
|
|
return [{ prosthesisTypeCode: dir }, { createdAt: 'desc' }, { id: 'asc' }];
|
|
|
|
|
return [
|
|
|
|
|
{ prosthesisTypeCode: dir },
|
|
|
|
|
{ createdAt: 'desc' },
|
|
|
|
|
...stepTiebreakers,
|
|
|
|
|
];
|
|
|
|
|
case 'taskType':
|
|
|
|
|
return [
|
|
|
|
|
{ workflowStepCode: dir },
|
|
|
|
|
{ stepOrder: 'asc' },
|
|
|
|
|
{ createdAt: 'desc' },
|
|
|
|
|
{ id: 'asc' },
|
|
|
|
|
...stepTiebreakers,
|
|
|
|
|
];
|
|
|
|
|
case 'date':
|
|
|
|
|
default:
|
|
|
|
|
// date / caseId / taskId / stepId — newest first by default.
|
|
|
|
|
return [
|
|
|
|
|
{ labCase: { sentAt: dir } },
|
|
|
|
|
{ labCaseId: dir },
|
|
|
|
|
{ id: dir },
|
|
|
|
|
{ stepOrder: dir },
|
|
|
|
|
{ labCaseId: 'asc' },
|
|
|
|
|
{ treatmentDetailId: 'asc' },
|
|
|
|
|
{ prosthesisTypeCode: 'asc' },
|
|
|
|
|
{ stepOrder: 'asc' },
|
|
|
|
|
{ id: 'asc' },
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -275,6 +364,7 @@ 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,
|
|
|
|
|
|