fix(plan): readable audit descriptions, project labels, dashboard invalidation, view-only & sticky-column UI

Work-plan fixes from this session:
- Czech audit descriptions for plan entries/overrides (was empty '-')
- server-embedded project label on grid cells (was '-' past the 100-project cap)
- dashboard activity + audit-log cache invalidation on plan mutations
- read-only cells: no '+' on empty, keep hover on cells with a record
- view modal: Czech category + formatted dates
- sticky date column made opaque (no bleed-through on horizontal scroll)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-06 00:24:33 +02:00
parent 2f76fe0bc1
commit 4a9c283789
13 changed files with 2240 additions and 261 deletions

View File

@@ -0,0 +1,94 @@
/**
* 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;
/** `plan_category` enum value. */
category: 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,
planCategoryLabel(opts.category),
];
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(" · ");
}