improvement: tasks and cases feature updated based on the new prosthesis types and their steps. the whole assignment proccess removed from the flow.
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
|
||||
import { normalizeTeeth } from '../treatments/treatment.utils';
|
||||
import { ListLabCasesDto, UpdateLabCaseTaskDto } from './dto/cases.dto';
|
||||
import { normalizeTaskTeeth } from './lab-case-task.util';
|
||||
|
||||
const labCaseListInclude = {
|
||||
treatment: {
|
||||
@@ -41,16 +42,29 @@ const labCaseListInclude = {
|
||||
},
|
||||
tasks: {
|
||||
orderBy: [
|
||||
{ tooth: 'asc' as const },
|
||||
{ treatmentType: 'asc' as const },
|
||||
{ treatmentDetailId: 'asc' as const },
|
||||
{ prosthesisTypeCode: 'asc' as const },
|
||||
{ stepOrder: 'asc' as const },
|
||||
],
|
||||
include: {
|
||||
assignee: { select: { id: true, name: true, email: true } },
|
||||
lastStatusChangedBy: { select: { id: true, name: true } },
|
||||
statusEvents: {
|
||||
orderBy: { changedAt: 'asc' as const },
|
||||
include: { changedBy: { select: { id: true, name: true } } },
|
||||
},
|
||||
},
|
||||
},
|
||||
} satisfies Prisma.LabCaseInclude;
|
||||
|
||||
type LabCaseTaskWithRelations = Prisma.LabCaseTaskGetPayload<{
|
||||
include: {
|
||||
lastStatusChangedBy: { select: { id: true; name: true } };
|
||||
statusEvents: {
|
||||
include: { changedBy: { select: { id: true; name: true } } };
|
||||
};
|
||||
};
|
||||
}>;
|
||||
|
||||
@Injectable()
|
||||
export class CasesService {
|
||||
constructor(
|
||||
@@ -294,23 +308,15 @@ export class CasesService {
|
||||
throw new NotFoundException('Task not found');
|
||||
}
|
||||
|
||||
if (dto.assigneeUserId !== undefined && dto.assigneeUserId !== null) {
|
||||
await this.ensureAssignableMember(dto.assigneeUserId, labOrganizationId);
|
||||
}
|
||||
|
||||
const updated = await this.prisma.labCaseTask.update({
|
||||
where: { id: taskId },
|
||||
data: {
|
||||
...(dto.assigneeUserId !== undefined
|
||||
? {
|
||||
assigneeUserId: dto.assigneeUserId,
|
||||
assignedAt: dto.assigneeUserId === null ? null : new Date(),
|
||||
}
|
||||
: {}),
|
||||
...(dto.priority !== undefined ? { priority: dto.priority } : {}),
|
||||
},
|
||||
data: { isImportant: dto.isImportant },
|
||||
include: {
|
||||
assignee: { select: { id: true, name: true, email: true } },
|
||||
lastStatusChangedBy: { select: { id: true, name: true } },
|
||||
statusEvents: {
|
||||
orderBy: { changedAt: 'asc' },
|
||||
include: { changedBy: { select: { id: true, name: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -324,35 +330,6 @@ export class CasesService {
|
||||
return { success: true, data: this.mapTask(updated, prosthesisLabels) };
|
||||
}
|
||||
|
||||
async listAssignableMembers(labOrganizationId: string, actorUserId: string) {
|
||||
await this.assertCanReadCases(actorUserId, labOrganizationId);
|
||||
|
||||
const memberships = await this.prisma.membership.findMany({
|
||||
where: { organizationId: labOrganizationId, isActive: true },
|
||||
include: {
|
||||
user: { select: { id: true, name: true, email: true } },
|
||||
permissions: { include: { permission: true } },
|
||||
},
|
||||
orderBy: [{ isOwner: 'desc' }, { createdAt: 'asc' }],
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: memberships
|
||||
.filter((m) => {
|
||||
if (m.isOwner) return true;
|
||||
const names = m.permissions.map((p) => p.permission.name);
|
||||
return names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT');
|
||||
})
|
||||
.map((m) => ({
|
||||
userId: m.user.id,
|
||||
name: m.user.name,
|
||||
email: m.user.email,
|
||||
isOwner: m.isOwner,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private buildListWhere(
|
||||
labOrganizationId: string,
|
||||
query: ListLabCasesDto,
|
||||
@@ -465,7 +442,7 @@ export class CasesService {
|
||||
prosthesisCodes,
|
||||
locale,
|
||||
);
|
||||
const tasksByTooth = this.groupTasksByTooth(lc.tasks, prosthesisLabels);
|
||||
const tasksByTooth = this.groupTasks(lc.tasks, prosthesisLabels);
|
||||
|
||||
return {
|
||||
id: lc.id,
|
||||
@@ -495,27 +472,15 @@ export class CasesService {
|
||||
};
|
||||
}
|
||||
|
||||
private groupTasksByTooth(
|
||||
tasks: Array<{
|
||||
id: string;
|
||||
tooth: string;
|
||||
treatmentType: string;
|
||||
prosthesisTypeCode: string;
|
||||
stepOrder: number;
|
||||
stepLabel: string;
|
||||
status: LabTaskStatus;
|
||||
priority: number;
|
||||
assigneeUserId: string | null;
|
||||
assignedAt: Date | null;
|
||||
createdAt: Date;
|
||||
assignee: { id: string; name: string; email: string } | null;
|
||||
}>,
|
||||
private groupTasks(
|
||||
tasks: LabCaseTaskWithRelations[],
|
||||
prosthesisLabels: Map<string, string>,
|
||||
) {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
tooth: string;
|
||||
treatmentDetailId: string;
|
||||
teeth: string[];
|
||||
treatmentType: string;
|
||||
prosthesisTypeCode: string;
|
||||
prosthesisTypeLabel: string;
|
||||
@@ -524,9 +489,10 @@ export class CasesService {
|
||||
>();
|
||||
|
||||
for (const task of tasks) {
|
||||
const key = `${task.tooth}:${task.treatmentType}:${task.prosthesisTypeCode}`;
|
||||
const key = `${task.treatmentDetailId}:${task.prosthesisTypeCode}`;
|
||||
const entry = groups.get(key) ?? {
|
||||
tooth: task.tooth,
|
||||
treatmentDetailId: task.treatmentDetailId,
|
||||
teeth: normalizeTaskTeeth(task.teeth),
|
||||
treatmentType: task.treatmentType,
|
||||
prosthesisTypeCode: task.prosthesisTypeCode,
|
||||
prosthesisTypeLabel:
|
||||
@@ -541,26 +507,13 @@ export class CasesService {
|
||||
}
|
||||
|
||||
private mapTask(
|
||||
task: {
|
||||
id: string;
|
||||
tooth: string;
|
||||
treatmentType: string;
|
||||
prosthesisTypeCode: string;
|
||||
workflowStepCode?: string;
|
||||
stepOrder: number;
|
||||
stepLabel: string;
|
||||
status: LabTaskStatus;
|
||||
priority: number;
|
||||
assigneeUserId: string | null;
|
||||
assignedAt: Date | null;
|
||||
createdAt: Date;
|
||||
assignee: { id: string; name: string; email: string } | null;
|
||||
},
|
||||
task: LabCaseTaskWithRelations,
|
||||
prosthesisLabels: Map<string, string>,
|
||||
) {
|
||||
return {
|
||||
id: task.id,
|
||||
tooth: task.tooth,
|
||||
treatmentDetailId: task.treatmentDetailId,
|
||||
teeth: normalizeTaskTeeth(task.teeth),
|
||||
treatmentType: task.treatmentType,
|
||||
prosthesisTypeCode: task.prosthesisTypeCode,
|
||||
prosthesisTypeLabel:
|
||||
@@ -569,32 +522,24 @@ export class CasesService {
|
||||
stepOrder: task.stepOrder,
|
||||
stepLabel: task.stepLabel,
|
||||
status: task.status,
|
||||
priority: task.priority,
|
||||
assignedAt: task.assignedAt?.toISOString() ?? null,
|
||||
isImportant: task.isImportant,
|
||||
createdAt: task.createdAt.toISOString(),
|
||||
assigneeUserId: task.assigneeUserId,
|
||||
assignee: task.assignee
|
||||
? { id: task.assignee.id, name: task.assignee.name, email: task.assignee.email }
|
||||
lastStatusChangedAt: task.lastStatusChangedAt?.toISOString() ?? null,
|
||||
lastStatusChangedBy: task.lastStatusChangedBy
|
||||
? { id: task.lastStatusChangedBy.id, name: task.lastStatusChangedBy.name }
|
||||
: null,
|
||||
timeline: task.statusEvents.map((event) => ({
|
||||
id: event.id,
|
||||
fromStatus: event.fromStatus,
|
||||
toStatus: event.toStatus,
|
||||
changedAt: event.changedAt.toISOString(),
|
||||
changedBy: event.changedBy
|
||||
? { id: event.changedBy.id, name: event.changedBy.name }
|
||||
: null,
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
private async ensureAssignableMember(userId: string, labOrganizationId: string) {
|
||||
const membership = await this.prisma.membership.findFirst({
|
||||
where: { userId, organizationId: labOrganizationId, isActive: true },
|
||||
include: { permissions: { include: { permission: true } } },
|
||||
});
|
||||
if (!membership) {
|
||||
throw new BadRequestException('Assignee must be an active member of this lab');
|
||||
}
|
||||
if (membership.isOwner) return;
|
||||
const names = membership.permissions.map((p) => p.permission.name);
|
||||
if (names.includes('TAB_TASKS_READ') || names.includes('TAB_TASKS_EDIT')) {
|
||||
return;
|
||||
}
|
||||
throw new BadRequestException('Assignee must have access to the Tasks tab');
|
||||
}
|
||||
|
||||
private async assertCanReadCases(userId: string, organizationId: string) {
|
||||
const m = await this.getMembership(userId, organizationId);
|
||||
if (!m) {
|
||||
|
||||
Reference in New Issue
Block a user