feat(plan): add Sprava kategorii management modal and button
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
145
src/admin/components/PlanCategoriesModal.tsx
Normal file
145
src/admin/components/PlanCategoriesModal.tsx
Normal file
@@ -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 (
|
||||||
|
<FormModal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={onClose}
|
||||||
|
title="Správa kategorií"
|
||||||
|
hideFooter
|
||||||
|
size="md"
|
||||||
|
>
|
||||||
|
<div className="plan-cat-manager">
|
||||||
|
{categories.map((c) => (
|
||||||
|
<div
|
||||||
|
key={c.id}
|
||||||
|
className={`plan-cat-row${c.is_active ? "" : " plan-cat-row--inactive"}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="plan-cat-color"
|
||||||
|
value={c.color}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => patch(c.id, { color: e.target.value })}
|
||||||
|
aria-label={`Barva – ${c.label}`}
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="admin-form-input"
|
||||||
|
defaultValue={c.label}
|
||||||
|
disabled={busy}
|
||||||
|
onBlur={(e) => {
|
||||||
|
const v = e.target.value.trim();
|
||||||
|
if (v && v !== c.label) patch(c.id, { label: v });
|
||||||
|
}}
|
||||||
|
aria-label={`Název – ${c.label}`}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary admin-btn-sm"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={() => patch(c.id, { is_active: !c.is_active })}
|
||||||
|
>
|
||||||
|
{c.is_active ? "Skrýt" : "Obnovit"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
|
||||||
|
<div className="plan-cat-row plan-cat-row--new">
|
||||||
|
<input
|
||||||
|
type="color"
|
||||||
|
className="plan-cat-color"
|
||||||
|
value={newColor}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => setNewColor(e.target.value)}
|
||||||
|
aria-label="Barva nové kategorie"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
className="admin-form-input"
|
||||||
|
placeholder="Nová kategorie…"
|
||||||
|
value={newLabel}
|
||||||
|
disabled={busy}
|
||||||
|
onChange={(e) => setNewLabel(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === "Enter" && handleAdd()}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-primary admin-btn-sm"
|
||||||
|
disabled={busy}
|
||||||
|
onClick={handleAdd}
|
||||||
|
>
|
||||||
|
Přidat
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</FormModal>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -11,6 +11,7 @@ import {
|
|||||||
import type { PlanCellModalMode } from "../components/PlanCellModal";
|
import type { PlanCellModalMode } from "../components/PlanCellModal";
|
||||||
import PlanGrid from "../components/PlanGrid";
|
import PlanGrid from "../components/PlanGrid";
|
||||||
import PlanCellModal from "../components/PlanCellModal";
|
import PlanCellModal from "../components/PlanCellModal";
|
||||||
|
import PlanCategoriesModal from "../components/PlanCategoriesModal";
|
||||||
import Forbidden from "../components/Forbidden";
|
import Forbidden from "../components/Forbidden";
|
||||||
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";
|
||||||
@@ -129,6 +130,8 @@ export default function PlanWork() {
|
|||||||
const { data: categoriesData } = useQuery(planCategoriesOptions());
|
const { data: categoriesData } = useQuery(planCategoriesOptions());
|
||||||
const categories: PlanCategory[] = categoriesData ?? [];
|
const categories: PlanCategory[] = categoriesData ?? [];
|
||||||
|
|
||||||
|
const [catModalOpen, setCatModalOpen] = useState(false);
|
||||||
|
|
||||||
// `lastMutated` is the (userId, date) of the most recent successful
|
// `lastMutated` is the (userId, date) of the most recent successful
|
||||||
// mutation. We pass it to PlanGrid as `pulseKey`; the matching cell
|
// mutation. We pass it to PlanGrid as `pulseKey`; the matching cell
|
||||||
// gets a one-shot CSS pulse animation. We clear it after 700ms (the
|
// gets a one-shot CSS pulse animation. We clear it after 700ms (the
|
||||||
@@ -638,6 +641,15 @@ export default function PlanWork() {
|
|||||||
Měsíc
|
Měsíc
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
{canEdit && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="admin-btn admin-btn-secondary"
|
||||||
|
onClick={() => setCatModalOpen(true)}
|
||||||
|
>
|
||||||
|
Správa kategorií
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
{gridLoading && (
|
{gridLoading && (
|
||||||
@@ -695,6 +707,11 @@ export default function PlanWork() {
|
|||||||
onCreateOverrideFromRange={createOverrideFromRange}
|
onCreateOverrideFromRange={createOverrideFromRange}
|
||||||
onSwitchToEditRange={switchToEditRange}
|
onSwitchToEditRange={switchToEditRange}
|
||||||
/>
|
/>
|
||||||
|
<PlanCategoriesModal
|
||||||
|
isOpen={catModalOpen}
|
||||||
|
onClose={() => setCatModalOpen(false)}
|
||||||
|
categories={categories}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -977,3 +977,37 @@
|
|||||||
animation: none !important;
|
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);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user