Files
app/src/utils/planAuditDescription.ts
BOHA a55356d417 feat(plan): validate dynamic category keys, resolve audit label from DB
- 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>
2026-06-06 00:41:50 +02:00

91 lines
3.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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(" · ");
}