diff --git a/src/admin/components/BulkPlanModal.tsx b/src/admin/components/BulkPlanModal.tsx
new file mode 100644
index 0000000..44507e8
--- /dev/null
+++ b/src/admin/components/BulkPlanModal.tsx
@@ -0,0 +1,191 @@
+import Box from "@mui/material/Box";
+import Typography from "@mui/material/Typography";
+import {
+ Modal,
+ Field,
+ DateField,
+ Select,
+ TextField,
+ CheckboxField,
+} from "../ui";
+import type { PlanUser, PlanCategory } from "../lib/queries/plan";
+import type { Project } from "../lib/queries/projects";
+
+export interface BulkPlanForm {
+ date_from: string;
+ date_to: string;
+ is_range: boolean;
+ user_ids: string[];
+ include_weekends: boolean;
+ project_id: string;
+ category: string;
+ note: string;
+}
+
+interface Props {
+ isOpen: boolean;
+ onClose: () => void;
+ form: BulkPlanForm;
+ setForm: (form: BulkPlanForm) => void;
+ users: PlanUser[];
+ projects: Project[];
+ categories: PlanCategory[];
+ onSubmit: () => void;
+ submitting: boolean;
+ toggleUser: (userId: number) => void;
+ toggleAllUsers: () => void;
+}
+
+export default function BulkPlanModal({
+ isOpen,
+ onClose,
+ form,
+ setForm,
+ users,
+ projects,
+ categories,
+ onSubmit,
+ submitting,
+ toggleUser,
+ toggleAllUsers,
+}: Props) {
+ const activeCategories = categories.filter((c) => c.is_active);
+ return (
+
+
+ Vytvoří záznamy pro vybrané dny u zvolených zaměstnanců. Dny, které už
+ mají 3 záznamy, se přeskočí.
+
+
+
+ setForm({ ...form, date_from: val })}
+ />
+
+
+ setForm({ ...form, is_range: checked })}
+ />
+
+ {form.is_range && (
+
+ setForm({ ...form, date_to: val })}
+ />
+
+ )}
+
+
+ setForm({ ...form, include_weekends: checked })
+ }
+ />
+
+
+
+
+
+ Zaměstnanci
+
+
+ {users.length > 0 && form.user_ids.length === users.length
+ ? "Odznačit vše"
+ : "Vybrat vše"}
+
+
+
+ {users.map((u) => (
+ toggleUser(u.id)}
+ />
+ ))}
+
+
+ Vybráno: {form.user_ids.length} z {users.length}
+
+
+
+
+
+
+
+
+ setForm({ ...form, note: e.target.value })}
+ inputProps={{ maxLength: 500 }}
+ />
+
+
+ );
+}
diff --git a/src/admin/hooks/usePlanWork.ts b/src/admin/hooks/usePlanWork.ts
index 5bb2a35..e1731dd 100644
--- a/src/admin/hooks/usePlanWork.ts
+++ b/src/admin/hooks/usePlanWork.ts
@@ -488,6 +488,20 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
},
});
+ const bulkCreate = useMutation({
+ mutationFn: (body: any) =>
+ apiCall("/api/admin/plan/entries/bulk", {
+ method: "POST",
+ body: JSON.stringify(body),
+ headers: { "Content-Type": "application/json" },
+ }),
+ onSuccess: () => {
+ // No optimistic patch — bulk can touch many cells; just invalidate and
+ // let the grid refetch the authoritative state.
+ invalidate();
+ },
+ });
+
// Roll back an optimistic patch on mutation error. Called from
// PlanWork's mutation wrappers via `rollbackMutation(mutation, key)`.
// `mutation` is a TanStack mutation result with a `_rolled` snapshot stashed
@@ -534,6 +548,7 @@ export function usePlanWork({ initialDate, canEdit }: UsePlanWorkArgs) {
createOverride,
updateOverride,
deleteOverride,
+ bulkCreate,
invalidate,
// Exposed so PlanWork can roll back an optimistic patch when a
// mutation throws. Pass the failed mutation and the userId it
diff --git a/src/admin/pages/PlanWork.tsx b/src/admin/pages/PlanWork.tsx
index 590de21..29f33ad 100644
--- a/src/admin/pages/PlanWork.tsx
+++ b/src/admin/pages/PlanWork.tsx
@@ -15,6 +15,7 @@ import type { PlanCellModalMode } from "../components/PlanCellModal";
import PlanGrid from "../components/PlanGrid";
import PlanCellModal from "../components/PlanCellModal";
import PlanCategoriesModal from "../components/PlanCategoriesModal";
+import BulkPlanModal, { type BulkPlanForm } from "../components/BulkPlanModal";
import Forbidden from "../components/Forbidden";
import { Button, Alert, LoadingState, PageEnter } from "../ui";
import { projectListOptions, type Project } from "../lib/queries/projects";
@@ -128,6 +129,7 @@ export default function PlanWork() {
createOverride,
updateOverride,
deleteOverride,
+ bulkCreate,
rollbackMutation,
} = usePlanWork({ canEdit });
@@ -140,6 +142,84 @@ export default function PlanWork() {
const [catModalOpen, setCatModalOpen] = useState(false);
+ const [bulkOpen, setBulkOpen] = useState(false);
+ const [bulkSubmitting, setBulkSubmitting] = useState(false);
+ const [bulkForm, setBulkForm] = useState(() => {
+ const today = todayIsoLocal();
+ return {
+ date_from: today,
+ date_to: today,
+ is_range: false,
+ user_ids: [],
+ include_weekends: false,
+ project_id: "",
+ category: "work",
+ note: "",
+ };
+ });
+
+ const planUsers = useMemo(() => grid?.users ?? [], [grid]);
+
+ const toggleBulkUser = useCallback((userId: number) => {
+ setBulkForm((prev) => ({
+ ...prev,
+ user_ids: prev.user_ids.includes(String(userId))
+ ? prev.user_ids.filter((u) => u !== String(userId))
+ : [...prev.user_ids, String(userId)],
+ }));
+ }, []);
+
+ const toggleAllBulkUsers = useCallback(() => {
+ const allIds = (grid?.users ?? []).map((u) => String(u.id));
+ if (allIds.length === 0) return; // grid not yet loaded — no-op
+ setBulkForm((prev) => ({
+ ...prev,
+ user_ids: prev.user_ids.length === allIds.length ? [] : allIds,
+ }));
+ }, [grid]);
+
+ const submitBulk = useCallback(async () => {
+ if (bulkForm.user_ids.length === 0) return;
+ setBulkSubmitting(true);
+ try {
+ const data: any = await bulkCreate.mutateAsync({
+ user_ids: bulkForm.user_ids.map(Number),
+ date_from: bulkForm.date_from,
+ date_to: bulkForm.is_range ? bulkForm.date_to : bulkForm.date_from,
+ include_weekends: bulkForm.include_weekends,
+ project_id: bulkForm.project_id ? Number(bulkForm.project_id) : null,
+ category: bulkForm.category,
+ note: bulkForm.note,
+ });
+ // apiCall returns the full envelope { success, data, message };
+ // the summary counts are under .data
+ const summary = data?.data ?? data;
+ const skipped =
+ summary?.skipped_days > 0
+ ? ` ${summary.skipped_days} dní přeskočeno (limit 3 na den).`
+ : "";
+ alert.success(
+ `Vytvořeno ${summary?.created_days ?? 0} záznamů pro ${summary?.users ?? 0} zaměstnanců.${skipped}`,
+ );
+ setBulkOpen(false);
+ // Reset the volatile parts so reopening starts clean (project /
+ // category / weekends stay sticky — users often repeat those).
+ const today = todayIsoLocal();
+ setBulkForm((prev) => ({
+ ...prev,
+ date_from: today,
+ date_to: today,
+ is_range: false,
+ user_ids: [],
+ note: "",
+ }));
+ } catch (e) {
+ alert.error(e instanceof Error ? e.message : "Chyba při ukládání");
+ } finally {
+ setBulkSubmitting(false);
+ }
+ }, [bulkForm, bulkCreate, alert]);
+
// `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
@@ -673,6 +753,15 @@ export default function PlanWork() {
Správa kategorií
)}
+ {canEdit && (
+
+ )}
{gridLoading && }
@@ -727,6 +816,19 @@ export default function PlanWork() {
onClose={() => setCatModalOpen(false)}
categories={categories}
/>
+ setBulkOpen(false)}
+ form={bulkForm}
+ setForm={setBulkForm}
+ users={planUsers}
+ projects={projects}
+ categories={categories}
+ onSubmit={submitBulk}
+ submitting={bulkSubmitting}
+ toggleUser={toggleBulkUser}
+ toggleAllUsers={toggleAllBulkUsers}
+ />
);
}