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--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 // Chips inside a cell
"& .plan-chip-block": { "& .plan-chip-block": {
display: "flex", display: "flex",
@@ -373,11 +384,7 @@ interface Props {
// the value so back-to-back mutations on the same cell re-trigger // 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). // the animation (React needs a new key to re-mount the class).
pulseKey?: { userId: number; date: string; nonce: number } | null; pulseKey?: { userId: number; date: string; nonce: number } | null;
onCellClick: ( onCellClick: (userId: number, date: string, cells: ResolvedCell[]) => void;
userId: number,
date: string,
cell: ResolvedCell | null,
) => void;
} }
// Full Czech weekday names, indexed by Date.getUTCDay() (0 = Sunday). // Full Czech weekday names, indexed by Date.getUTCDay() (0 = Sunday).
@@ -572,29 +579,51 @@ export default function PlanGrid({
} as CSSProperties) } as CSSProperties)
: undefined : undefined
} }
onClick={() => onCellClick(u.id, date, cell)} onClick={() => onCellClick(u.id, date, cellArr)}
aria-label={ aria-label={
cell 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 : isLocked
? `${u.full_name}, ${date}, ${past ? "uplynulý den" : "prázdné"} — bez záznamu` ? `${u.full_name}, ${date}, ${past ? "uplynulý den" : "prázdné"} — bez záznamu`
: `${u.full_name}, ${date}, prázdné — přidat záznam` : `${u.full_name}, ${date}, prázdné — přidat záznam`
} }
> >
<PlanRangeChips {cellArr.map((c, i) => (
cell={cell} <Box
project={ key={
cell?.project_id c.entryId != null
? (projects.find( ? c.entryId
(p) => p.id === cell.project_id, : c.overrideId != null
) ?? null) ? `o${c.overrideId}`
: null : i
} }
readonly={!canEdit} className="plan-cell-record"
categoryLabel={ style={
cell ? planCategoryLabel(cell.category, catMap) : "" {
} "--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> </button>
</td> </td>
); );

View File

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

View File

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