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:
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import FormModal from "./FormModal";
|
import Box from "@mui/material/Box";
|
||||||
|
import { Modal, TextField, Button } from "../ui";
|
||||||
import { useApiMutation } from "../lib/queries/mutations";
|
import { useApiMutation } from "../lib/queries/mutations";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import { PlanCategory } from "../lib/queries/plan";
|
import { PlanCategory } from "../lib/queries/plan";
|
||||||
@@ -91,20 +92,26 @@ export default function PlanCategoriesModal({
|
|||||||
const busy =
|
const busy =
|
||||||
createCat.isPending || updateCat.isPending || deleteCat.isPending;
|
createCat.isPending || updateCat.isPending || deleteCat.isPending;
|
||||||
|
|
||||||
|
// Shared layout for a category row: color swatch | text input | actions.
|
||||||
|
const rowSx = {
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: 1,
|
||||||
|
py: 1,
|
||||||
|
} as const;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormModal
|
<Modal
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
|
onSubmit={onClose}
|
||||||
title="Správa kategorií"
|
title="Správa kategorií"
|
||||||
hideFooter
|
submitText="Zavřít"
|
||||||
size="md"
|
maxWidth="md"
|
||||||
>
|
>
|
||||||
<div className="plan-cat-manager">
|
<Box>
|
||||||
{categories.map((c) => (
|
{categories.map((c) => (
|
||||||
<div
|
<Box key={c.id} sx={{ ...rowSx, opacity: c.is_active ? 1 : 0.55 }}>
|
||||||
key={c.id}
|
|
||||||
className={`plan-cat-row${c.is_active ? "" : " plan-cat-row--inactive"}`}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
className="plan-cat-color"
|
className="plan-cat-color"
|
||||||
@@ -122,62 +129,66 @@ export default function PlanCategoriesModal({
|
|||||||
}}
|
}}
|
||||||
aria-label={`Barva – ${c.label}`}
|
aria-label={`Barva – ${c.label}`}
|
||||||
/>
|
/>
|
||||||
<input
|
<TextField
|
||||||
type="text"
|
|
||||||
className="admin-form-input"
|
|
||||||
defaultValue={c.label}
|
defaultValue={c.label}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onBlur={(e) => {
|
onBlur={(e) => {
|
||||||
const v = e.target.value.trim();
|
const v = e.target.value.trim();
|
||||||
if (v && v !== c.label) patch(c.id, { label: v });
|
if (v && v !== c.label) patch(c.id, { label: v });
|
||||||
}}
|
}}
|
||||||
aria-label={`Název – ${c.label}`}
|
inputProps={{ "aria-label": `Název – ${c.label}` }}
|
||||||
/>
|
/>
|
||||||
{confirmDeleteId === c.id ? (
|
{confirmDeleteId === c.id ? (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="contained"
|
||||||
className="admin-btn admin-btn-danger admin-btn-sm"
|
color="error"
|
||||||
|
size="small"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => handleDelete(c.id)}
|
onClick={() => handleDelete(c.id)}
|
||||||
>
|
>
|
||||||
Opravdu smazat
|
Opravdu smazat
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="outlined"
|
||||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
color="inherit"
|
||||||
|
size="small"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => setConfirmDeleteId(null)}
|
onClick={() => setConfirmDeleteId(null)}
|
||||||
>
|
>
|
||||||
Zrušit
|
Zrušit
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="outlined"
|
||||||
className="admin-btn admin-btn-secondary admin-btn-sm"
|
color="inherit"
|
||||||
|
size="small"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => patch(c.id, { is_active: !c.is_active })}
|
onClick={() => patch(c.id, { is_active: !c.is_active })}
|
||||||
>
|
>
|
||||||
{c.is_active ? "Skrýt" : "Obnovit"}
|
{c.is_active ? "Skrýt" : "Obnovit"}
|
||||||
</button>
|
</Button>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="contained"
|
||||||
className="admin-btn admin-btn-danger admin-btn-sm plan-cat-delete"
|
color="error"
|
||||||
|
size="small"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={() => setConfirmDeleteId(c.id)}
|
onClick={() => setConfirmDeleteId(c.id)}
|
||||||
aria-label={`Smazat – ${c.label}`}
|
aria-label={`Smazat – ${c.label}`}
|
||||||
title="Smazat kategorii"
|
title="Smazat kategorii"
|
||||||
>
|
>
|
||||||
Smazat
|
Smazat
|
||||||
</button>
|
</Button>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="plan-cat-row plan-cat-row--new">
|
<Box
|
||||||
|
sx={{ ...rowSx, mt: 1, pt: 2, borderTop: 1, borderColor: "divider" }}
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
type="color"
|
type="color"
|
||||||
className="plan-cat-color"
|
className="plan-cat-color"
|
||||||
@@ -186,25 +197,24 @@ export default function PlanCategoriesModal({
|
|||||||
onChange={(e) => setNewColor(e.target.value)}
|
onChange={(e) => setNewColor(e.target.value)}
|
||||||
aria-label="Barva nové kategorie"
|
aria-label="Barva nové kategorie"
|
||||||
/>
|
/>
|
||||||
<input
|
<TextField
|
||||||
type="text"
|
|
||||||
className="admin-form-input"
|
|
||||||
placeholder="Nová kategorie…"
|
placeholder="Nová kategorie…"
|
||||||
value={newLabel}
|
value={newLabel}
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onChange={(e) => setNewLabel(e.target.value)}
|
onChange={(e) => setNewLabel(e.target.value)}
|
||||||
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
||||||
/>
|
/>
|
||||||
<button
|
<Button
|
||||||
type="button"
|
variant="contained"
|
||||||
className="admin-btn admin-btn-primary admin-btn-sm"
|
color="primary"
|
||||||
|
size="small"
|
||||||
disabled={busy}
|
disabled={busy}
|
||||||
onClick={handleAdd}
|
onClick={handleAdd}
|
||||||
>
|
>
|
||||||
Přidat
|
Přidat
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</Box>
|
||||||
</div>
|
</Box>
|
||||||
</FormModal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
import FormModal from "./FormModal";
|
import Box from "@mui/material/Box";
|
||||||
import FormField from "./FormField";
|
import Typography from "@mui/material/Typography";
|
||||||
import AdminDatePicker from "./AdminDatePicker";
|
import {
|
||||||
import ConfirmModal from "./ConfirmModal";
|
Modal,
|
||||||
|
ConfirmDialog,
|
||||||
|
Field,
|
||||||
|
TextField,
|
||||||
|
Select,
|
||||||
|
DateField,
|
||||||
|
CheckboxField,
|
||||||
|
Button,
|
||||||
|
Card,
|
||||||
|
} from "../ui";
|
||||||
import useReducedMotion from "../hooks/useReducedMotion";
|
import useReducedMotion from "../hooks/useReducedMotion";
|
||||||
import {
|
import {
|
||||||
ResolvedCell,
|
ResolvedCell,
|
||||||
@@ -77,6 +86,26 @@ interface Props {
|
|||||||
onSwitchToEditRange: (entryId: number, userId: number, date: string) => void;
|
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) {
|
export default function PlanCellModal(props: Props) {
|
||||||
const { open, mode, onClose } = props;
|
const { open, mode, onClose } = props;
|
||||||
if (!open || mode.kind === "closed") return null;
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormModal
|
<Modal
|
||||||
isOpen
|
isOpen
|
||||||
title={title}
|
title={title}
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
onSubmit={handleSubmit}
|
onSubmit={handleSubmit}
|
||||||
submitLabel={mode.kind === "create" ? "Vytvořit" : "Uložit"}
|
submitText={mode.kind === "create" ? "Vytvořit" : "Uložit"}
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
footerLeft={
|
>
|
||||||
mode.kind !== "create" ? (
|
{mode.kind !== "create" && (
|
||||||
<button
|
<Box sx={{ mb: 2 }}>
|
||||||
type="button"
|
<Button
|
||||||
className="admin-btn admin-btn-secondary"
|
variant="outlined"
|
||||||
|
color="error"
|
||||||
|
startIcon={TrashIcon}
|
||||||
onClick={() => setConfirmDelete(true)}
|
onClick={() => setConfirmDelete(true)}
|
||||||
disabled={submitting}
|
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
|
Smazat
|
||||||
</button>
|
</Button>
|
||||||
) : null
|
</Box>
|
||||||
}
|
)}
|
||||||
>
|
<Field label="Datum od">
|
||||||
<FormField label="Datum od">
|
<DateField
|
||||||
<AdminDatePicker
|
|
||||||
value={dateFrom}
|
value={dateFrom}
|
||||||
onChange={setDateFrom}
|
onChange={setDateFrom}
|
||||||
disabled={isOverride}
|
disabled={isOverride}
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</Field>
|
||||||
{!isOverride && (
|
{!isOverride && (
|
||||||
<FormField label="Rozsah dnů">
|
<Field label="Rozsah dnů">
|
||||||
<label className="admin-form-checkbox">
|
<CheckboxField
|
||||||
<input
|
label="Více dní"
|
||||||
type="checkbox"
|
checked={isRange}
|
||||||
checked={isRange}
|
onChange={setIsRange}
|
||||||
onChange={(e) => setIsRange(e.target.checked)}
|
/>
|
||||||
/>
|
</Field>
|
||||||
<span>Více dní</span>
|
|
||||||
</label>
|
|
||||||
</FormField>
|
|
||||||
)}
|
)}
|
||||||
{!isOverride && isRange && (
|
{!isOverride && isRange && (
|
||||||
<FormField label="Datum do">
|
<Field label="Datum do">
|
||||||
<AdminDatePicker value={dateTo} onChange={setDateTo} />
|
<DateField value={dateTo} onChange={setDateTo} />
|
||||||
</FormField>
|
</Field>
|
||||||
)}
|
)}
|
||||||
<FormField label="Projekt">
|
<Field label="Projekt">
|
||||||
<select
|
<Select
|
||||||
className="admin-form-select"
|
value={projectId === null ? "" : String(projectId)}
|
||||||
value={projectId ?? ""}
|
onChange={(val) => setProjectId(val ? Number(val) : null)}
|
||||||
onChange={(e) =>
|
options={[
|
||||||
setProjectId(e.target.value ? Number(e.target.value) : null)
|
{ value: "", label: "— bez projektu —" },
|
||||||
}
|
...projects.map((p) => ({
|
||||||
>
|
value: String(p.id),
|
||||||
<option value="">— bez projektu —</option>
|
label: p.name,
|
||||||
{projects.map((p) => (
|
})),
|
||||||
<option key={p.id} value={p.id}>
|
]}
|
||||||
{p.name}
|
/>
|
||||||
</option>
|
</Field>
|
||||||
))}
|
<Field label="Kategorie">
|
||||||
</select>
|
<Select
|
||||||
</FormField>
|
|
||||||
<FormField label="Kategorie">
|
|
||||||
<select
|
|
||||||
className="admin-form-select"
|
|
||||||
value={category}
|
value={category}
|
||||||
onChange={(e) => setCategory(e.target.value)}
|
onChange={(val) => setCategory(val)}
|
||||||
>
|
options={[
|
||||||
{/* If the row's current category was retired/hidden, still show it
|
...(showRetiredCategory
|
||||||
(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. */}
|
value: category,
|
||||||
{category && !activeCategories.some((c) => c.key === category) && (
|
label: `${planCategoryLabel(
|
||||||
<option value={category}>
|
category,
|
||||||
{planCategoryLabel(
|
Object.fromEntries(
|
||||||
category,
|
props.categories.map((x) => [x.key, x]),
|
||||||
Object.fromEntries(props.categories.map((x) => [x.key, x])),
|
),
|
||||||
)}{" "}
|
)} (skrytá)`,
|
||||||
(skrytá)
|
},
|
||||||
</option>
|
]
|
||||||
)}
|
: []),
|
||||||
{activeCategories.map((c) => (
|
...activeCategories.map((c) => ({
|
||||||
<option key={c.key} value={c.key}>
|
value: c.key,
|
||||||
{c.label}
|
label: c.label,
|
||||||
</option>
|
})),
|
||||||
))}
|
]}
|
||||||
</select>
|
/>
|
||||||
</FormField>
|
</Field>
|
||||||
<FormField label="Text poznámky (volitelný)">
|
<Field label="Text poznámky (volitelný)">
|
||||||
<textarea
|
<TextField
|
||||||
className="admin-form-textarea"
|
multiline
|
||||||
|
minRows={3}
|
||||||
value={note}
|
value={note}
|
||||||
onChange={(e) => setNote(e.target.value)}
|
onChange={(e) => setNote(e.target.value)}
|
||||||
maxLength={500}
|
inputProps={{ maxLength: 500 }}
|
||||||
rows={3}
|
|
||||||
/>
|
/>
|
||||||
</FormField>
|
</Field>
|
||||||
</FormModal>
|
</Modal>
|
||||||
|
|
||||||
<ConfirmModal
|
<ConfirmDialog
|
||||||
isOpen={confirmDelete}
|
isOpen={confirmDelete}
|
||||||
title="Smazat záznam plánu?"
|
title="Smazat záznam plánu?"
|
||||||
message="Tato akce je nevratná (záznam bude soft-delete)."
|
message="Tato akce je nevratná (záznam bude soft-delete)."
|
||||||
confirmText="Smazat"
|
confirmText="Smazat"
|
||||||
type="danger"
|
|
||||||
confirmVariant="danger"
|
confirmVariant="danger"
|
||||||
loading={submitting}
|
loading={submitting}
|
||||||
onClose={() => setConfirmDelete(false)}
|
onClose={() => setConfirmDelete(false)}
|
||||||
@@ -376,26 +388,26 @@ function DayInRangeModal(
|
|||||||
];
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FormModal
|
<Modal
|
||||||
isOpen
|
isOpen
|
||||||
title="Den je součástí rozsahu"
|
title="Den je součástí rozsahu"
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
hideFooter
|
onSubmit={onClose}
|
||||||
size="md"
|
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{" "}
|
Den <strong>{mode.date}</strong> je součástí rozsahu{" "}
|
||||||
<strong>
|
<strong>
|
||||||
{mode.range.date_from} – {mode.range.date_to}
|
{mode.range.date_from} – {mode.range.date_to}
|
||||||
</strong>
|
</strong>
|
||||||
. Vyberte, co chcete udělat:
|
. Vyberte, co chcete udělat:
|
||||||
</p>
|
</Typography>
|
||||||
<ul className="plan-choices">
|
<Box sx={{ display: "flex", flexDirection: "column", gap: 1.5 }}>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{choices.map((c, i) => (
|
{choices.map((c, i) => (
|
||||||
<motion.li
|
<motion.div
|
||||||
key={c.key}
|
key={c.key}
|
||||||
className={`plan-choice plan-choice-${c.tone}`}
|
|
||||||
initial={reducedMotion ? false : { opacity: 0, y: 8 }}
|
initial={reducedMotion ? false : { opacity: 0, y: 8 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
exit={reducedMotion ? undefined : { opacity: 0, y: -4 }}
|
exit={reducedMotion ? undefined : { opacity: 0, y: -4 }}
|
||||||
@@ -405,35 +417,42 @@ function DayInRangeModal(
|
|||||||
ease: [0.16, 1, 0.3, 1],
|
ease: [0.16, 1, 0.3, 1],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="plan-choice-body">
|
<Card
|
||||||
<div className="plan-choice-title">{c.title}</div>
|
variant="outlined"
|
||||||
<div className="plan-choice-desc">{c.desc}</div>
|
sx={{
|
||||||
</div>
|
borderColor:
|
||||||
<button
|
c.tone === "primary" ? "primary.main" : "divider",
|
||||||
type="button"
|
}}
|
||||||
className={
|
|
||||||
c.tone === "primary"
|
|
||||||
? "admin-btn admin-btn-primary"
|
|
||||||
: "admin-btn admin-btn-secondary"
|
|
||||||
}
|
|
||||||
onClick={c.onClick}
|
|
||||||
>
|
>
|
||||||
{c.cta}
|
<Box
|
||||||
</button>
|
sx={{
|
||||||
</motion.li>
|
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>
|
</AnimatePresence>
|
||||||
</ul>
|
</Box>
|
||||||
<div className="plan-modal-actions plan-modal-actions--solo">
|
</Modal>
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
onClick={onClose}
|
|
||||||
>
|
|
||||||
Zavřít — ponechat rozsah beze změny
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</FormModal>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,29 +473,35 @@ function ViewModal(props: Props) {
|
|||||||
: fallback.name
|
: fallback.name
|
||||||
: null);
|
: null);
|
||||||
return (
|
return (
|
||||||
<FormModal isOpen title="Detail plánu" onClose={onClose} hideFooter>
|
<Modal
|
||||||
<p>
|
isOpen
|
||||||
|
title="Detail plánu"
|
||||||
|
onClose={onClose}
|
||||||
|
onSubmit={onClose}
|
||||||
|
submitText="Zavřít"
|
||||||
|
>
|
||||||
|
<Typography sx={{ mb: 1 }}>
|
||||||
<strong>Datum:</strong> {formatDate(c.shift_date)}
|
<strong>Datum:</strong> {formatDate(c.shift_date)}
|
||||||
</p>
|
</Typography>
|
||||||
<p>
|
<Typography sx={{ mb: 1 }}>
|
||||||
<strong>Kategorie:</strong>{" "}
|
<strong>Kategorie:</strong>{" "}
|
||||||
{planCategoryLabel(
|
{planCategoryLabel(
|
||||||
c.category,
|
c.category,
|
||||||
Object.fromEntries(categories.map((x) => [x.key, x])),
|
Object.fromEntries(categories.map((x) => [x.key, x])),
|
||||||
)}
|
)}
|
||||||
</p>
|
</Typography>
|
||||||
<p>
|
<Typography sx={{ mb: 1 }}>
|
||||||
<strong>Projekt:</strong> {projectLabel || "—"}
|
<strong>Projekt:</strong> {projectLabel || "—"}
|
||||||
</p>
|
</Typography>
|
||||||
<p>
|
<Typography sx={{ mb: 1 }}>
|
||||||
<strong>Text:</strong> {c.note || "—"}
|
<strong>Text:</strong> {c.note || "—"}
|
||||||
</p>
|
</Typography>
|
||||||
{c.rangeFrom && c.rangeTo && (
|
{c.rangeFrom && c.rangeTo && (
|
||||||
<p>
|
<Typography sx={{ mb: 1 }}>
|
||||||
<strong>Patří do rozsahu:</strong> {formatDate(c.rangeFrom)} –{" "}
|
<strong>Patří do rozsahu:</strong> {formatDate(c.rangeFrom)} –{" "}
|
||||||
{formatDate(c.rangeTo)}
|
{formatDate(c.rangeTo)}
|
||||||
</p>
|
</Typography>
|
||||||
)}
|
)}
|
||||||
</FormModal>
|
</Modal>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { useState, useMemo, useCallback, useRef, useEffect } from "react";
|
import { useState, useMemo, useCallback, useRef, useEffect } from "react";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { motion, AnimatePresence } from "framer-motion";
|
import { motion, AnimatePresence } from "framer-motion";
|
||||||
|
import Box from "@mui/material/Box";
|
||||||
|
import Typography from "@mui/material/Typography";
|
||||||
import { useAuth } from "../context/AuthContext";
|
import { useAuth } from "../context/AuthContext";
|
||||||
import { useAlert } from "../context/AlertContext";
|
import { useAlert } from "../context/AlertContext";
|
||||||
import {
|
import {
|
||||||
@@ -13,6 +15,7 @@ import PlanGrid from "../components/PlanGrid";
|
|||||||
import PlanCellModal from "../components/PlanCellModal";
|
import PlanCellModal from "../components/PlanCellModal";
|
||||||
import PlanCategoriesModal from "../components/PlanCategoriesModal";
|
import PlanCategoriesModal from "../components/PlanCategoriesModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
|
import { Button, Alert } from "../ui";
|
||||||
import { projectListOptions, type Project } from "../lib/queries/projects";
|
import { projectListOptions, type Project } from "../lib/queries/projects";
|
||||||
import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan";
|
import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan";
|
||||||
// plan.css is imported once globally in AdminApp.tsx — no per-page re-import.
|
// plan.css is imported once globally in AdminApp.tsx — no per-page re-import.
|
||||||
@@ -542,120 +545,129 @@ export default function PlanWork() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="plan-work-page">
|
<Box className="plan-work-page">
|
||||||
<motion.h1
|
<motion.div
|
||||||
className="admin-page-title"
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25 }}
|
transition={{ duration: 0.25 }}
|
||||||
>
|
>
|
||||||
Plán prací
|
<Typography variant="h4" sx={{ fontWeight: 700, mb: 2 }}>
|
||||||
</motion.h1>
|
Plán prací
|
||||||
|
</Typography>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
{!canEdit && (
|
{!canEdit && (
|
||||||
<motion.div
|
<motion.div
|
||||||
className="plan-banner"
|
|
||||||
role="status"
|
role="status"
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.06 }}
|
transition={{ duration: 0.25, delay: 0.06 }}
|
||||||
>
|
>
|
||||||
<span className="plan-banner-icon" aria-hidden>
|
<Box sx={{ mb: 2 }}>
|
||||||
i
|
<Alert severity="info">
|
||||||
</span>
|
<strong>Režim náhledu</strong> — můžete pouze prohlížet plán.
|
||||||
<span>
|
Úpravy jsou dostupné oprávněným uživatelům.
|
||||||
<strong>Režim náhledu</strong> — můžete pouze prohlížet plán. Úpravy
|
</Alert>
|
||||||
jsou dostupné oprávněným uživatelům.
|
</Box>
|
||||||
</span>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<motion.div
|
<motion.div
|
||||||
className="plan-toolbar"
|
|
||||||
initial={{ opacity: 0, y: 12 }}
|
initial={{ opacity: 0, y: 12 }}
|
||||||
animate={{ opacity: 1, y: 0 }}
|
animate={{ opacity: 1, y: 0 }}
|
||||||
transition={{ duration: 0.25, delay: 0.12 }}
|
transition={{ duration: 0.25, delay: 0.12 }}
|
||||||
>
|
>
|
||||||
{/* Nav buttons grouped so they stay on one row when the toolbar
|
<Box
|
||||||
stacks on mobile (see .plan-toolbar-nav in plan.css). On desktop
|
sx={{
|
||||||
the wrapper is an inline-flex with the same 12px gap, so the
|
display: "flex",
|
||||||
layout is visually identical. */}
|
flexWrap: "wrap",
|
||||||
<div className="plan-toolbar-nav">
|
alignItems: "center",
|
||||||
<button
|
gap: 1.5,
|
||||||
type="button"
|
mb: 2,
|
||||||
className="admin-btn admin-btn-secondary"
|
}}
|
||||||
onClick={goToToday}
|
|
||||||
>
|
|
||||||
Dnes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
onClick={goPrev}
|
|
||||||
aria-label="Předchozí"
|
|
||||||
>
|
|
||||||
←
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="admin-btn admin-btn-secondary"
|
|
||||||
onClick={goNext}
|
|
||||||
aria-label="Další"
|
|
||||||
>
|
|
||||||
→
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<span className="plan-range-label-slot">
|
|
||||||
<AnimatePresence mode="popLayout" initial={false}>
|
|
||||||
<motion.span
|
|
||||||
className="plan-range-label"
|
|
||||||
key={`${isoDate(range.from)}-${view}`}
|
|
||||||
initial={{ opacity: 0 }}
|
|
||||||
animate={{ opacity: 1 }}
|
|
||||||
exit={{ opacity: 0 }}
|
|
||||||
transition={{ duration: 0.15 }}
|
|
||||||
>
|
|
||||||
{formatRangeLabel(isoDate(range.from), isoDate(range.to), view)}
|
|
||||||
</motion.span>
|
|
||||||
</AnimatePresence>
|
|
||||||
</span>
|
|
||||||
<div
|
|
||||||
className="plan-view-toggle"
|
|
||||||
role="group"
|
|
||||||
aria-label="Měřítko zobrazení"
|
|
||||||
>
|
>
|
||||||
<button
|
{/* Nav buttons grouped so they stay on one row when the toolbar
|
||||||
type="button"
|
wraps on mobile. */}
|
||||||
className={
|
<Box sx={{ display: "inline-flex", gap: 1 }}>
|
||||||
view === "week"
|
<Button variant="outlined" color="inherit" onClick={goToToday}>
|
||||||
? "admin-btn admin-btn-primary"
|
Dnes
|
||||||
: "admin-btn admin-btn-secondary"
|
</Button>
|
||||||
}
|
<Button
|
||||||
onClick={() => setViewWithAnim("week")}
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={goPrev}
|
||||||
|
aria-label="Předchozí"
|
||||||
|
>
|
||||||
|
←
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={goNext}
|
||||||
|
aria-label="Další"
|
||||||
|
>
|
||||||
|
→
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
flex: 1,
|
||||||
|
minWidth: 220,
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "center",
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Týden
|
<AnimatePresence mode="popLayout" initial={false}>
|
||||||
</button>
|
<motion.div
|
||||||
<button
|
key={`${isoDate(range.from)}-${view}`}
|
||||||
type="button"
|
initial={{ opacity: 0 }}
|
||||||
className={
|
animate={{ opacity: 1 }}
|
||||||
view === "month"
|
exit={{ opacity: 0 }}
|
||||||
? "admin-btn admin-btn-primary"
|
transition={{ duration: 0.15 }}
|
||||||
: "admin-btn admin-btn-secondary"
|
>
|
||||||
}
|
<Typography
|
||||||
onClick={() => setViewWithAnim("month")}
|
variant="h6"
|
||||||
|
sx={{ fontWeight: 600, whiteSpace: "nowrap" }}
|
||||||
|
>
|
||||||
|
{formatRangeLabel(
|
||||||
|
isoDate(range.from),
|
||||||
|
isoDate(range.to),
|
||||||
|
view,
|
||||||
|
)}
|
||||||
|
</Typography>
|
||||||
|
</motion.div>
|
||||||
|
</AnimatePresence>
|
||||||
|
</Box>
|
||||||
|
<Box
|
||||||
|
role="group"
|
||||||
|
aria-label="Měřítko zobrazení"
|
||||||
|
sx={{ display: "inline-flex", gap: 1 }}
|
||||||
>
|
>
|
||||||
Měsíc
|
<Button
|
||||||
</button>
|
variant={view === "week" ? "contained" : "outlined"}
|
||||||
</div>
|
color={view === "week" ? "primary" : "inherit"}
|
||||||
{canEdit && (
|
onClick={() => setViewWithAnim("week")}
|
||||||
<button
|
>
|
||||||
type="button"
|
Týden
|
||||||
className="admin-btn admin-btn-secondary plan-toolbar-cat"
|
</Button>
|
||||||
onClick={() => setCatModalOpen(true)}
|
<Button
|
||||||
>
|
variant={view === "month" ? "contained" : "outlined"}
|
||||||
Správa kategorií
|
color={view === "month" ? "primary" : "inherit"}
|
||||||
</button>
|
onClick={() => setViewWithAnim("month")}
|
||||||
)}
|
>
|
||||||
|
Měsíc
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
{canEdit && (
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
color="inherit"
|
||||||
|
onClick={() => setCatModalOpen(true)}
|
||||||
|
>
|
||||||
|
Správa kategorií
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{gridLoading && (
|
{gridLoading && (
|
||||||
@@ -718,6 +730,6 @@ export default function PlanWork() {
|
|||||||
onClose={() => setCatModalOpen(false)}
|
onClose={() => setCatModalOpen(false)}
|
||||||
categories={categories}
|
categories={categories}
|
||||||
/>
|
/>
|
||||||
</div>
|
</Box>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user