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:
2026-07-07 15:31:09 +03:30
parent ed7e7b1d8f
commit cb63ced4e3
35 changed files with 1819 additions and 454 deletions

View File

@@ -0,0 +1,83 @@
-- Lab workflow refactor: remove task assignment/priority, group tasks by prosthesis,
-- add importance flag, status timeline, and per-case comments.
-- Local dev data only: existing tasks are truncated and regenerated on next dispatch/send.
-- 1. Clear existing task data (task shape changes: tooth -> teeth[]).
TRUNCATE TABLE "lab_case_tasks" CASCADE;
-- 2. Drop assignment / priority machinery.
ALTER TABLE "lab_case_tasks" DROP CONSTRAINT IF EXISTS "lab_case_tasks_assigneeUserId_fkey";
DROP INDEX IF EXISTS "lab_case_tasks_assigneeUserId_priority_createdAt_idx";
DROP INDEX IF EXISTS "lab_case_tasks_assignedAt_labCaseId_priority_idx";
DROP INDEX IF EXISTS "lab_case_tasks_labCaseId_treatmentDetailId_tooth_stepOrder_key";
ALTER TABLE "lab_case_tasks"
DROP COLUMN IF EXISTS "assigneeUserId",
DROP COLUMN IF EXISTS "assignedAt",
DROP COLUMN IF EXISTS "priority",
DROP COLUMN IF EXISTS "tooth";
-- 3. Rebuild LabTaskStatus enum without PENDING.
ALTER TABLE "lab_case_tasks" ALTER COLUMN "status" DROP DEFAULT;
ALTER TYPE "LabTaskStatus" RENAME TO "LabTaskStatus_old";
CREATE TYPE "LabTaskStatus" AS ENUM ('IN_PROGRESS', 'COMPLETED');
ALTER TABLE "lab_case_tasks"
ALTER COLUMN "status" TYPE "LabTaskStatus" USING ("status"::text::"LabTaskStatus");
ALTER TABLE "lab_case_tasks" ALTER COLUMN "status" SET DEFAULT 'IN_PROGRESS';
DROP TYPE "LabTaskStatus_old";
-- 4. New task columns.
ALTER TABLE "lab_case_tasks" ADD COLUMN "teeth" JSONB NOT NULL DEFAULT '[]';
ALTER TABLE "lab_case_tasks" ALTER COLUMN "teeth" DROP DEFAULT;
ALTER TABLE "lab_case_tasks" ADD COLUMN "isImportant" BOOLEAN NOT NULL DEFAULT false;
ALTER TABLE "lab_case_tasks" ADD COLUMN "lastStatusChangedByUserId" TEXT;
ALTER TABLE "lab_case_tasks" ADD COLUMN "lastStatusChangedAt" TIMESTAMP(3);
-- 5. New unique + indexes.
CREATE UNIQUE INDEX "lab_case_tasks_labCaseId_treatmentDetailId_prosthesisTypeCode_stepOrder_key"
ON "lab_case_tasks"("labCaseId", "treatmentDetailId", "prosthesisTypeCode", "stepOrder");
CREATE INDEX "lab_case_tasks_labCaseId_isImportant_idx"
ON "lab_case_tasks"("labCaseId", "isImportant");
ALTER TABLE "lab_case_tasks" ADD CONSTRAINT "lab_case_tasks_lastStatusChangedByUserId_fkey"
FOREIGN KEY ("lastStatusChangedByUserId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- 6. Task status timeline.
CREATE TABLE "lab_case_task_status_events" (
"id" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"fromStatus" "LabTaskStatus",
"toStatus" "LabTaskStatus" NOT NULL,
"changedByUserId" TEXT,
"changedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "lab_case_task_status_events_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "lab_case_task_status_events_taskId_changedAt_idx"
ON "lab_case_task_status_events"("taskId", "changedAt");
ALTER TABLE "lab_case_task_status_events" ADD CONSTRAINT "lab_case_task_status_events_taskId_fkey"
FOREIGN KEY ("taskId") REFERENCES "lab_case_tasks"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "lab_case_task_status_events" ADD CONSTRAINT "lab_case_task_status_events_changedByUserId_fkey"
FOREIGN KEY ("changedByUserId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- 7. Per-case comments.
CREATE TYPE "LabCaseCommentSide" AS ENUM ('LAB', 'CLINIC');
CREATE TABLE "lab_case_comments" (
"id" TEXT NOT NULL,
"labCaseId" TEXT NOT NULL,
"authorUserId" TEXT,
"authorOrganizationId" TEXT,
"authorSide" "LabCaseCommentSide" NOT NULL,
"body" TEXT NOT NULL,
"visibleToClinic" BOOLEAN NOT NULL DEFAULT false,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "lab_case_comments_pkey" PRIMARY KEY ("id")
);
CREATE INDEX "lab_case_comments_labCaseId_createdAt_idx"
ON "lab_case_comments"("labCaseId", "createdAt");
ALTER TABLE "lab_case_comments" ADD CONSTRAINT "lab_case_comments_labCaseId_fkey"
FOREIGN KEY ("labCaseId") REFERENCES "lab_cases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "lab_case_comments" ADD CONSTRAINT "lab_case_comments_authorUserId_fkey"
FOREIGN KEY ("authorUserId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;
ALTER TABLE "lab_case_comments" ADD CONSTRAINT "lab_case_comments_authorOrganizationId_fkey"
FOREIGN KEY ("authorOrganizationId") REFERENCES "organizations"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,60 @@
/**
* Dev-only: regenerate lab case tasks from existing LabCaseToothProsthesis rows.
* Usage: npx ts-node prisma/regenerate-lab-tasks.ts
*
* The lab workflow refactor truncated lab_case_tasks. This rebuilds task sets
* (grouped by treatment detail + prosthesis type, one set per workflow step)
* for every already-sent case that still has prosthesis selections.
*/
import { PrismaClient } from '@prisma/client';
import { config } from 'dotenv';
import path from 'path';
import { generateLabCaseTasks } from '../src/modules/cases/lab-case-task.generator';
const envPath = path.join(__dirname, '..', '.env');
config({ path: envPath });
if (process.env.NODE_ENV === 'production') {
console.error('regenerate-lab-tasks is not allowed in production');
process.exit(1);
}
const prisma = new PrismaClient();
async function main() {
const cases = await prisma.labCase.findMany({
where: {
sentAt: { not: null },
toothProsthesis: { some: {} },
},
select: {
id: true,
treatment: { select: { organization: { select: { owner: { select: { language: true } } } } } },
},
});
console.log(`Regenerating tasks for ${cases.length} sent case(s)...`);
let total = 0;
for (const labCase of cases) {
const locale = labCase.treatment.organization.owner.language ?? 'en';
// Clear any stale tasks first so the generator's "already exists" guard passes.
await prisma.labCaseTask.deleteMany({ where: { labCaseId: labCase.id } });
const created = await prisma.$transaction((tx) =>
generateLabCaseTasks(tx, labCase.id, locale),
);
total += created;
console.log(` - ${labCase.id}: ${created} task(s)`);
}
console.log(`Done. ${total} task(s) created.`);
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});

View File

@@ -21,6 +21,8 @@ const prisma = new PrismaClient();
// FK-safe order: children before parents.
const TABLES_IN_ORDER = [
'lab_case_task_status_events',
'lab_case_comments',
'lab_case_tasks',
'lab_case_sends',
'lab_case_tooth_prosthesis',

View File

@@ -23,7 +23,9 @@ model User {
sessions Session[] // 👈 ADD THIS - opposite relation for Session
sentStaffInvites StaffInvitation[]
sentOrganizationInvitations OrganizationInvitation[]
assignedLabCaseTasks LabCaseTask[] @relation("LabCaseTaskAssignee")
statusChangedLabCaseTasks LabCaseTask[] @relation("LabCaseTaskLastStatusChangedBy")
labCaseTaskStatusEvents LabCaseTaskStatusEvent[]
labCaseComments LabCaseComment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -65,6 +67,7 @@ model Organization {
appointments Appointment[]
treatments Treatment[]
labCaseSends LabCaseSend[]
labCaseComments LabCaseComment[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -115,11 +118,15 @@ model Appointment {
}
enum LabTaskStatus {
PENDING
IN_PROGRESS
COMPLETED
}
enum LabCaseCommentSide {
LAB
CLINIC
}
model Treatment {
id String @id @default(uuid())
organizationId String
@@ -194,6 +201,7 @@ model LabCase {
sends LabCaseSend[]
tasks LabCaseTask[]
toothProsthesis LabCaseToothProsthesis[]
comments LabCaseComment[]
@@index([treatmentId, sortOrder])
@@map("lab_cases")
@@ -306,31 +314,66 @@ model LabCaseTask {
id String @id @default(uuid())
labCaseId String
treatmentDetailId String
tooth String
teeth Json
treatmentType String
prosthesisTypeCode String
workflowStepCode String
stepOrder Int
stepLabel String
assigneeUserId String?
assignedAt DateTime?
priority Int @default(3)
status LabTaskStatus @default(PENDING)
isImportant Boolean @default(false)
status LabTaskStatus @default(IN_PROGRESS)
lastStatusChangedByUserId String?
lastStatusChangedAt DateTime?
labCase LabCase @relation(fields: [labCaseId], references: [id], onDelete: Cascade)
detail TreatmentDetail @relation(fields: [treatmentDetailId], references: [id], onDelete: Cascade)
assignee User? @relation("LabCaseTaskAssignee", fields: [assigneeUserId], references: [id], onDelete: SetNull)
lastStatusChangedBy User? @relation("LabCaseTaskLastStatusChangedBy", fields: [lastStatusChangedByUserId], references: [id], onDelete: SetNull)
statusEvents LabCaseTaskStatusEvent[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([labCaseId, treatmentDetailId, tooth, stepOrder])
@@unique([labCaseId, treatmentDetailId, prosthesisTypeCode, stepOrder])
@@index([labCaseId, status])
@@index([assigneeUserId, priority, createdAt])
@@index([assignedAt, labCaseId, priority])
@@index([labCaseId, isImportant])
@@map("lab_case_tasks")
}
model LabCaseTaskStatusEvent {
id String @id @default(uuid())
taskId String
fromStatus LabTaskStatus?
toStatus LabTaskStatus
changedByUserId String?
changedAt DateTime @default(now())
task LabCaseTask @relation(fields: [taskId], references: [id], onDelete: Cascade)
changedBy User? @relation(fields: [changedByUserId], references: [id], onDelete: SetNull)
@@index([taskId, changedAt])
@@map("lab_case_task_status_events")
}
model LabCaseComment {
id String @id @default(uuid())
labCaseId String
authorUserId String?
authorOrganizationId String?
authorSide LabCaseCommentSide
body String
visibleToClinic Boolean @default(false)
labCase LabCase @relation(fields: [labCaseId], references: [id], onDelete: Cascade)
authorUser User? @relation(fields: [authorUserId], references: [id], onDelete: SetNull)
authorOrganization Organization? @relation(fields: [authorOrganizationId], references: [id], onDelete: SetNull)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([labCaseId, createdAt])
@@map("lab_case_comments")
}
model Plan {
id String @id @default(uuid())
name String @unique // "Solo", "Small", "Medium", "Large", "Enterprise"