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

@@ -1,9 +1,17 @@
import { useState } from "react";
import { motion, AnimatePresence } from "framer-motion";
import FormModal from "./FormModal";
import FormField from "./FormField";
import AdminDatePicker from "./AdminDatePicker";
import ConfirmModal from "./ConfirmModal";
import { ResolvedCell } from "../lib/queries/plan";
import useReducedMotion from "../hooks/useReducedMotion";
import {
ResolvedCell,
PLAN_CATEGORIES,
planCategoryLabel,
cellProjectLabel,
} from "../lib/queries/plan";
import { formatDate } from "../utils/formatters";
interface Project {
id: number;
@@ -68,16 +76,6 @@ interface Props {
onSwitchToEditRange: (entryId: number, userId: number, date: string) => void;
}
const CATEGORIES = [
{ value: "work", label: "Práce" },
{ value: "preparation", label: "Příprava" },
{ value: "travel", label: "Cesta / Montáž" },
{ value: "leave", label: "Dovolená" },
{ value: "sick", label: "Nemoc" },
{ value: "training", label: "Školení" },
{ value: "other", label: "Jiné" },
];
export default function PlanCellModal(props: Props) {
const { open, mode, onClose } = props;
if (!open || mode.kind === "closed") return null;
@@ -218,19 +216,36 @@ function EditForm(props: Props) {
onSubmit={handleSubmit}
submitLabel={mode.kind === "create" ? "Vytvořit" : "Uložit"}
loading={submitting}
>
{mode.kind !== "create" && (
<div className="plan-modal-actions">
footerLeft={
mode.kind !== "create" ? (
<button
type="button"
className="admin-btn admin-btn-danger"
className="admin-btn admin-btn-secondary"
onClick={() => setConfirmDelete(true)}
disabled={submitting}
>
<svg
width="14"
height="14"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden
>
<polyline points="3 6 5 6 21 6" />
<path d="M19 6l-1 14a2 2 0 0 1-2 2H8a2 2 0 0 1-2-2L5 6" />
<path d="M10 11v6" />
<path d="M14 11v6" />
<path d="M9 6V4a1 1 0 0 1 1-1h4a1 1 0 0 1 1 1v2" />
</svg>
Smazat
</button>
</div>
)}
) : null
}
>
<FormField label="Datum od">
<AdminDatePicker
value={dateFrom}
@@ -240,13 +255,13 @@ function EditForm(props: Props) {
</FormField>
{!isOverride && (
<FormField label="Rozsah dnů">
<label>
<label className="admin-form-checkbox">
<input
type="checkbox"
checked={isRange}
onChange={(e) => setIsRange(e.target.checked)}
/>{" "}
Více dní
/>
<span>Více dní</span>
</label>
</FormField>
)}
@@ -257,6 +272,7 @@ function EditForm(props: Props) {
)}
<FormField label="Projekt">
<select
className="admin-form-select"
value={projectId ?? ""}
onChange={(e) =>
setProjectId(e.target.value ? Number(e.target.value) : null)
@@ -272,10 +288,11 @@ function EditForm(props: Props) {
</FormField>
<FormField label="Kategorie">
<select
className="admin-form-select"
value={category}
onChange={(e) => setCategory(e.target.value)}
>
{CATEGORIES.map((c) => (
{PLAN_CATEGORIES.map((c) => (
<option key={c.value} value={c.value}>
{c.label}
</option>
@@ -284,6 +301,7 @@ function EditForm(props: Props) {
</FormField>
<FormField label="Text poznámky (volitelný)">
<textarea
className="admin-form-textarea"
value={note}
onChange={(e) => setNote(e.target.value)}
maxLength={500}
@@ -312,62 +330,88 @@ function DayInRangeModal(
) {
const { mode, onClose, onCreateOverrideFromRange, onSwitchToEditRange } =
props;
const reducedMotion = useReducedMotion();
// Two clear paths: edit the whole range (default, safe) or carve out
// a one-day override (deviates from the plan). Close = no action.
const choices = [
{
key: "edit-range",
title: "Upravit celý rozsah",
desc: `Změní projekt, kategorii nebo poznámku pro všechny dny v rozsahu ${mode.range.date_from} ${mode.range.date_to}.`,
tone: "primary" as const,
onClick: () => onSwitchToEditRange(mode.entryId, mode.userId, mode.date),
cta: "Upravit rozsah",
},
{
key: "override-day",
title: "Upravit pouze tento den",
desc: `Vytvoří přepsání pro ${mode.range.date_from === mode.date ? "tento" : mode.date}; zbytek rozsahu zůstane beze změny.`,
tone: "secondary" as const,
onClick: async () => {
await onCreateOverrideFromRange(mode.entryId, mode.userId, mode.date);
onClose();
},
cta: "Vytvořit přepsání",
},
];
return (
<FormModal
isOpen
title={`Den ${mode.date} je součástí rozsahu`}
title="Den je součástí rozsahu"
onClose={onClose}
hideFooter
size="md"
>
<p>
Tento den je součástí rozsahu:{" "}
<p className="plan-modal-intro">
Den <strong>{mode.date}</strong> je součástí rozsahu{" "}
<strong>
{mode.range.date_from} {mode.range.date_to}
</strong>
. Vyberte, co chcete udělat:
</p>
<p>Co chcete udělat?</p>
<ul>
<li>
<strong>Upravit celý rozsah</strong> otevře editor celého rozsahu.
</li>
<li>
<strong>Upravit pouze tento den</strong> vytvoří přepsání (override)
pro toto datum; zbytek rozsahu zůstane beze změny.
</li>
<li>
<strong>Zavřít</strong> ponechá rozsah beze změny.
</li>
<ul className="plan-choices">
<AnimatePresence>
{choices.map((c, i) => (
<motion.li
key={c.key}
className={`plan-choice plan-choice-${c.tone}`}
initial={reducedMotion ? false : { opacity: 0, y: 8 }}
animate={{ opacity: 1, y: 0 }}
exit={reducedMotion ? undefined : { opacity: 0, y: -4 }}
transition={{
duration: reducedMotion ? 0 : 0.22,
delay: reducedMotion ? 0 : 0.04 + i * 0.05,
ease: [0.16, 1, 0.3, 1],
}}
>
<div className="plan-choice-body">
<div className="plan-choice-title">{c.title}</div>
<div className="plan-choice-desc">{c.desc}</div>
</div>
<button
type="button"
className={
c.tone === "primary"
? "admin-btn admin-btn-primary"
: "admin-btn admin-btn-secondary"
}
onClick={c.onClick}
>
{c.cta}
</button>
</motion.li>
))}
</AnimatePresence>
</ul>
<div className="plan-modal-actions">
<div className="plan-modal-actions plan-modal-actions--solo">
<button
type="button"
className="admin-btn admin-btn-secondary"
onClick={onClose}
>
Zavřít
</button>
<button
type="button"
className="admin-btn admin-btn-primary"
onClick={async () => {
await onCreateOverrideFromRange(
mode.entryId,
mode.userId,
mode.date,
);
onClose();
}}
>
Upravit pouze tento den
</button>
<button
type="button"
className="admin-btn admin-btn-primary"
onClick={() =>
onSwitchToEditRange(mode.entryId, mode.userId, mode.date)
}
>
Upravit celý rozsah
Zavřít ponechat rozsah beze změny
</button>
</div>
</FormModal>
@@ -378,31 +422,36 @@ function ViewModal(props: Props) {
const { mode, onClose, projects } = props;
if (mode.kind !== "view") return null;
const c = mode.cell;
const project = c.project_id
// Prefer the server-embedded project label; fall back to the projects-list
// lookup only for cells that predate the embed (stale cache).
const fallback = c.project_id
? (projects.find((p) => p.id === c.project_id) ?? null)
: null;
const projectLabel =
cellProjectLabel(c) ??
(fallback
? fallback.project_number
? `${fallback.project_number}${fallback.name}`
: fallback.name
: null);
return (
<FormModal isOpen title="Detail plánu" onClose={onClose} hideFooter>
<p>
<strong>Datum:</strong> {c.shift_date}
<strong>Datum:</strong> {formatDate(c.shift_date)}
</p>
<p>
<strong>Kategorie:</strong> {c.category}
<strong>Kategorie:</strong> {planCategoryLabel(c.category)}
</p>
<p>
<strong>Projekt:</strong>{" "}
{project
? project.project_number
? `${project.project_number}${project.name}`
: project.name
: "—"}
<strong>Projekt:</strong> {projectLabel || ""}
</p>
<p>
<strong>Text:</strong> {c.note || "—"}
</p>
{c.rangeFrom && c.rangeTo && (
<p>
<strong>Patří do rozsahu:</strong> {c.rangeFrom} {c.rangeTo}
<strong>Patří do rozsahu:</strong> {formatDate(c.rangeFrom)} {" "}
{formatDate(c.rangeTo)}
</p>
)}
</FormModal>