refactor(api): strengthen report template persistence
This commit is contained in:
@@ -28,6 +28,63 @@ type ReportTemplateRecord = {
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
const RESOURCE_MONTH_RECOMMENDED_COLUMNS = [
|
||||
"monthKey",
|
||||
"displayName",
|
||||
"eid",
|
||||
"chapter",
|
||||
"countryCode",
|
||||
"countryName",
|
||||
"federalState",
|
||||
"metroCityName",
|
||||
"orgUnitName",
|
||||
"managementLevelGroupName",
|
||||
"monthlyBaseWorkingDays",
|
||||
"monthlyEffectiveWorkingDays",
|
||||
"monthlyBaseAvailableHours",
|
||||
"monthlyPublicHolidayCount",
|
||||
"monthlyPublicHolidayWorkdayCount",
|
||||
"monthlyPublicHolidayHoursDeduction",
|
||||
"monthlyAbsenceDayEquivalent",
|
||||
"monthlyAbsenceHoursDeduction",
|
||||
"monthlySahHours",
|
||||
"monthlyChargeabilityTargetPct",
|
||||
"monthlyTargetHours",
|
||||
"monthlyActualBookedHours",
|
||||
"monthlyExpectedBookedHours",
|
||||
"monthlyActualChargeabilityPct",
|
||||
"monthlyExpectedChargeabilityPct",
|
||||
"monthlyUnassignedHours",
|
||||
] as const;
|
||||
|
||||
const RESOURCE_MONTH_MINIMUM_AUDIT_COLUMNS = [
|
||||
"monthKey",
|
||||
"displayName",
|
||||
"countryName",
|
||||
"federalState",
|
||||
"metroCityName",
|
||||
"monthlyPublicHolidayCount",
|
||||
"monthlyPublicHolidayHoursDeduction",
|
||||
"monthlyAbsenceDayEquivalent",
|
||||
"monthlyAbsenceHoursDeduction",
|
||||
"monthlySahHours",
|
||||
"monthlyTargetHours",
|
||||
"monthlyActualBookedHours",
|
||||
"monthlyUnassignedHours",
|
||||
] as const;
|
||||
|
||||
type ResourceMonthTemplateCompleteness = {
|
||||
scope: "resource_month";
|
||||
isAuditReady: boolean;
|
||||
isRecommendedComplete: boolean;
|
||||
recommendedColumnCount: number;
|
||||
selectedRecommendedColumnCount: number;
|
||||
minimumAuditColumnCount: number;
|
||||
selectedMinimumAuditColumnCount: number;
|
||||
missingRecommendedColumns: string[];
|
||||
missingMinimumAuditColumns: string[];
|
||||
};
|
||||
|
||||
type ReportTemplateContext = Pick<TRPCContext, "db" | "dbUser">;
|
||||
|
||||
export const SaveReportTemplateInputSchema = z.object({
|
||||
@@ -75,7 +132,6 @@ export async function saveReportTemplate(
|
||||
validateReportInput(input.config);
|
||||
const payload = input.config as unknown as Prisma.InputJsonValue;
|
||||
const entity = toTemplateEntity(input.config.entity);
|
||||
const writeData = buildTemplateWriteData(input, entity, payload);
|
||||
|
||||
if (input.id) {
|
||||
const existing = await ctx.db.reportTemplate.findUnique({
|
||||
@@ -87,27 +143,32 @@ export async function saveReportTemplate(
|
||||
throw new TRPCError({ code: "FORBIDDEN", message: "Template cannot be updated" });
|
||||
}
|
||||
|
||||
return ctx.db.reportTemplate.update({
|
||||
where: { id: input.id },
|
||||
data: writeData,
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
try {
|
||||
return await ctx.db.reportTemplate.update({
|
||||
where: { id: input.id },
|
||||
data: buildTemplateUpdateData(input, entity, payload),
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
} catch (error) {
|
||||
throw mapTemplateWriteError(error, input.name);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.db.reportTemplate.upsert({
|
||||
where: {
|
||||
ownerId_name: {
|
||||
ownerId,
|
||||
name: input.name,
|
||||
try {
|
||||
return await ctx.db.reportTemplate.upsert({
|
||||
where: {
|
||||
ownerId_name: {
|
||||
ownerId,
|
||||
name: input.name,
|
||||
},
|
||||
},
|
||||
},
|
||||
update: omitNameFromTemplateWriteData(writeData),
|
||||
create: {
|
||||
ownerId,
|
||||
...writeData,
|
||||
},
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
update: buildTemplateUpsertUpdateData(input, entity, payload),
|
||||
create: buildTemplateCreateData(ownerId, input, entity, payload),
|
||||
select: { id: true, updatedAt: true },
|
||||
});
|
||||
} catch (error) {
|
||||
throw mapTemplateWriteError(error, input.name);
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteReportTemplate(
|
||||
@@ -128,40 +189,139 @@ export async function deleteReportTemplate(
|
||||
return { ok: true };
|
||||
}
|
||||
|
||||
function buildTemplateWriteData(
|
||||
function normalizeTemplateDescription(description: string | undefined): string | null | undefined {
|
||||
if (description === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
return description.length > 0 ? description : null;
|
||||
}
|
||||
|
||||
function mapTemplateWriteError(error: unknown, templateName: string): TRPCError {
|
||||
if (
|
||||
typeof error === "object"
|
||||
&& error !== null
|
||||
&& "code" in error
|
||||
&& error.code === "P2002"
|
||||
) {
|
||||
return new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: `A report template named "${templateName}" already exists`,
|
||||
});
|
||||
}
|
||||
|
||||
if (error instanceof TRPCError) {
|
||||
return error;
|
||||
}
|
||||
|
||||
return new TRPCError({
|
||||
code: "INTERNAL_SERVER_ERROR",
|
||||
message: "Failed to save report template",
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
|
||||
function buildTemplateCreateData(
|
||||
ownerId: string,
|
||||
input: z.infer<typeof SaveReportTemplateInputSchema>,
|
||||
entity: ReportTemplateEntity,
|
||||
config: Prisma.InputJsonValue,
|
||||
) {
|
||||
return {
|
||||
) : Prisma.ReportTemplateUncheckedCreateInput {
|
||||
const description = normalizeTemplateDescription(input.description);
|
||||
const data: Prisma.ReportTemplateUncheckedCreateInput = {
|
||||
ownerId,
|
||||
name: input.name,
|
||||
entity,
|
||||
config,
|
||||
isShared: input.isShared,
|
||||
...(input.description !== undefined ? { description: input.description } : {}),
|
||||
};
|
||||
|
||||
if (description !== undefined) {
|
||||
data.description = description;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function omitNameFromTemplateWriteData(
|
||||
data: ReturnType<typeof buildTemplateWriteData>,
|
||||
) {
|
||||
const { name: _name, ...updateData } = data;
|
||||
return updateData;
|
||||
function buildTemplateUpdateData(
|
||||
input: z.infer<typeof SaveReportTemplateInputSchema>,
|
||||
entity: ReportTemplateEntity,
|
||||
config: Prisma.InputJsonValue,
|
||||
): Prisma.ReportTemplateUncheckedUpdateInput {
|
||||
const description = normalizeTemplateDescription(input.description);
|
||||
const data: Prisma.ReportTemplateUncheckedUpdateInput = {
|
||||
name: input.name,
|
||||
entity,
|
||||
config,
|
||||
isShared: input.isShared,
|
||||
};
|
||||
|
||||
if (description !== undefined) {
|
||||
data.description = description;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function buildTemplateUpsertUpdateData(
|
||||
input: z.infer<typeof SaveReportTemplateInputSchema>,
|
||||
entity: ReportTemplateEntity,
|
||||
config: Prisma.InputJsonValue,
|
||||
): Prisma.ReportTemplateUncheckedUpdateInput {
|
||||
const description = normalizeTemplateDescription(input.description);
|
||||
const data: Prisma.ReportTemplateUncheckedUpdateInput = {
|
||||
entity,
|
||||
config,
|
||||
isShared: input.isShared,
|
||||
};
|
||||
|
||||
if (description !== undefined) {
|
||||
data.description = description;
|
||||
}
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
function mapTemplateRecord(template: ReportTemplateRecord, ownerId: string) {
|
||||
const config = ReportTemplateConfigSchema.parse(template.config);
|
||||
return {
|
||||
id: template.id,
|
||||
name: template.name,
|
||||
description: template.description,
|
||||
entity: fromTemplateEntity(template.entity),
|
||||
config: ReportTemplateConfigSchema.parse(template.config),
|
||||
config,
|
||||
isShared: template.isShared,
|
||||
isOwner: template.ownerId === ownerId,
|
||||
completeness: getTemplateCompleteness(config),
|
||||
updatedAt: template.updatedAt,
|
||||
};
|
||||
}
|
||||
|
||||
function getTemplateCompleteness(
|
||||
config: z.infer<typeof ReportTemplateConfigSchema>,
|
||||
): ResourceMonthTemplateCompleteness | null {
|
||||
if (config.entity !== "resource_month") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const selectedColumns = new Set(config.columns);
|
||||
const missingRecommendedColumns = RESOURCE_MONTH_RECOMMENDED_COLUMNS
|
||||
.filter((column) => !selectedColumns.has(column));
|
||||
const missingMinimumAuditColumns = RESOURCE_MONTH_MINIMUM_AUDIT_COLUMNS
|
||||
.filter((column) => !selectedColumns.has(column));
|
||||
|
||||
return {
|
||||
scope: "resource_month",
|
||||
isAuditReady: missingMinimumAuditColumns.length === 0,
|
||||
isRecommendedComplete: missingRecommendedColumns.length === 0,
|
||||
recommendedColumnCount: RESOURCE_MONTH_RECOMMENDED_COLUMNS.length,
|
||||
selectedRecommendedColumnCount: RESOURCE_MONTH_RECOMMENDED_COLUMNS.length - missingRecommendedColumns.length,
|
||||
minimumAuditColumnCount: RESOURCE_MONTH_MINIMUM_AUDIT_COLUMNS.length,
|
||||
selectedMinimumAuditColumnCount: RESOURCE_MONTH_MINIMUM_AUDIT_COLUMNS.length - missingMinimumAuditColumns.length,
|
||||
missingRecommendedColumns,
|
||||
missingMinimumAuditColumns,
|
||||
};
|
||||
}
|
||||
|
||||
function toTemplateEntity(entity: EntityKey): ReportTemplateEntity {
|
||||
switch (entity) {
|
||||
case "resource":
|
||||
|
||||
Reference in New Issue
Block a user