- Replace hardcoded planCategoryEnum z.enum with z.string().trim().min(1) - Add assertActiveCategory() helper that rejects inactive/unknown keys - Add resolveCategoryLabel() helper that resolves the Czech label from plan_categories DB table, falling back to built-in map - Change PlanAuditDescriptionInput.category -> categoryLabel (pre-resolved string) - Update buildPlanAuditDescription to use opts.categoryLabel directly - Update all 6 buildPlanAuditDescription call sites in plan.service.ts to await resolveCategoryLabel(row.category) - Add category validation in createEntry, updateEntry, createOverride, updateOverride - Update planAuditDescription tests to pass categoryLabel instead of category Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
91 lines
3.3 KiB
TypeScript
91 lines
3.3 KiB
TypeScript
/**
|
||
* Human-readable Czech descriptions for work-plan audit-log rows.
|
||
*
|
||
* The audit log table ("Popis" column) shows only the `description` string,
|
||
* so it must be self-contained: who, when, what. Previously the plan routes
|
||
* left `description` undefined for normal edits, which rendered as "-" and
|
||
* made the audit rows useless. These helpers build a readable subject line
|
||
* such as: `Jan Novák · 08.06.2026 – 12.06.2026 · Práce · Projekt ABC`.
|
||
*
|
||
* Everything here is pure (no Prisma, no Date.now) so it is trivially unit
|
||
* testable. The service layer resolves user/project names and feeds plain
|
||
* strings in.
|
||
*/
|
||
|
||
/** Czech labels for the `plan_category` enum. Mirrors the frontend
|
||
* PlanCellModal options so the audit log reads the same as the UI. */
|
||
export const PLAN_CATEGORY_LABELS_CS: Record<string, string> = {
|
||
work: "Práce",
|
||
preparation: "Příprava",
|
||
travel: "Cesta / Montáž",
|
||
leave: "Dovolená",
|
||
sick: "Nemoc",
|
||
training: "Školení",
|
||
other: "Jiné",
|
||
};
|
||
|
||
/** Translate a plan category to its Czech label, falling back to the raw
|
||
* value for unknown categories so nothing is ever silently dropped. */
|
||
export function planCategoryLabel(category: string): string {
|
||
return PLAN_CATEGORY_LABELS_CS[category] ?? category;
|
||
}
|
||
|
||
/**
|
||
* Format a `YYYY-MM-DD` string as Czech `DD.MM.YYYY`.
|
||
*
|
||
* Takes a string (not a Date) on purpose: work-plan dates live in `@db.Date`
|
||
* columns and the service derives the ISO day via `toISOString().slice(0,10)`,
|
||
* so there is no timezone ambiguity to reintroduce here. Matches the
|
||
* `localDateCzStr` style in `src/utils/date.ts`.
|
||
*/
|
||
export function formatPlanDate(iso: string): string {
|
||
const [y, m, d] = iso.split("-");
|
||
if (!y || !m || !d) return iso;
|
||
return `${d}.${m}.${y}`;
|
||
}
|
||
|
||
export interface PlanAuditDescriptionInput {
|
||
/** Employee the plan row belongs to (display name). */
|
||
userName: string;
|
||
/** Already-resolved Czech category label (resolved from plan_categories). */
|
||
categoryLabel: string;
|
||
/** Resolved project name, or null when the row has no project. */
|
||
projectName?: string | null;
|
||
/** Range start (YYYY-MM-DD). For overrides, pass the shift date here. */
|
||
dateFrom: string;
|
||
/** Range end (YYYY-MM-DD). For overrides/single days, equals `dateFrom`. */
|
||
dateTo: string;
|
||
/** Append "nouzová úprava" when the change bypassed the past-date lock. */
|
||
force?: boolean;
|
||
/** Extra Czech note appended at the end (e.g. "nahrazeno novým záznamem"). */
|
||
suffix?: string;
|
||
}
|
||
|
||
/**
|
||
* Build a Czech audit description: a `·`-separated subject line. The action
|
||
* (create/update/delete) and entity type are shown in their own audit-table
|
||
* columns, so the description carries only the subject — never the verb.
|
||
*/
|
||
export function buildPlanAuditDescription(
|
||
opts: PlanAuditDescriptionInput,
|
||
): string {
|
||
const dateLabel =
|
||
opts.dateFrom === opts.dateTo
|
||
? formatPlanDate(opts.dateFrom)
|
||
: `${formatPlanDate(opts.dateFrom)} – ${formatPlanDate(opts.dateTo)}`;
|
||
|
||
const parts: string[] = [opts.userName, dateLabel, opts.categoryLabel];
|
||
|
||
if (opts.projectName && opts.projectName.trim()) {
|
||
parts.push(opts.projectName.trim());
|
||
}
|
||
if (opts.suffix && opts.suffix.trim()) {
|
||
parts.push(opts.suffix.trim());
|
||
}
|
||
if (opts.force) {
|
||
parts.push("nouzová úprava");
|
||
}
|
||
|
||
return parts.join(" · ");
|
||
}
|