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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user