feat(plan): grid component with sticky headers and weekend tint

This commit is contained in:
BOHA
2026-06-05 12:57:41 +02:00
parent ca911bf96c
commit 003807a074
3 changed files with 244 additions and 0 deletions

View File

@@ -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 (
<div className="admin-loading">
<div className="admin-spinner" />
</div>
);
const days = eachDay(data.date_from, data.date_to);
const users = data.users;
return (
<div className="plan-grid-wrap">
<table className="plan-grid">
<thead>
<tr>
<th className="plan-grid-date-col">Datum</th>
{users.map((u) => (
<th key={u.id}>{u.full_name}</th>
))}
</tr>
</thead>
<tbody>
{days.map((date) => (
<tr
key={date}
className={isWeekend(date) ? "plan-grid-weekend" : ""}
>
<td className="plan-grid-date-col">
{czechWeekday(date)} {date.slice(8)}.{date.slice(5, 7)}.
</td>
{users.map((u) => {
const cell = data.cells[u.id]?.[date] ?? null;
const cls = `plan-cell${canEdit ? "" : " plan-cell--readonly"}`;
return (
<td key={u.id}>
<button
type="button"
className={cls}
onClick={() => onCellClick(u.id, date, cell)}
>
<PlanRangeChips cell={cell} readonly={!canEdit} />
</button>
</td>
);
})}
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -0,0 +1,33 @@
import { ResolvedCell } from "../lib/queries/plan";
interface Props {
cell: ResolvedCell | null;
readonly?: boolean;
}
const CATEGORY_LABELS: Record<string, string> = {
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 (
<div>
<span className={`plan-chip plan-chip--${cell.category}`}>
{CATEGORY_LABELS[cell.category] ?? cell.category}
</span>
{cell.project_id && (
<span style={{ marginLeft: 6, fontSize: 12, fontWeight: 600 }}>
{`#${cell.project_id}`}
</span>
)}
{cell.note && <span className="plan-cell-note">{cell.note}</span>}
</div>
);
}