feat(plan): grid renders up to 3 stacked records per cell

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-08 11:02:13 +02:00
parent 72888bf9cd
commit a146fc26a3
3 changed files with 62 additions and 24 deletions

View File

@@ -290,6 +290,17 @@ const PlanGridRoot = styled(Box)(({ theme }) => ({
},
"& .plan-cell--pulse": { animation: "plan-cell-pulse 600ms ease-out" },
"& .plan-cell-record": {
display: "block",
width: "100%",
minWidth: 0,
},
"& .plan-cell-record + .plan-cell-record": {
marginTop: 5,
paddingTop: 5,
borderTop: `1px dashed ${theme.vars!.palette.divider}`,
},
// Chips inside a cell
"& .plan-chip-block": {
display: "flex",
@@ -373,11 +384,7 @@ interface Props {
// the value so back-to-back mutations on the same cell re-trigger
// the animation (React needs a new key to re-mount the class).
pulseKey?: { userId: number; date: string; nonce: number } | null;
onCellClick: (
userId: number,
date: string,
cell: ResolvedCell | null,
) => void;
onCellClick: (userId: number, date: string, cells: ResolvedCell[]) => void;
}
// Full Czech weekday names, indexed by Date.getUTCDay() (0 = Sunday).
@@ -572,29 +579,51 @@ export default function PlanGrid({
} as CSSProperties)
: undefined
}
onClick={() => onCellClick(u.id, date, cell)}
onClick={() => onCellClick(u.id, date, cellArr)}
aria-label={
cell
? `${u.full_name}, ${date}, ${planCategoryLabel(cell.category, catMap)}`
? cellArr.length === 1
? `${u.full_name}, ${date}, ${planCategoryLabel(cell.category, catMap)}`
: `${u.full_name}, ${date}, ${cellArr.length} záznamy`
: isLocked
? `${u.full_name}, ${date}, ${past ? "uplynulý den" : "prázdné"} — bez záznamu`
: `${u.full_name}, ${date}, prázdné — přidat záznam`
}
>
<PlanRangeChips
cell={cell}
project={
cell?.project_id
? (projects.find(
(p) => p.id === cell.project_id,
) ?? null)
: null
}
readonly={!canEdit}
categoryLabel={
cell ? planCategoryLabel(cell.category, catMap) : ""
}
/>
{cellArr.map((c, i) => (
<Box
key={
c.entryId != null
? c.entryId
: c.overrideId != null
? `o${c.overrideId}`
: i
}
className="plan-cell-record"
style={
{
"--cat-color": catMap[c.category]?.color,
} as CSSProperties
}
>
<PlanRangeChips
cell={c}
project={
c.project_id
? (projects.find(
(p) => p.id === c.project_id,
) ?? null)
: null
}
readonly={!canEdit}
categoryLabel={planCategoryLabel(
c.category,
catMap,
)}
showNote={cellArr.length === 1}
/>
</Box>
))}
</button>
</td>
);

View File

@@ -6,6 +6,7 @@ interface Props {
project: Project | null;
readonly?: boolean;
categoryLabel: string;
showNote?: boolean;
}
export default function PlanRangeChips({
@@ -13,6 +14,7 @@ export default function PlanRangeChips({
project,
readonly,
categoryLabel,
showNote = true,
}: Props) {
void readonly;
if (!cell) return null;
@@ -36,7 +38,9 @@ export default function PlanRangeChips({
</span>
)}
</div>
{cell.note && <div className="plan-cell-note">{cell.note}</div>}
{showNote && cell.note && (
<div className="plan-cell-note">{cell.note}</div>
)}
</div>
);
}

View File

@@ -17,7 +17,11 @@ import PlanCategoriesModal from "../components/PlanCategoriesModal";
import Forbidden from "../components/Forbidden";
import { Button, Alert, LoadingState, PageEnter } from "../ui";
import { projectListOptions, type Project } from "../lib/queries/projects";
import { planCategoriesOptions, type PlanCategory } from "../lib/queries/plan";
import {
planCategoriesOptions,
type PlanCategory,
type ResolvedCell,
} from "../lib/queries/plan";
// plan.css is imported once globally in AdminApp.tsx — no per-page re-import.
const MONTH_NAMES = [
@@ -202,7 +206,8 @@ export default function PlanWork() {
);
const openCell = useCallback(
(userId: number, date: string) => {
(userId: number, date: string, _cells: ResolvedCell[]) => {
void _cells;
const cell = getCell(grid, userId, date);
// Past-date guard: the server rejects create/edit on past dates with
// a 403 (see assertNotPastDate in plan.service.ts). To keep the user