diff --git a/src/admin/components/PlanGrid.tsx b/src/admin/components/PlanGrid.tsx
new file mode 100644
index 0000000..46483a0
--- /dev/null
+++ b/src/admin/components/PlanGrid.tsx
@@ -0,0 +1,86 @@
+import { GridData, ResolvedCell } from "../lib/queries/plan";
+import PlanRangeChips from "./PlanRangeChips";
+
+interface Props {
+ data: GridData | undefined;
+ canEdit: boolean;
+ onCellClick: (
+ userId: number,
+ date: string,
+ cell: ResolvedCell | null,
+ ) => void;
+}
+
+const CZECH_WEEKDAYS = ["Ne", "Po", "Út", "St", "Čt", "Pá", "So"];
+
+function eachDay(from: string, to: string): string[] {
+ const out: string[] = [];
+ const a = new Date(from + "T00:00:00.000Z");
+ const b = new Date(to + "T00:00:00.000Z");
+ for (let d = new Date(a); d <= b; d.setUTCDate(d.getUTCDate() + 1)) {
+ out.push(d.toISOString().slice(0, 10));
+ }
+ return out;
+}
+
+function czechWeekday(dateStr: string): string {
+ return CZECH_WEEKDAYS[new Date(dateStr + "T00:00:00.000Z").getUTCDay()];
+}
+
+function isWeekend(dateStr: string): boolean {
+ const day = new Date(dateStr + "T00:00:00.000Z").getUTCDay();
+ return day === 0 || day === 6;
+}
+
+export default function PlanGrid({ data, canEdit, onCellClick }: Props) {
+ if (!data)
+ return (
+
+ );
+ const days = eachDay(data.date_from, data.date_to);
+ const users = data.users;
+
+ return (
+
+
+
+
+ | Datum |
+ {users.map((u) => (
+ {u.full_name} |
+ ))}
+
+
+
+ {days.map((date) => (
+
+ |
+ {czechWeekday(date)} {date.slice(8)}.{date.slice(5, 7)}.
+ |
+ {users.map((u) => {
+ const cell = data.cells[u.id]?.[date] ?? null;
+ const cls = `plan-cell${canEdit ? "" : " plan-cell--readonly"}`;
+ return (
+
+
+ |
+ );
+ })}
+
+ ))}
+
+
+
+ );
+}
diff --git a/src/admin/components/PlanRangeChips.tsx b/src/admin/components/PlanRangeChips.tsx
new file mode 100644
index 0000000..16ec9d7
--- /dev/null
+++ b/src/admin/components/PlanRangeChips.tsx
@@ -0,0 +1,33 @@
+import { ResolvedCell } from "../lib/queries/plan";
+
+interface Props {
+ cell: ResolvedCell | null;
+ readonly?: boolean;
+}
+
+const CATEGORY_LABELS: Record = {
+ work: "Práce",
+ preparation: "Příprava",
+ travel: "Cesta / Montáž",
+ leave: "Dovolená",
+ sick: "Nemoc",
+ training: "Školení",
+ other: "Jiné",
+};
+
+export default function PlanRangeChips({ cell, readonly }: Props) {
+ if (!cell) return null;
+ return (
+
+
+ {CATEGORY_LABELS[cell.category] ?? cell.category}
+
+ {cell.project_id && (
+
+ {`#${cell.project_id}`}
+
+ )}
+ {cell.note && {cell.note}}
+
+ );
+}
diff --git a/src/admin/plan.css b/src/admin/plan.css
new file mode 100644
index 0000000..a62eace
--- /dev/null
+++ b/src/admin/plan.css
@@ -0,0 +1,125 @@
+/* === Plán prací (work schedule) module === */
+
+.plan-grid-wrap {
+ overflow: auto;
+ border: 1px solid var(--border, #e5e7eb);
+ border-radius: 8px;
+ max-height: calc(100vh - 240px);
+}
+
+.plan-grid {
+ border-collapse: separate;
+ border-spacing: 0;
+ width: 100%;
+ font-size: 13px;
+}
+
+.plan-grid thead th {
+ position: sticky;
+ top: 0;
+ background: var(--bg-secondary, #f9fafb);
+ z-index: 2;
+ padding: 8px;
+ border-bottom: 1px solid var(--border, #e5e7eb);
+ text-align: left;
+ font-weight: 600;
+ white-space: nowrap;
+}
+
+.plan-grid thead th.plan-grid-date-col {
+ left: 0;
+ z-index: 3;
+ min-width: 110px;
+}
+
+.plan-grid tbody td {
+ padding: 6px 8px;
+ border-bottom: 1px solid var(--border, #e5e7eb);
+ vertical-align: top;
+ min-width: 140px;
+}
+
+.plan-grid tbody td.plan-grid-date-col {
+ position: sticky;
+ left: 0;
+ background: var(--bg-secondary, #f9fafb);
+ z-index: 1;
+ font-weight: 500;
+ color: var(--text-muted, #6b7280);
+ white-space: nowrap;
+}
+
+.plan-grid tr.plan-grid-weekend td {
+ background: var(--bg-weekend, #fefce8);
+}
+.plan-grid tr.plan-grid-weekend td.plan-grid-date-col {
+ background: var(--bg-weekend-strong, #fef3c7);
+}
+
+.plan-cell {
+ display: block;
+ width: 100%;
+ text-align: left;
+ background: transparent;
+ border: 1px dashed transparent;
+ border-radius: 6px;
+ padding: 4px 6px;
+ cursor: pointer;
+ min-height: 40px;
+}
+.plan-cell:hover {
+ border-color: var(--primary, #2563eb);
+}
+.plan-cell--readonly {
+ cursor: default;
+}
+.plan-cell--readonly:hover {
+ border-color: transparent;
+}
+
+.plan-chip {
+ display: inline-block;
+ border-radius: 4px;
+ padding: 2px 6px;
+ font-size: 12px;
+ font-weight: 500;
+ color: #1f2937;
+ background: var(--chip-default, #e5e7eb);
+}
+.plan-chip--work {
+ background: #dbeafe;
+}
+.plan-chip--preparation {
+ background: #ccfbf1;
+}
+.plan-chip--travel {
+ background: #fde68a;
+}
+.plan-chip--leave {
+ background: #bbf7d0;
+}
+.plan-chip--sick {
+ background: #fecaca;
+}
+.plan-chip--training {
+ background: #e9d5ff;
+}
+.plan-chip--other {
+ background: #e5e7eb;
+}
+
+.plan-cell-note {
+ display: block;
+ margin-top: 2px;
+ font-size: 11px;
+ color: var(--text-muted, #6b7280);
+ line-height: 1.3;
+}
+
+.plan-toolbar {
+ display: flex;
+ gap: 12px;
+ align-items: center;
+ margin-bottom: 12px;
+ flex-wrap: wrap;
+}