improvement: all clinic side ui components related to treatment types updated based on the new real world data.

This commit is contained in:
2026-07-07 13:13:04 +03:30
parent 667b08ed0c
commit ed7e7b1d8f
20 changed files with 321 additions and 148 deletions

View File

@@ -7,7 +7,17 @@ export type CatalogTranslationSeed = {
label: string;
};
export const TREATMENT_TYPES = [
export type TreatmentTypeSeed = {
code: string;
labDependent: boolean;
sortOrder: number;
/** Selectable when booking an appointment. Defaults to true. */
availableInAppointments?: boolean;
/** Selectable as a treatment plan detail. Defaults to true. */
availableInTreatment?: boolean;
};
export const TREATMENT_TYPES: readonly TreatmentTypeSeed[] = [
{ code: 'restoration', labDependent: false, sortOrder: 1 },
{ code: 'specialized_restoration', labDependent: false, sortOrder: 2 },
{ code: 'radiography', labDependent: false, sortOrder: 3 },
@@ -19,11 +29,23 @@ export const TREATMENT_TYPES = [
{ code: 'perio', labDependent: false, sortOrder: 9 },
{ code: 'pediatrics', labDependent: false, sortOrder: 10 },
{ code: 'extraction', labDependent: false, sortOrder: 11 },
{ code: 'clinic_visit', labDependent: false, sortOrder: 12 },
// Appointment-only: not real treatment plan details.
{
code: 'clinic_visit',
labDependent: false,
sortOrder: 12,
availableInTreatment: false,
},
{
code: 'continue_treatment',
labDependent: false,
sortOrder: 13,
availableInTreatment: false,
},
] as const;
/** Legacy codes kept for historical rows; hidden from catalog. */
export const LEGACY_TREATMENT_TYPES = [
export const LEGACY_TREATMENT_TYPES: readonly TreatmentTypeSeed[] = [
{ code: 'consultation', labDependent: false, sortOrder: 99 },
{ code: 'filling', labDependent: false, sortOrder: 100 },
{ code: 'visit', labDependent: false, sortOrder: 101 },
@@ -207,7 +229,8 @@ const TREATMENT_LABELS: Record<string, Record<string, string>> = {
perio: { en: 'Perio', fa: 'پریو', nl: 'Paro' },
pediatrics: { en: 'Pediatrics', fa: 'اطفال', nl: 'Kinderen' },
extraction: { en: 'Extraction', fa: 'کشیدن', nl: 'Extractie' },
clinic_visit: { en: 'Clinic Visit', fa: 'درمانگاه', nl: 'Kliniekbezoek' },
clinic_visit: { en: 'Clinic Visit', fa: 'ویزیت درمانگاه', nl: 'Kliniekbezoek' },
continue_treatment: { en: 'Continue Treatment', fa: 'ادامه درمان', nl: 'Behandeling Voortzetten' },
consultation: { en: 'Consultation', fa: 'مشاوره', nl: 'Consult' },
filling: { en: 'Filling', fa: 'پر کردن', nl: 'Vulling' },
visit: { en: 'Visit', fa: 'ویزیت', nl: 'Bezoek' },

View File

@@ -0,0 +1,3 @@
-- Treatment type context flags: control which selection contexts each type appears in.
ALTER TABLE "treatment_types" ADD COLUMN "availableInAppointments" BOOLEAN NOT NULL DEFAULT true;
ALTER TABLE "treatment_types" ADD COLUMN "availableInTreatment" BOOLEAN NOT NULL DEFAULT true;

View File

@@ -1,6 +1,9 @@
/**
* Dev-only: truncate treatment and lab case data (preserves catalog tables).
* Usage: npx ts-node prisma/reset-treatment-data.ts
*
* Safe to run before or after `prisma migrate deploy`: tables that do not yet
* exist are skipped instead of throwing.
*/
import { PrismaClient } from '@prisma/client';
import { config } from 'dotenv';
@@ -16,17 +19,44 @@ if (process.env.NODE_ENV === 'production') {
const prisma = new PrismaClient();
// FK-safe order: children before parents.
const TABLES_IN_ORDER = [
'lab_case_tasks',
'lab_case_sends',
'lab_case_tooth_prosthesis',
'lab_case_details',
'lab_cases',
'treatment_detail_attachments',
'treatment_details',
'treatments',
];
async function tableExists(table: string): Promise<boolean> {
const rows = await prisma.$queryRawUnsafe<Array<{ exists: string | null }>>(
`SELECT to_regclass('public."${table}"')::text AS exists`,
);
return rows[0]?.exists != null;
}
async function main() {
console.log('Truncating treatment and lab case data...');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_tasks" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_sends" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_tooth_prosthesis" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_case_details" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "lab_cases" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "treatment_detail_attachments" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "treatment_details" CASCADE');
await prisma.$executeRawUnsafe('TRUNCATE TABLE "treatments" CASCADE');
const existing: string[] = [];
for (const table of TABLES_IN_ORDER) {
if (await tableExists(table)) {
existing.push(table);
} else {
console.log(` - skipping "${table}" (does not exist yet)`);
}
}
if (existing.length === 0) {
console.log('No target tables exist yet. Run `prisma migrate deploy` first.');
return;
}
const targets = existing.map((t) => `"${t}"`).join(', ');
await prisma.$executeRawUnsafe(`TRUNCATE TABLE ${targets} CASCADE`);
console.log('Done.');
}

View File

@@ -224,11 +224,13 @@ model LabCaseSend {
}
model TreatmentType {
id String @id @default(uuid())
code String @unique
labDependent Boolean @default(false)
sortOrder Int @default(0)
isActive Boolean @default(true)
id String @id @default(uuid())
code String @unique
labDependent Boolean @default(false)
sortOrder Int @default(0)
isActive Boolean @default(true)
availableInAppointments Boolean @default(true)
availableInTreatment Boolean @default(true)
@@map("treatment_types")
}

View File

@@ -138,12 +138,16 @@ async function main() {
for (const type of [...TREATMENT_TYPES, ...LEGACY_TREATMENT_TYPES]) {
const isActive = TREATMENT_TYPES.some((t) => t.code === type.code);
const availableInAppointments = type.availableInAppointments ?? true;
const availableInTreatment = type.availableInTreatment ?? true;
await prisma.treatmentType.upsert({
where: { code: type.code },
update: {
labDependent: type.labDependent,
sortOrder: type.sortOrder,
isActive,
availableInAppointments,
availableInTreatment,
},
create: {
id: randomUUID(),
@@ -151,6 +155,8 @@ async function main() {
labDependent: type.labDependent,
sortOrder: type.sortOrder,
isActive,
availableInAppointments,
availableInTreatment,
},
});
}

View File

@@ -1,7 +1,10 @@
import { Controller, Get, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiTags } from '@nestjs/swagger';
import { Controller, Get, Query, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard';
import { TreatmentCatalogService } from './treatment-catalog.service';
import {
TreatmentCatalogContext,
TreatmentCatalogService,
} from './treatment-catalog.service';
@ApiTags('treatment-catalog')
@ApiBearerAuth('JWT-auth')
@@ -12,8 +15,24 @@ export class TreatmentCatalogController {
@Get()
@ApiOperation({ summary: 'List active treatment types with localized labels' })
async list(@Req() req: { user?: { language?: string | null } }) {
const data = await this.treatmentCatalogService.list(req.user?.language);
@ApiQuery({
name: 'context',
required: false,
enum: ['appointment', 'treatment'],
description: 'Filter to types selectable in the given context',
})
async list(
@Req() req: { user?: { language?: string | null } },
@Query('context') context?: string,
) {
const normalizedContext =
context === 'appointment' || context === 'treatment'
? (context as TreatmentCatalogContext)
: undefined;
const data = await this.treatmentCatalogService.list(
req.user?.language,
normalizedContext,
);
return { success: true, data };
}
}

View File

@@ -7,12 +7,16 @@ import {
normalizeCatalogLocale,
} from '../catalog/catalog-label.service';
export type TreatmentCatalogContext = 'appointment' | 'treatment';
export type TreatmentTypeCatalogEntry = {
id: string;
code: string;
labDependent: boolean;
sortOrder: number;
label: string;
availableInAppointments: boolean;
availableInTreatment: boolean;
};
@Injectable()
@@ -33,7 +37,14 @@ export class TreatmentCatalogService implements OnModuleInit {
const rows = await this.prisma.treatmentType.findMany({
where: { isActive: true },
orderBy: [{ sortOrder: 'asc' }, { code: 'asc' }],
select: { id: true, code: true, labDependent: true, sortOrder: true },
select: {
id: true,
code: true,
labDependent: true,
sortOrder: true,
availableInAppointments: true,
availableInTreatment: true,
},
});
this.byCode = new Map(
@@ -45,17 +56,26 @@ export class TreatmentCatalogService implements OnModuleInit {
labDependent: row.labDependent,
sortOrder: row.sortOrder,
label: row.code,
availableInAppointments: row.availableInAppointments,
availableInTreatment: row.availableInTreatment,
},
]),
);
this.loaded = true;
}
async list(localeInput?: string | null): Promise<TreatmentTypeCatalogEntry[]> {
async list(
localeInput?: string | null,
context?: TreatmentCatalogContext | null,
): Promise<TreatmentTypeCatalogEntry[]> {
await this.ensureLabels(localeInput);
return [...this.byCode.values()].sort(
(a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code),
);
return [...this.byCode.values()]
.filter((entry) => {
if (context === 'appointment') return entry.availableInAppointments;
if (context === 'treatment') return entry.availableInTreatment;
return true;
})
.sort((a, b) => a.sortOrder - b.sortOrder || a.code.localeCompare(b.code));
}
private async ensureLabels(localeInput?: string | null) {