feature: Phase3 - Task templates + generation on send

This commit is contained in:
2026-06-28 16:46:42 +03:30
parent 8b4ef6195d
commit 21f545ebdb
25 changed files with 1448 additions and 31 deletions

View File

@@ -0,0 +1,46 @@
-- Treatment workflow steps + lab case tasks
CREATE TYPE "LabTaskStatus" AS ENUM ('PENDING', 'IN_PROGRESS', 'COMPLETED');
CREATE TABLE "treatment_workflow_steps" (
"id" TEXT NOT NULL,
"treatmentType" TEXT NOT NULL,
"stepOrder" INTEGER NOT NULL,
"label" TEXT NOT NULL,
CONSTRAINT "treatment_workflow_steps_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "treatment_workflow_steps_treatmentType_stepOrder_key"
ON "treatment_workflow_steps"("treatmentType", "stepOrder");
CREATE TABLE "lab_case_tasks" (
"id" TEXT NOT NULL,
"labCaseId" TEXT NOT NULL,
"treatmentDetailId" TEXT NOT NULL,
"tooth" TEXT NOT NULL,
"treatmentType" TEXT NOT NULL,
"stepOrder" INTEGER NOT NULL,
"stepLabel" TEXT NOT NULL,
"assigneeUserId" TEXT,
"status" "LabTaskStatus" NOT NULL DEFAULT 'PENDING',
CONSTRAINT "lab_case_tasks_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "lab_case_tasks_labCaseId_tooth_treatmentType_stepOrder_key"
ON "lab_case_tasks"("labCaseId", "tooth", "treatmentType", "stepOrder");
CREATE INDEX "lab_case_tasks_labCaseId_status_idx" ON "lab_case_tasks"("labCaseId", "status");
ALTER TABLE "lab_case_tasks"
ADD CONSTRAINT "lab_case_tasks_labCaseId_fkey"
FOREIGN KEY ("labCaseId") REFERENCES "lab_cases"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "lab_case_tasks"
ADD CONSTRAINT "lab_case_tasks_treatmentDetailId_fkey"
FOREIGN KEY ("treatmentDetailId") REFERENCES "treatment_details"("id") ON DELETE CASCADE ON UPDATE CASCADE;
ALTER TABLE "lab_case_tasks"
ADD CONSTRAINT "lab_case_tasks_assigneeUserId_fkey"
FOREIGN KEY ("assigneeUserId") REFERENCES "users"("id") ON DELETE SET NULL ON UPDATE CASCADE;

View File

@@ -0,0 +1,49 @@
-- Treatment type catalog (data-driven; business logic reads from here)
CREATE TABLE "treatment_types" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"labDependent" BOOLEAN NOT NULL DEFAULT false,
"sortOrder" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "treatment_types_pkey" PRIMARY KEY ("id")
);
CREATE UNIQUE INDEX "treatment_types_code_key" ON "treatment_types"("code");
-- Temporary catalog (will be replaced with 14 real-world types later)
INSERT INTO "treatment_types" ("id", "code", "labDependent", "sortOrder") VALUES
('tt-consultation', 'consultation', false, 1),
('tt-filling', 'filling', false, 2),
('tt-endo', 'endo', true, 3),
('tt-visit', 'visit', false, 4),
('tt-hygiene', 'hygiene', false, 5);
-- Re-link workflow steps to catalog rows
ALTER TABLE "treatment_workflow_steps" ADD COLUMN "treatmentTypeId" TEXT;
UPDATE "treatment_workflow_steps" AS w
SET "treatmentTypeId" = t."id"
FROM "treatment_types" AS t
WHERE t."code" = w."treatmentType";
-- Drop steps for clinic-only types; only lab-dependent types keep workflows
DELETE FROM "treatment_workflow_steps" AS w
USING "treatment_types" AS t
WHERE w."treatmentTypeId" = t."id" AND t."labDependent" = false;
DELETE FROM "treatment_workflow_steps" WHERE "treatmentTypeId" IS NULL;
ALTER TABLE "treatment_workflow_steps" DROP CONSTRAINT IF EXISTS "treatment_workflow_steps_treatmentType_stepOrder_key";
DROP INDEX IF EXISTS "treatment_workflow_steps_treatmentType_stepOrder_key";
ALTER TABLE "treatment_workflow_steps" DROP COLUMN "treatmentType";
ALTER TABLE "treatment_workflow_steps" ALTER COLUMN "treatmentTypeId" SET NOT NULL;
ALTER TABLE "treatment_workflow_steps"
ADD CONSTRAINT "treatment_workflow_steps_treatmentTypeId_fkey"
FOREIGN KEY ("treatmentTypeId") REFERENCES "treatment_types"("id") ON DELETE CASCADE ON UPDATE CASCADE;
CREATE UNIQUE INDEX "treatment_workflow_steps_treatmentTypeId_stepOrder_key"
ON "treatment_workflow_steps"("treatmentTypeId", "stepOrder");

View File

@@ -23,6 +23,7 @@ model User {
sessions Session[] // 👈 ADD THIS - opposite relation for Session
sentStaffInvites StaffInvitation[]
sentOrganizationInvitations OrganizationInvitation[]
assignedLabCaseTasks LabCaseTask[] @relation("LabCaseTaskAssignee")
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@ -118,6 +119,12 @@ enum TreatmentStatus {
COMPLETED
}
enum LabTaskStatus {
PENDING
IN_PROGRESS
COMPLETED
}
model Treatment {
id String @id @default(uuid())
organizationId String
@@ -154,6 +161,7 @@ model TreatmentDetail {
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)
attachments TreatmentDetailAttachment[]
labCaseLink LabCaseDetail?
labCaseTasks LabCaseTask[]
@@index([treatmentId, sortOrder])
@@map("treatment_details")
@@ -190,6 +198,7 @@ model LabCase {
treatment Treatment @relation(fields: [treatmentId], references: [id], onDelete: Cascade)
details LabCaseDetail[]
sends LabCaseSend[]
tasks LabCaseTask[]
@@index([treatmentId, sortOrder])
@@map("lab_cases")
@@ -219,6 +228,49 @@ model LabCaseSend {
@@map("lab_case_sends")
}
model TreatmentType {
id String @id @default(uuid())
code String @unique
labDependent Boolean @default(false)
sortOrder Int @default(0)
workflowSteps TreatmentWorkflowStep[]
@@map("treatment_types")
}
model TreatmentWorkflowStep {
id String @id @default(uuid())
treatmentTypeId String
stepOrder Int
label String
treatmentType TreatmentType @relation(fields: [treatmentTypeId], references: [id], onDelete: Cascade)
@@unique([treatmentTypeId, stepOrder])
@@map("treatment_workflow_steps")
}
model LabCaseTask {
id String @id @default(uuid())
labCaseId String
treatmentDetailId String
tooth String
treatmentType String
stepOrder Int
stepLabel String
assigneeUserId String?
status LabTaskStatus @default(PENDING)
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)
@@unique([labCaseId, tooth, treatmentType, stepOrder])
@@index([labCaseId, status])
@@map("lab_case_tasks")
}
model Plan {
id String @id @default(uuid())
name String @unique // "Solo", "Small", "Medium", "Large", "Enterprise"

View File

@@ -1,5 +1,6 @@
// backend/prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import { randomUUID } from 'crypto';
import { config } from 'dotenv';
import path from 'path';
@@ -123,6 +124,57 @@ async function main() {
}
console.log('✅ Created features and permissions');
const workflowSteps = [
{ code: 'endo', stepOrder: 1, label: 'Access review' },
{ code: 'endo', stepOrder: 2, label: 'Fabrication' },
] as const;
const treatmentTypes = [
{ code: 'consultation', labDependent: false, sortOrder: 1 },
{ code: 'filling', labDependent: false, sortOrder: 2 },
{ code: 'endo', labDependent: true, sortOrder: 3 },
{ code: 'visit', labDependent: false, sortOrder: 4 },
{ code: 'hygiene', labDependent: false, sortOrder: 5 },
] as const;
for (const type of treatmentTypes) {
await prisma.treatmentType.upsert({
where: { code: type.code },
update: { labDependent: type.labDependent, sortOrder: type.sortOrder },
create: {
id: randomUUID(),
code: type.code,
labDependent: type.labDependent,
sortOrder: type.sortOrder,
},
});
}
console.log('✅ Seeded treatment type catalog');
for (const step of workflowSteps) {
const treatmentType = await prisma.treatmentType.findUniqueOrThrow({
where: { code: step.code },
select: { id: true },
});
await prisma.treatmentWorkflowStep.upsert({
where: {
treatmentTypeId_stepOrder: {
treatmentTypeId: treatmentType.id,
stepOrder: step.stepOrder,
},
},
update: { label: step.label },
create: {
id: randomUUID(),
treatmentTypeId: treatmentType.id,
stepOrder: step.stepOrder,
label: step.label,
},
});
}
console.log('✅ Seeded lab workflow steps');
console.log('🌱 Seeding completed successfully!');
}