diff --git a/src/admin/components/PlanCategoriesModal.tsx b/src/admin/components/PlanCategoriesModal.tsx new file mode 100644 index 0000000..6852125 --- /dev/null +++ b/src/admin/components/PlanCategoriesModal.tsx @@ -0,0 +1,145 @@ +import { useState } from "react"; +import FormModal from "./FormModal"; +import { useApiMutation } from "../lib/queries/mutations"; +import { useAlert } from "../context/AlertContext"; +import { PlanCategory } from "../lib/queries/plan"; + +interface Props { + isOpen: boolean; + onClose: () => void; + categories: PlanCategory[]; +} + +const DEFAULT_NEW_COLOR = "#2563eb"; + +export default function PlanCategoriesModal({ + isOpen, + onClose, + categories, +}: Props) { + const alert = useAlert(); + const [newLabel, setNewLabel] = useState(""); + const [newColor, setNewColor] = useState(DEFAULT_NEW_COLOR); + + const createCat = useApiMutation< + { label: string; color: string }, + PlanCategory + >({ + url: "/api/admin/plan/categories", + method: "POST", + invalidate: ["plan", "dashboard", "audit-log"], + }); + const updateCat = useApiMutation< + { id: number; label?: string; color?: string; is_active?: boolean }, + PlanCategory + >({ + url: (v) => `/api/admin/plan/categories/${v.id}`, + method: "PATCH", + invalidate: ["plan", "dashboard", "audit-log"], + }); + + const handleAdd = () => { + const label = newLabel.trim(); + if (!label) { + alert.error("Zadejte název kategorie"); + return; + } + createCat.mutate( + { label, color: newColor }, + { + onSuccess: () => { + setNewLabel(""); + setNewColor(DEFAULT_NEW_COLOR); + }, + onError: (e) => alert.error(e.message), + }, + ); + }; + + const patch = ( + id: number, + body: { label?: string; color?: string; is_active?: boolean }, + ) => { + updateCat.mutate( + { id, ...body }, + { onError: (e) => alert.error(e.message) }, + ); + }; + + const busy = createCat.isPending || updateCat.isPending; + + return ( + + + {categories.map((c) => ( + + patch(c.id, { color: e.target.value })} + aria-label={`Barva – ${c.label}`} + /> + { + const v = e.target.value.trim(); + if (v && v !== c.label) patch(c.id, { label: v }); + }} + aria-label={`Název – ${c.label}`} + /> + patch(c.id, { is_active: !c.is_active })} + > + {c.is_active ? "Skrýt" : "Obnovit"} + + + ))} + + + setNewColor(e.target.value)} + aria-label="Barva nové kategorie" + /> + setNewLabel(e.target.value)} + onKeyDown={(e) => e.key === "Enter" && handleAdd()} + /> + + Přidat + + + + + ); +} diff --git a/src/admin/pages/PlanWork.tsx b/src/admin/pages/PlanWork.tsx index de344fb..823a1d5 100644 --- a/src/admin/pages/PlanWork.tsx +++ b/src/admin/pages/PlanWork.tsx @@ -11,6 +11,7 @@ import { import type { PlanCellModalMode } from "../components/PlanCellModal"; import PlanGrid from "../components/PlanGrid"; import PlanCellModal from "../components/PlanCellModal"; +import PlanCategoriesModal from "../components/PlanCategoriesModal"; import Forbidden from "../components/Forbidden"; import { projectListOptions, type Project } from "../lib/queries/projects"; import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan"; @@ -129,6 +130,8 @@ export default function PlanWork() { const { data: categoriesData } = useQuery(planCategoriesOptions()); const categories: PlanCategory[] = categoriesData ?? []; + const [catModalOpen, setCatModalOpen] = useState(false); + // `lastMutated` is the (userId, date) of the most recent successful // mutation. We pass it to PlanGrid as `pulseKey`; the matching cell // gets a one-shot CSS pulse animation. We clear it after 700ms (the @@ -638,6 +641,15 @@ export default function PlanWork() { Měsíc + {canEdit && ( + setCatModalOpen(true)} + > + Správa kategorií + + )} {gridLoading && ( @@ -695,6 +707,11 @@ export default function PlanWork() { onCreateOverrideFromRange={createOverrideFromRange} onSwitchToEditRange={switchToEditRange} /> + setCatModalOpen(false)} + categories={categories} + /> ); } diff --git a/src/admin/plan.css b/src/admin/plan.css index 25293cd..b7800b7 100644 --- a/src/admin/plan.css +++ b/src/admin/plan.css @@ -977,3 +977,37 @@ animation: none !important; } } + +/* Category manager modal rows */ +.plan-cat-manager { + display: flex; + flex-direction: column; + gap: 8px; +} +.plan-cat-row { + display: flex; + align-items: center; + gap: 8px; +} +.plan-cat-row--inactive { + opacity: 0.55; +} +.plan-cat-row .admin-form-input { + flex: 1; + min-width: 0; +} +.plan-cat-color { + width: 34px; + height: 34px; + padding: 0; + border: 1px solid var(--plan-paper-rule); + border-radius: 6px; + background: none; + cursor: pointer; + flex: 0 0 auto; +} +.plan-cat-row--new { + margin-top: 6px; + padding-top: 10px; + border-top: 1px dashed var(--plan-paper-rule); +}