feat(mui): migrate PlanWork modals + toolbar onto MUI kit (planner grid kept bespoke)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-07 01:29:48 +02:00
parent cc4b14f862
commit ebda3ecf23
3 changed files with 324 additions and 277 deletions

View File

@@ -1,9 +1,18 @@
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 Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";
import {
Modal,
ConfirmDialog,
Field,
TextField,
Select,
DateField,
CheckboxField,
Button,
Card,
} from "../ui";
import useReducedMotion from "../hooks/useReducedMotion";
import {
ResolvedCell,
@@ -77,6 +86,26 @@ interface Props {
onSwitchToEditRange: (entryId: number, userId: number, date: string) => void;
}
const TrashIcon = (
<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>
);
export default function PlanCellModal(props: Props) {
const { open, mode, onClose } = props;
if (!open || mode.kind === "closed") return null;
@@ -213,128 +242,111 @@ function EditForm(props: Props) {
}
};
// If the row's current category was retired/hidden, still show it (marked)
// so the select reflects reality instead of silently snapping to the first
// active option. The server only re-checks the category if it changes, so
// keeping it saves fine.
const showRetiredCategory =
category && !activeCategories.some((c) => c.key === category);
return (
<>
<FormModal
<Modal
isOpen
title={title}
onClose={onClose}
onSubmit={handleSubmit}
submitLabel={mode.kind === "create" ? "Vytvořit" : "Uložit"}
submitText={mode.kind === "create" ? "Vytvořit" : "Uložit"}
loading={submitting}
footerLeft={
mode.kind !== "create" ? (
<button
type="button"
className="admin-btn admin-btn-secondary"
>
{mode.kind !== "create" && (
<Box sx={{ mb: 2 }}>
<Button
variant="outlined"
color="error"
startIcon={TrashIcon}
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>
) : null
}
>
<FormField label="Datum od">
<AdminDatePicker
</Button>
</Box>
)}
<Field label="Datum od">
<DateField
value={dateFrom}
onChange={setDateFrom}
disabled={isOverride}
/>
</FormField>
</Field>
{!isOverride && (
<FormField label="Rozsah dnů">
<label className="admin-form-checkbox">
<input
type="checkbox"
checked={isRange}
onChange={(e) => setIsRange(e.target.checked)}
/>
<span>Více dní</span>
</label>
</FormField>
<Field label="Rozsah dnů">
<CheckboxField
label="Více dní"
checked={isRange}
onChange={setIsRange}
/>
</Field>
)}
{!isOverride && isRange && (
<FormField label="Datum do">
<AdminDatePicker value={dateTo} onChange={setDateTo} />
</FormField>
<Field label="Datum do">
<DateField value={dateTo} onChange={setDateTo} />
</Field>
)}
<FormField label="Projekt">
<select
className="admin-form-select"
value={projectId ?? ""}
onChange={(e) =>
setProjectId(e.target.value ? Number(e.target.value) : null)
}
>
<option value=""> bez projektu </option>
{projects.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</FormField>
<FormField label="Kategorie">
<select
className="admin-form-select"
<Field label="Projekt">
<Select
value={projectId === null ? "" : String(projectId)}
onChange={(val) => setProjectId(val ? Number(val) : null)}
options={[
{ value: "", label: "— bez projektu —" },
...projects.map((p) => ({
value: String(p.id),
label: p.name,
})),
]}
/>
</Field>
<Field label="Kategorie">
<Select
value={category}
onChange={(e) => setCategory(e.target.value)}
>
{/* If the row's current category was retired/hidden, still show it
(marked) so the select reflects reality instead of silently
snapping to the first active option. The server only re-checks
the category if it changes, so keeping it saves fine. */}
{category && !activeCategories.some((c) => c.key === category) && (
<option value={category}>
{planCategoryLabel(
category,
Object.fromEntries(props.categories.map((x) => [x.key, x])),
)}{" "}
(skrytá)
</option>
)}
{activeCategories.map((c) => (
<option key={c.key} value={c.key}>
{c.label}
</option>
))}
</select>
</FormField>
<FormField label="Text poznámky (volitelný)">
<textarea
className="admin-form-textarea"
onChange={(val) => setCategory(val)}
options={[
...(showRetiredCategory
? [
{
value: category,
label: `${planCategoryLabel(
category,
Object.fromEntries(
props.categories.map((x) => [x.key, x]),
),
)} (skrytá)`,
},
]
: []),
...activeCategories.map((c) => ({
value: c.key,
label: c.label,
})),
]}
/>
</Field>
<Field label="Text poznámky (volitelný)">
<TextField
multiline
minRows={3}
value={note}
onChange={(e) => setNote(e.target.value)}
maxLength={500}
rows={3}
inputProps={{ maxLength: 500 }}
/>
</FormField>
</FormModal>
</Field>
</Modal>
<ConfirmModal
<ConfirmDialog
isOpen={confirmDelete}
title="Smazat záznam plánu?"
message="Tato akce je nevratná (záznam bude soft-delete)."
confirmText="Smazat"
type="danger"
confirmVariant="danger"
loading={submitting}
onClose={() => setConfirmDelete(false)}
@@ -376,26 +388,26 @@ function DayInRangeModal(
];
return (
<FormModal
<Modal
isOpen
title="Den je součástí rozsahu"
onClose={onClose}
hideFooter
size="md"
onSubmit={onClose}
submitText="Zavřít — ponechat rozsah beze změny"
maxWidth="md"
>
<p className="plan-modal-intro">
<Typography variant="body2" sx={{ mb: 2 }}>
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>
<ul className="plan-choices">
</Typography>
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
<AnimatePresence>
{choices.map((c, i) => (
<motion.li
<motion.div
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 }}
@@ -405,35 +417,42 @@ function DayInRangeModal(
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}
<Card
variant="outlined"
sx={{
borderColor:
c.tone === "primary" ? "primary.main" : "divider",
}}
>
{c.cta}
</button>
</motion.li>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 2,
flexWrap: "wrap",
}}
>
<Box sx={{ flex: 1, minWidth: 200 }}>
<Typography sx={{ fontWeight: 600 }}>{c.title}</Typography>
<Typography variant="body2" color="text.secondary">
{c.desc}
</Typography>
</Box>
<Button
variant={c.tone === "primary" ? "contained" : "outlined"}
color={c.tone === "primary" ? "primary" : "inherit"}
onClick={c.onClick}
>
{c.cta}
</Button>
</Box>
</Card>
</motion.div>
))}
</AnimatePresence>
</ul>
<div className="plan-modal-actions plan-modal-actions--solo">
<button
type="button"
className="admin-btn admin-btn-secondary"
onClick={onClose}
>
Zavřít ponechat rozsah beze změny
</button>
</div>
</FormModal>
</Box>
</Modal>
);
}
@@ -454,29 +473,35 @@ function ViewModal(props: Props) {
: fallback.name
: null);
return (
<FormModal isOpen title="Detail plánu" onClose={onClose} hideFooter>
<p>
<Modal
isOpen
title="Detail plánu"
onClose={onClose}
onSubmit={onClose}
submitText="Zavřít"
>
<Typography sx={{ mb: 1 }}>
<strong>Datum:</strong> {formatDate(c.shift_date)}
</p>
<p>
</Typography>
<Typography sx={{ mb: 1 }}>
<strong>Kategorie:</strong>{" "}
{planCategoryLabel(
c.category,
Object.fromEntries(categories.map((x) => [x.key, x])),
)}
</p>
<p>
</Typography>
<Typography sx={{ mb: 1 }}>
<strong>Projekt:</strong> {projectLabel || "—"}
</p>
<p>
</Typography>
<Typography sx={{ mb: 1 }}>
<strong>Text:</strong> {c.note || "—"}
</p>
</Typography>
{c.rangeFrom && c.rangeTo && (
<p>
<Typography sx={{ mb: 1 }}>
<strong>Patří do rozsahu:</strong> {formatDate(c.rangeFrom)} {" "}
{formatDate(c.rangeTo)}
</p>
</Typography>
)}
</FormModal>
</Modal>
);
}