TreatmentType and ProsthesisType database shcema and data updated. Lab dispatch wired through new data.
This commit is contained in:
@@ -46,7 +46,7 @@ export class CasesController {
|
||||
@ApiOperation({ summary: 'Get one lab case with tasks grouped by tooth' })
|
||||
getOne(@Param('id') id: string, @Req() req) {
|
||||
const organizationId = this.casesService.getOrganizationIdFromUser(req.user);
|
||||
return this.casesService.getOne(id, organizationId, req.user.id);
|
||||
return this.casesService.getOne(id, organizationId, req.user.id, req.user.language);
|
||||
}
|
||||
|
||||
@Patch(':id/tasks/:taskId')
|
||||
@@ -58,6 +58,6 @@ export class CasesController {
|
||||
@Req() req,
|
||||
) {
|
||||
const organizationId = this.casesService.getOrganizationIdFromUser(req.user);
|
||||
return this.casesService.updateTask(id, taskId, dto, organizationId, req.user.id);
|
||||
return this.casesService.updateTask(id, taskId, dto, organizationId, req.user.id, req.user.language);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,9 +4,13 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { LabTaskStatus, Prisma } from '@prisma/client';
|
||||
import { CatalogEntityKind, LabTaskStatus, Prisma } from '@prisma/client';
|
||||
import { PrismaService } from '../../../prisma/prisma.service';
|
||||
import { normalizeMobile } from '../../common/phone';
|
||||
import {
|
||||
CatalogLabelService,
|
||||
normalizeCatalogLocale,
|
||||
} from '../catalog/catalog-label.service';
|
||||
import { TreatmentCatalogService } from '../treatment-catalog/treatment-catalog.service';
|
||||
import { normalizeTeeth } from '../treatments/treatment.utils';
|
||||
import { ListLabCasesDto, UpdateLabCaseTaskDto } from './dto/cases.dto';
|
||||
@@ -52,6 +56,7 @@ export class CasesService {
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
private readonly treatmentCatalog: TreatmentCatalogService,
|
||||
private readonly catalogLabels: CatalogLabelService,
|
||||
) {}
|
||||
|
||||
getOrganizationIdFromUser(user: { organizationId?: string }) {
|
||||
@@ -142,8 +147,8 @@ export class CasesService {
|
||||
}
|
||||
}
|
||||
|
||||
const treatmentTypes = this.treatmentCatalog
|
||||
.list()
|
||||
const catalog = await this.treatmentCatalog.list();
|
||||
const treatmentTypes = catalog
|
||||
.filter((entry) => entry.labDependent && typeCodes.has(entry.code))
|
||||
.map((entry) => ({ code: entry.code, labDependent: entry.labDependent }));
|
||||
|
||||
@@ -218,6 +223,7 @@ export class CasesService {
|
||||
labCaseId: string,
|
||||
clinicOrganizationId: string,
|
||||
labOrganizationId: string,
|
||||
localeInput?: string | null,
|
||||
) {
|
||||
const labCase = await this.prisma.labCase.findFirst({
|
||||
where: {
|
||||
@@ -233,10 +239,18 @@ export class CasesService {
|
||||
throw new NotFoundException('Case not found');
|
||||
}
|
||||
|
||||
return { success: true, data: this.mapLabCaseDetail(labCase) };
|
||||
return {
|
||||
success: true,
|
||||
data: await this.mapLabCaseDetail(labCase, localeInput),
|
||||
};
|
||||
}
|
||||
|
||||
async getOne(labCaseId: string, labOrganizationId: string, actorUserId: string) {
|
||||
async getOne(
|
||||
labCaseId: string,
|
||||
labOrganizationId: string,
|
||||
actorUserId: string,
|
||||
localeInput?: string | null,
|
||||
) {
|
||||
await this.assertCanReadCases(actorUserId, labOrganizationId);
|
||||
|
||||
const labCase = await this.prisma.labCase.findFirst({
|
||||
@@ -252,7 +266,7 @@ export class CasesService {
|
||||
throw new NotFoundException('Case not found');
|
||||
}
|
||||
|
||||
return { success: true, data: this.mapLabCaseDetail(labCase) };
|
||||
return { success: true, data: await this.mapLabCaseDetail(labCase, localeInput) };
|
||||
}
|
||||
|
||||
async updateTask(
|
||||
@@ -261,6 +275,7 @@ export class CasesService {
|
||||
dto: UpdateLabCaseTaskDto,
|
||||
labOrganizationId: string,
|
||||
actorUserId: string,
|
||||
localeInput?: string | null,
|
||||
) {
|
||||
await this.assertCanEditCases(actorUserId, labOrganizationId);
|
||||
|
||||
@@ -299,7 +314,14 @@ export class CasesService {
|
||||
},
|
||||
});
|
||||
|
||||
return { success: true, data: this.mapTask(updated) };
|
||||
const locale = normalizeCatalogLocale(localeInput);
|
||||
const prosthesisLabels = await this.catalogLabels.resolveLabels(
|
||||
CatalogEntityKind.PROSTHESIS_TYPE,
|
||||
[updated.prosthesisTypeCode],
|
||||
locale,
|
||||
);
|
||||
|
||||
return { success: true, data: this.mapTask(updated, prosthesisLabels) };
|
||||
}
|
||||
|
||||
async listAssignableMembers(labOrganizationId: string, actorUserId: string) {
|
||||
@@ -431,9 +453,19 @@ export class CasesService {
|
||||
};
|
||||
}
|
||||
|
||||
private mapLabCaseDetail(lc: Prisma.LabCaseGetPayload<{ include: typeof labCaseListInclude }>) {
|
||||
private async mapLabCaseDetail(
|
||||
lc: Prisma.LabCaseGetPayload<{ include: typeof labCaseListInclude }>,
|
||||
localeInput?: string | null,
|
||||
) {
|
||||
const locale = normalizeCatalogLocale(localeInput);
|
||||
const treatmentTypes = [...new Set(lc.details.map((d) => d.detail.treatmentType))];
|
||||
const tasksByTooth = this.groupTasksByTooth(lc.tasks);
|
||||
const prosthesisCodes = [...new Set(lc.tasks.map((t) => t.prosthesisTypeCode).filter(Boolean))];
|
||||
const prosthesisLabels = await this.catalogLabels.resolveLabels(
|
||||
CatalogEntityKind.PROSTHESIS_TYPE,
|
||||
prosthesisCodes,
|
||||
locale,
|
||||
);
|
||||
const tasksByTooth = this.groupTasksByTooth(lc.tasks, prosthesisLabels);
|
||||
|
||||
return {
|
||||
id: lc.id,
|
||||
@@ -454,7 +486,7 @@ export class CasesService {
|
||||
organizationName: s.organization.name,
|
||||
sentAt: s.sentAt.toISOString(),
|
||||
})),
|
||||
tasks: lc.tasks.map((t) => this.mapTask(t)),
|
||||
tasks: lc.tasks.map((t) => this.mapTask(t, prosthesisLabels)),
|
||||
tasksByTooth,
|
||||
taskProgress: {
|
||||
completed: lc.tasks.filter((t) => t.status === LabTaskStatus.COMPLETED).length,
|
||||
@@ -468,6 +500,7 @@ export class CasesService {
|
||||
id: string;
|
||||
tooth: string;
|
||||
treatmentType: string;
|
||||
prosthesisTypeCode: string;
|
||||
stepOrder: number;
|
||||
stepLabel: string;
|
||||
status: LabTaskStatus;
|
||||
@@ -477,47 +510,62 @@ export class CasesService {
|
||||
createdAt: Date;
|
||||
assignee: { id: string; name: string; email: string } | null;
|
||||
}>,
|
||||
prosthesisLabels: Map<string, string>,
|
||||
) {
|
||||
const groups = new Map<
|
||||
string,
|
||||
{
|
||||
tooth: string;
|
||||
treatmentType: string;
|
||||
prosthesisTypeCode: string;
|
||||
prosthesisTypeLabel: string;
|
||||
tasks: ReturnType<CasesService['mapTask']>[];
|
||||
}
|
||||
>();
|
||||
|
||||
for (const task of tasks) {
|
||||
const key = `${task.tooth}:${task.treatmentType}`;
|
||||
const key = `${task.tooth}:${task.treatmentType}:${task.prosthesisTypeCode}`;
|
||||
const entry = groups.get(key) ?? {
|
||||
tooth: task.tooth,
|
||||
treatmentType: task.treatmentType,
|
||||
prosthesisTypeCode: task.prosthesisTypeCode,
|
||||
prosthesisTypeLabel:
|
||||
prosthesisLabels.get(task.prosthesisTypeCode) ?? task.prosthesisTypeCode,
|
||||
tasks: [],
|
||||
};
|
||||
entry.tasks.push(this.mapTask(task));
|
||||
entry.tasks.push(this.mapTask(task, prosthesisLabels));
|
||||
groups.set(key, entry);
|
||||
}
|
||||
|
||||
return [...groups.values()];
|
||||
}
|
||||
|
||||
private mapTask(task: {
|
||||
id: string;
|
||||
tooth: string;
|
||||
treatmentType: 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 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;
|
||||
},
|
||||
prosthesisLabels: Map<string, string>,
|
||||
) {
|
||||
return {
|
||||
id: task.id,
|
||||
tooth: task.tooth,
|
||||
treatmentType: task.treatmentType,
|
||||
prosthesisTypeCode: task.prosthesisTypeCode,
|
||||
prosthesisTypeLabel:
|
||||
prosthesisLabels.get(task.prosthesisTypeCode) ?? task.prosthesisTypeCode,
|
||||
workflowStepCode: task.workflowStepCode ?? '',
|
||||
stepOrder: task.stepOrder,
|
||||
stepLabel: task.stepLabel,
|
||||
status: task.status,
|
||||
|
||||
154
backend/src/modules/cases/lab-case-task.generator.spec.ts
Normal file
154
backend/src/modules/cases/lab-case-task.generator.spec.ts
Normal file
@@ -0,0 +1,154 @@
|
||||
import { CatalogEntityKind } from '@prisma/client';
|
||||
import {
|
||||
PROSTHESIS_TYPES,
|
||||
buildProsthesisStepCodes,
|
||||
} from '../../../prisma/catalog-seed-data';
|
||||
import { generateLabCaseTasks } from './lab-case-task.generator';
|
||||
|
||||
function buildMockTx(options: {
|
||||
existingCount?: number;
|
||||
toothProsthesisRows: Array<{
|
||||
treatmentDetailId: string;
|
||||
tooth: string;
|
||||
prosthesisTypeCode: string;
|
||||
treatmentType?: string;
|
||||
}>;
|
||||
prosthesisTypes: Array<{
|
||||
code: string;
|
||||
steps: Array<{ stepOrder: number; workflowStepCode: string }>;
|
||||
}>;
|
||||
stepLabels?: Record<string, string>;
|
||||
}) {
|
||||
const created: unknown[] = [];
|
||||
|
||||
const tx = {
|
||||
labCaseTask: {
|
||||
count: jest.fn().mockResolvedValue(options.existingCount ?? 0),
|
||||
createMany: jest.fn().mockImplementation(({ data }) => {
|
||||
created.push(...data);
|
||||
return { count: data.length };
|
||||
}),
|
||||
},
|
||||
labCaseToothProsthesis: {
|
||||
findMany: jest.fn().mockResolvedValue(
|
||||
options.toothProsthesisRows.map((row) => ({
|
||||
treatmentDetailId: row.treatmentDetailId,
|
||||
tooth: row.tooth,
|
||||
prosthesisTypeCode: row.prosthesisTypeCode,
|
||||
detail: {
|
||||
id: row.treatmentDetailId,
|
||||
treatmentType: row.treatmentType ?? 'prosthesis',
|
||||
},
|
||||
})),
|
||||
),
|
||||
},
|
||||
prosthesisType: {
|
||||
findMany: jest.fn().mockResolvedValue(
|
||||
options.prosthesisTypes.map((type) => ({
|
||||
code: type.code,
|
||||
isActive: true,
|
||||
steps: type.steps.map((step) => ({
|
||||
stepOrder: step.stepOrder,
|
||||
labWorkflowStep: { code: step.workflowStepCode },
|
||||
})),
|
||||
})),
|
||||
),
|
||||
},
|
||||
catalogTranslation: {
|
||||
findMany: jest.fn().mockImplementation(({ where }) => {
|
||||
const codes = where.entityCode?.in ?? [];
|
||||
return codes.map((code: string) => ({
|
||||
entityCode: code,
|
||||
locale: 'en',
|
||||
label: options.stepLabels?.[code] ?? code,
|
||||
entityKind: CatalogEntityKind.LAB_WORKFLOW_STEP,
|
||||
}));
|
||||
}),
|
||||
},
|
||||
};
|
||||
|
||||
return { tx, created };
|
||||
}
|
||||
|
||||
function stepsFromSeed(code: string) {
|
||||
const seed = PROSTHESIS_TYPES.find((type) => type.code === code);
|
||||
if (!seed) {
|
||||
throw new Error(`Unknown prosthesis code: ${code}`);
|
||||
}
|
||||
return buildProsthesisStepCodes(seed).map((workflowStepCode, index) => ({
|
||||
stepOrder: index + 1,
|
||||
workflowStepCode,
|
||||
}));
|
||||
}
|
||||
|
||||
describe('generateLabCaseTasks', () => {
|
||||
it('creates tasks for pfm_crown with universal and type-specific steps', async () => {
|
||||
const pfmSteps = stepsFromSeed('pfm_crown');
|
||||
const { tx, created } = buildMockTx({
|
||||
toothProsthesisRows: [
|
||||
{
|
||||
treatmentDetailId: 'detail-1',
|
||||
tooth: '14',
|
||||
prosthesisTypeCode: 'pfm_crown',
|
||||
},
|
||||
],
|
||||
prosthesisTypes: [{ code: 'pfm_crown', steps: pfmSteps }],
|
||||
stepLabels: { intraoral_scan: 'Intraoral Scan', packing: 'Packing' },
|
||||
});
|
||||
|
||||
const count = await generateLabCaseTasks(tx as never, 'lab-case-1', 'en');
|
||||
|
||||
expect(count).toBe(pfmSteps.length);
|
||||
expect(created).toHaveLength(pfmSteps.length);
|
||||
expect(created[0]).toMatchObject({
|
||||
tooth: '14',
|
||||
prosthesisTypeCode: 'pfm_crown',
|
||||
workflowStepCode: 'intraoral_scan',
|
||||
stepLabel: 'Intraoral Scan',
|
||||
});
|
||||
const stepCodes = (created as Array<{ workflowStepCode: string }>).map(
|
||||
(row) => row.workflowStepCode,
|
||||
);
|
||||
expect(stepCodes).toEqual(pfmSteps.map((step) => step.workflowStepCode));
|
||||
expect(stepCodes).toContain('packing');
|
||||
expect(stepCodes).toContain('shipping');
|
||||
expect(stepCodes).toContain('milling_wet');
|
||||
});
|
||||
|
||||
it('omits packing and shipping for smile_design', async () => {
|
||||
const smileSteps = stepsFromSeed('smile_design');
|
||||
const { tx, created } = buildMockTx({
|
||||
toothProsthesisRows: [
|
||||
{
|
||||
treatmentDetailId: 'detail-1',
|
||||
tooth: '11',
|
||||
prosthesisTypeCode: 'smile_design',
|
||||
},
|
||||
],
|
||||
prosthesisTypes: [{ code: 'smile_design', steps: smileSteps }],
|
||||
});
|
||||
|
||||
const count = await generateLabCaseTasks(tx as never, 'lab-case-2', 'en');
|
||||
|
||||
expect(count).toBe(smileSteps.length);
|
||||
const stepCodes = (created as Array<{ workflowStepCode: string }>).map(
|
||||
(row) => row.workflowStepCode,
|
||||
);
|
||||
expect(stepCodes).not.toContain('packing');
|
||||
expect(stepCodes).not.toContain('shipping');
|
||||
expect(stepCodes).toContain('printer_resin');
|
||||
});
|
||||
|
||||
it('skips generation when tasks already exist', async () => {
|
||||
const { tx } = buildMockTx({
|
||||
existingCount: 3,
|
||||
toothProsthesisRows: [],
|
||||
prosthesisTypes: [],
|
||||
});
|
||||
|
||||
const count = await generateLabCaseTasks(tx as never, 'lab-case-3', 'en');
|
||||
|
||||
expect(count).toBe(0);
|
||||
expect(tx.labCaseTask.createMany).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
@@ -1,84 +1,83 @@
|
||||
import { LabTaskStatus, Prisma } from '@prisma/client';
|
||||
import { normalizeTeeth } from '../treatments/treatment.utils';
|
||||
import { CatalogEntityKind, LabTaskStatus, Prisma } from '@prisma/client';
|
||||
import { normalizeCatalogLocale } from '../catalog/catalog-label.service';
|
||||
|
||||
type TransactionClient = Prisma.TransactionClient;
|
||||
|
||||
export async function generateLabCaseTasks(
|
||||
tx: TransactionClient,
|
||||
labCaseId: string,
|
||||
localeInput?: string | null,
|
||||
): Promise<number> {
|
||||
const existingCount = await tx.labCaseTask.count({ where: { labCaseId } });
|
||||
if (existingCount > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labCase = await tx.labCase.findUnique({
|
||||
where: { id: labCaseId },
|
||||
const locale = normalizeCatalogLocale(localeInput);
|
||||
|
||||
const toothProsthesisRows = await tx.labCaseToothProsthesis.findMany({
|
||||
where: { labCaseId },
|
||||
include: {
|
||||
details: {
|
||||
include: {
|
||||
detail: {
|
||||
select: { id: true, treatmentType: true, teeth: true },
|
||||
},
|
||||
},
|
||||
detail: { select: { id: true, treatmentType: true } },
|
||||
},
|
||||
});
|
||||
|
||||
if (toothProsthesisRows.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const prosthesisCodes = [...new Set(toothProsthesisRows.map((r) => r.prosthesisTypeCode))];
|
||||
|
||||
const prosthesisTypes = await tx.prosthesisType.findMany({
|
||||
where: { code: { in: prosthesisCodes }, isActive: true },
|
||||
include: {
|
||||
steps: {
|
||||
orderBy: { stepOrder: 'asc' },
|
||||
include: { labWorkflowStep: { select: { code: true } } },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!labCase?.details.length) {
|
||||
return 0;
|
||||
}
|
||||
const stepsByProsthesisCode = new Map(
|
||||
prosthesisTypes.map((type) => [
|
||||
type.code,
|
||||
type.steps.map((s) => ({
|
||||
stepOrder: s.stepOrder,
|
||||
workflowStepCode: s.labWorkflowStep.code,
|
||||
})),
|
||||
]),
|
||||
);
|
||||
|
||||
const treatmentTypeCodes = [...new Set(labCase.details.map((d) => d.detail.treatmentType))];
|
||||
const allStepCodes = [
|
||||
...new Set(
|
||||
prosthesisTypes.flatMap((type) =>
|
||||
type.steps.map((s) => s.labWorkflowStep.code),
|
||||
),
|
||||
),
|
||||
];
|
||||
|
||||
const labDependentTypes = await tx.treatmentType.findMany({
|
||||
where: { code: { in: treatmentTypeCodes }, labDependent: true },
|
||||
select: { id: true, code: true },
|
||||
});
|
||||
|
||||
if (labDependentTypes.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const labDependentCodes = new Set(labDependentTypes.map((t) => t.code));
|
||||
|
||||
const workflowSteps = await tx.treatmentWorkflowStep.findMany({
|
||||
where: { treatmentTypeId: { in: labDependentTypes.map((t) => t.id) } },
|
||||
orderBy: [{ treatmentTypeId: 'asc' }, { stepOrder: 'asc' }],
|
||||
include: { treatmentType: { select: { code: true } } },
|
||||
});
|
||||
|
||||
const stepsByTypeCode = new Map<string, { stepOrder: number; label: string }[]>();
|
||||
for (const step of workflowSteps) {
|
||||
const code = step.treatmentType.code;
|
||||
const list = stepsByTypeCode.get(code) ?? [];
|
||||
list.push({ stepOrder: step.stepOrder, label: step.label });
|
||||
stepsByTypeCode.set(code, list);
|
||||
}
|
||||
const stepLabels = await resolveStepLabels(tx, allStepCodes, locale);
|
||||
|
||||
const taskRows: Prisma.LabCaseTaskCreateManyInput[] = [];
|
||||
|
||||
for (const link of labCase.details) {
|
||||
const detail = link.detail;
|
||||
if (!labDependentCodes.has(detail.treatmentType)) {
|
||||
for (const row of toothProsthesisRows) {
|
||||
const typeSteps = stepsByProsthesisCode.get(row.prosthesisTypeCode) ?? [];
|
||||
if (typeSteps.length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const teeth = normalizeTeeth(detail.teeth);
|
||||
const typeSteps = stepsByTypeCode.get(detail.treatmentType) ?? [];
|
||||
|
||||
for (const tooth of teeth) {
|
||||
for (const step of typeSteps) {
|
||||
taskRows.push({
|
||||
labCaseId,
|
||||
treatmentDetailId: detail.id,
|
||||
tooth,
|
||||
treatmentType: detail.treatmentType,
|
||||
stepOrder: step.stepOrder,
|
||||
stepLabel: step.label,
|
||||
status: LabTaskStatus.PENDING,
|
||||
});
|
||||
}
|
||||
for (const step of typeSteps) {
|
||||
taskRows.push({
|
||||
labCaseId,
|
||||
treatmentDetailId: row.treatmentDetailId,
|
||||
tooth: row.tooth,
|
||||
treatmentType: row.detail.treatmentType,
|
||||
prosthesisTypeCode: row.prosthesisTypeCode,
|
||||
workflowStepCode: step.workflowStepCode,
|
||||
stepOrder: step.stepOrder,
|
||||
stepLabel: stepLabels.get(step.workflowStepCode) ?? step.workflowStepCode,
|
||||
status: LabTaskStatus.PENDING,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,3 +88,37 @@ export async function generateLabCaseTasks(
|
||||
await tx.labCaseTask.createMany({ data: taskRows });
|
||||
return taskRows.length;
|
||||
}
|
||||
|
||||
async function resolveStepLabels(
|
||||
tx: TransactionClient,
|
||||
stepCodes: string[],
|
||||
locale: string,
|
||||
): Promise<Map<string, string>> {
|
||||
if (stepCodes.length === 0) {
|
||||
return new Map();
|
||||
}
|
||||
|
||||
const rows = await tx.catalogTranslation.findMany({
|
||||
where: {
|
||||
entityKind: CatalogEntityKind.LAB_WORKFLOW_STEP,
|
||||
entityCode: { in: stepCodes },
|
||||
locale: { in: [locale, 'en'] },
|
||||
},
|
||||
select: { entityCode: true, locale: true, label: true },
|
||||
});
|
||||
|
||||
const byCode = new Map<string, { en?: string; locale?: string }>();
|
||||
for (const row of rows) {
|
||||
const entry = byCode.get(row.entityCode) ?? {};
|
||||
if (row.locale === 'en') entry.en = row.label;
|
||||
if (row.locale === locale) entry.locale = row.label;
|
||||
byCode.set(row.entityCode, entry);
|
||||
}
|
||||
|
||||
const result = new Map<string, string>();
|
||||
for (const code of stepCodes) {
|
||||
const entry = byCode.get(code);
|
||||
result.set(code, entry?.locale ?? entry?.en ?? code);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user