Corrige la grille des besoins : postes manquants au-delà du premier
Chaque cellule de la grille (label de poste, en-têtes, créneaux, cases vides) recevait sa position via l'ordre d'émission des enfants, en comptant sur le fait que chaque ligne totalise exactement le nombre de tranches de 30 min d'un jour pour que l'auto-flow CSS reste aligné. Un poste dont les créneaux ne tombaient pas exactement sur ce total (deux créneaux adjacents mais non alignés sur 30 min, courant depuis la création de créneaux à la main) décalait tous les postes suivants — leur nom de poste n'apparaissait plus sur la bonne ligne. Chaque cellule reçoit maintenant sa position de grille explicitement (gridColumn/gridRow calculés), immunisé contre ce genre de décalage : l'anomalie d'un poste reste locale à sa propre ligne. Nouveau test de non-régression (scripts/smoke.tsx:checkGridLayout) : inspecte les gridRow réellement rendus plutôt que la simple absence de crash — un décalage CSS n'est pas détectable par un rendu SSR ordinaire. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
5f80f167c8
commit
8b9c0ac498
@@ -463,12 +463,22 @@ temps : une colonne = 30 min, bornée par jour à la plage réellement utilisée
|
|||||||
(du créneau le plus matinal au plus tardif, arrondi à la demi-heure
|
(du créneau le plus matinal au plus tardif, arrondi à la demi-heure
|
||||||
englobante — pas les 24 h complètes). Les créneaux gardent des horaires
|
englobante — pas les 24 h complètes). Les créneaux gardent des horaires
|
||||||
libres (`ShiftCreate`, § 2) ; ils sont replacés dans cette grille fixe en
|
libres (`ShiftCreate`, § 2) ; ils sont replacés dans cette grille fixe en
|
||||||
fusionnant les cases qu'ils couvrent (`gridColumn: span N`), arrondis à la
|
fusionnant les cases qu'ils couvrent, arrondis à la tranche englobante s'ils
|
||||||
tranche englobante s'ils ne tombent pas pile sur un multiple de 30 min
|
ne tombent pas pile sur un multiple de 30 min (`buildTimeGrid` /
|
||||||
(`buildTimeGrid` / `placeInTimeGrid` dans `frontend/src/lib/planning.ts`).
|
`placeInTimeGrid` dans `frontend/src/lib/planning.ts`). **Chaque cellule
|
||||||
Largeur adaptative par jour (`columnWidthPx`, en fonction du nombre de
|
reçoit sa position de grille explicitement** (`gridColumn`/`gridRow`
|
||||||
tranches) : peu de tranches ⇒ colonnes larges, beaucoup ⇒ largeur compacte
|
calculés dans `GrillePage.tsx`, pas seulement `gridColumn: span N`) : un
|
||||||
avec défilement horizontal — jamais de colonnes étroites par défaut.
|
poste dont les créneaux ne totalisent pas exactement le nombre de tranches
|
||||||
|
d'un jour (deux créneaux adjacents mais non alignés sur 30 min, désormais
|
||||||
|
courant depuis la création à la main) ne doit jamais décaler les lignes
|
||||||
|
suivantes — bug corrigé une fois (postes manquants au-delà du premier), un
|
||||||
|
positionnement implicite (auto-flow) recréerait la même classe de bug.
|
||||||
|
Vérifié par `scripts/smoke.tsx:checkGridLayout` (inspecte les `grid-row`
|
||||||
|
rendus, pas seulement l'absence de crash — un décalage CSS n'est pas visible
|
||||||
|
dans un simple rendu SSR). Largeur adaptative par jour (`columnWidthPx`, en
|
||||||
|
fonction du nombre de tranches) : peu de tranches ⇒ colonnes larges, beaucoup
|
||||||
|
⇒ largeur compacte avec défilement horizontal — jamais de colonnes étroites
|
||||||
|
par défaut.
|
||||||
La « Vue bénévoles » (`VolunteerViewPage.tsx`) reste sur l'ancien modèle,
|
La « Vue bénévoles » (`VolunteerViewPage.tsx`) reste sur l'ancien modèle,
|
||||||
distinct : une colonne par **créneau** (signature `starts_at`/`ends_at`)
|
distinct : une colonne par **créneau** (signature `starts_at`/`ends_at`)
|
||||||
plutôt qu'un axe temporel continu (`buildLattice` / `LatticeColumn`), plus
|
plutôt qu'un axe temporel continu (`buildLattice` / `LatticeColumn`), plus
|
||||||
@@ -664,6 +674,7 @@ Aucune dépendance locale requise hors Docker.
|
|||||||
| 25 | Formulaire public : photo recadrée en carré côté navigateur avant l'envoi, aperçu agrandi ; personnalisation de la page (logo, texte d'introduction) depuis l'écran de configuration (`/inscriptions`) | à valider |
|
| 25 | Formulaire public : photo recadrée en carré côté navigateur avant l'envoi, aperçu agrandi ; personnalisation de la page (logo, texte d'introduction) depuis l'écran de configuration (`/inscriptions`) | à valider |
|
||||||
| 26 | Feuilles de poste : programmation du jour (horaires des artistes, tous postes confondus) sous le référent | à valider |
|
| 26 | Feuilles de poste : programmation du jour (horaires des artistes, tous postes confondus) sous le référent | à valider |
|
||||||
| 27 | Formulaire public : encart de paiement de la cotisation via Yapla (lien externe configurable), affiché sous la case correspondante | à valider |
|
| 27 | Formulaire public : encart de paiement de la cotisation via Yapla (lien externe configurable), affiché sous la case correspondante | à valider |
|
||||||
|
| 28 | Correctif grille des besoins : positionnement explicite (`gridColumn`/`gridRow`) de chaque cellule — un poste aux créneaux non alignés sur 30 min ne décale plus les postes suivants (noms de poste manquants au-delà du premier) | à valider |
|
||||||
|
|
||||||
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
|
**Reste hors périmètre** (modèle prêt à les accueillir) : SMS, constructeur
|
||||||
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +
|
de formulaire, pointage sur site, IA. Frontend : squelette + 6 écrans +
|
||||||
|
|||||||
@@ -228,6 +228,22 @@ async function checkShiftCreation() {
|
|||||||
});
|
});
|
||||||
const generated = await api.positions.generateShifts(e, position.id);
|
const generated = await api.positions.generateShifts(e, position.id);
|
||||||
|
|
||||||
|
// Deuxième créneau adjacent au premier mais non aligné sur 30 min (fin à
|
||||||
|
// 22:45, comme le premier) : arrondis à la tranche englobante, les deux
|
||||||
|
// créneaux se chevauchent légèrement dans la grille (voir GrillePage.tsx,
|
||||||
|
// `PositionRow`) — ne doit jamais planter le rendu ni décaler les postes
|
||||||
|
// suivants (régression corrigée : la grille positionnait ses cellules par
|
||||||
|
// ordre d'émission plutôt qu'explicitement, un poste dont les créneaux ne
|
||||||
|
// totalisaient pas exactement le nombre de tranches d'un jour décalait
|
||||||
|
// tous les postes suivants).
|
||||||
|
await api.shifts.create(e, {
|
||||||
|
position_id: position.id,
|
||||||
|
starts_at: `${edition.start_date}T22:45:00`,
|
||||||
|
ends_at: `${edition.start_date}T23:15:00`,
|
||||||
|
min_staff: 1,
|
||||||
|
ideal_staff: 1,
|
||||||
|
});
|
||||||
|
|
||||||
const grid = buildTimeGrid([shift]);
|
const grid = buildTimeGrid([shift]);
|
||||||
const day = grid[0];
|
const day = grid[0];
|
||||||
const placement = day ? placeInTimeGrid(grid, [shift]).get(day.date)?.[0] : undefined;
|
const placement = day ? placeInTimeGrid(grid, [shift]).get(day.date)?.[0] : undefined;
|
||||||
@@ -260,6 +276,56 @@ async function checkShiftCreation() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Grille des besoins : chaque poste affiché doit occuper sa propre ligne de
|
||||||
|
* grille CSS (`gridRow` explicite, voir `GrillePage.tsx:PositionRow`).
|
||||||
|
* Régression testée : avant le passage à un positionnement explicite, un
|
||||||
|
* poste dont les créneaux ne totalisaient pas exactement le nombre de
|
||||||
|
* tranches de 30 min d'un jour (deux créneaux adjacents mais non alignés
|
||||||
|
* sur 30 min, par ex. — cas courant depuis la création de créneaux à la
|
||||||
|
* main) décalait, par un pur effet de bord CSS (auto-flow), tous les postes
|
||||||
|
* suivants — leur nom de poste n'apparaissait alors plus sur sa propre
|
||||||
|
* ligne. Purement visuel : un rendu SSR ordinaire ne l'aurait pas détecté,
|
||||||
|
* d'où cette vérification directe des lignes de grille attribuées.
|
||||||
|
*/
|
||||||
|
async function checkGridLayout() {
|
||||||
|
if (!coordinator) throw new Error('grille : pas de session coordination pour ce test');
|
||||||
|
const positions = await api.positions.list(e);
|
||||||
|
const client = new QueryClient({ defaultOptions: { queries: { retry: false } } });
|
||||||
|
await prefetchCore(client);
|
||||||
|
const html = renderToString(
|
||||||
|
createElement(
|
||||||
|
QueryClientProvider,
|
||||||
|
{ client },
|
||||||
|
createElement(
|
||||||
|
AuthContext.Provider,
|
||||||
|
{ value: authValue(coordinator) },
|
||||||
|
createElement(MemoryRouter, null, createElement(GrillePage)),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const rowNumbers = [...html.matchAll(/grid-row:\s*(\d+)/g)].map((m) => Number(m[1]));
|
||||||
|
const uniqueRows = [...new Set(rowNumbers)].sort((a, b) => a - b);
|
||||||
|
// 2 lignes d'en-tête (jours, puis tranches) + une par poste affiché,
|
||||||
|
// strictement consécutives (1, 2, 3, … sans trou ni doublon).
|
||||||
|
const expected = Array.from({ length: positions.length + 2 }, (_, i) => i + 1);
|
||||||
|
const sequential = uniqueRows.length === expected.length
|
||||||
|
&& uniqueRows.every((row, i) => row === expected[i]);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
JSON.stringify(
|
||||||
|
{ positionsCount: positions.length, gridRowsUsed: uniqueRows.length, sequential },
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
if (!sequential) {
|
||||||
|
throw new Error(
|
||||||
|
`grille des besoins : lignes de grille ${JSON.stringify(uniqueRows)}, attendu ${JSON.stringify(expected)}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function prefetchCore(client: QueryClient) {
|
async function prefetchCore(client: QueryClient) {
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
client.prefetchQuery({ queryKey: queryKeys.volunteers, queryFn: () => api.volunteers.list(e) }),
|
client.prefetchQuery({ queryKey: queryKeys.volunteers, queryFn: () => api.volunteers.list(e) }),
|
||||||
@@ -330,6 +396,7 @@ await checkAuth().catch((err) => fail('checkAuth', err));
|
|||||||
await checkPublicForm().catch((err) => fail('checkPublicForm', err));
|
await checkPublicForm().catch((err) => fail('checkPublicForm', err));
|
||||||
await checkPositionSheets().catch((err) => fail('checkPositionSheets', err));
|
await checkPositionSheets().catch((err) => fail('checkPositionSheets', err));
|
||||||
await checkShiftCreation().catch((err) => fail('checkShiftCreation', err));
|
await checkShiftCreation().catch((err) => fail('checkShiftCreation', err));
|
||||||
|
await checkGridLayout().catch((err) => fail('checkGridLayout', err));
|
||||||
|
|
||||||
renderPublic('/connexion');
|
renderPublic('/connexion');
|
||||||
renderPublic('/connexion/coordination');
|
renderPublic('/connexion/coordination');
|
||||||
|
|||||||
@@ -94,6 +94,21 @@ export function GrillePage() {
|
|||||||
}),
|
}),
|
||||||
].join(' ');
|
].join(' ');
|
||||||
|
|
||||||
|
// Ligne de grille (1-based) où commence chaque jour, colonne 1 réservée au
|
||||||
|
// nom du poste — chaque cellule reçoit sa position explicitement
|
||||||
|
// (`gridColumn`/`gridRow`) plutôt que de compter sur l'ordre d'émission
|
||||||
|
// des enfants : un poste dont les créneaux ne totalisent pas exactement le
|
||||||
|
// nombre de tranches d'un jour (créneaux adjacents non alignés sur 30 min,
|
||||||
|
// par ex.) ne doit jamais décaler les lignes suivantes.
|
||||||
|
const dayColumnStarts: number[] = [];
|
||||||
|
{
|
||||||
|
let col = 2;
|
||||||
|
for (const day of shownDays) {
|
||||||
|
dayColumnStarts.push(col);
|
||||||
|
col += day.slotLabels.length;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const selected = selectedShiftId
|
const selected = selectedShiftId
|
||||||
? matrix.data.positions
|
? matrix.data.positions
|
||||||
.flatMap((p) => p.shifts.map((s) => ({ position: p, shift: s })))
|
.flatMap((p) => p.shifts.map((s) => ({ position: p, shift: s })))
|
||||||
@@ -206,15 +221,21 @@ export function GrillePage() {
|
|||||||
<div className="flex min-h-0 flex-1 items-stretch">
|
<div className="flex min-h-0 flex-1 items-stretch">
|
||||||
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto">
|
<div className="max-h-[calc(100vh-118px)] min-w-0 flex-1 overflow-auto">
|
||||||
<div className="grid w-max border-b-[3px] border-ink" style={{ gridTemplateColumns }}>
|
<div className="grid w-max border-b-[3px] border-ink" style={{ gridTemplateColumns }}>
|
||||||
<div className="sticky left-0 top-0 z-[6] flex items-center gap-[6px] border-b-2 border-r-[3px] border-ink border-b-cream bg-ink px-2 py-[6px] font-display text-[10px] font-extrabold uppercase tracking-wider text-cream">
|
<div
|
||||||
|
style={{ gridColumn: '1 / span 1', gridRow: 1 }}
|
||||||
|
className="sticky left-0 top-0 z-[6] flex items-center gap-[6px] border-b-2 border-r-[3px] border-ink border-b-cream bg-ink px-2 py-[6px] font-display text-[10px] font-extrabold uppercase tracking-wider text-cream"
|
||||||
|
>
|
||||||
{t('grille.positionsCount', { count: matrix.data.positions.length })}
|
{t('grille.positionsCount', { count: matrix.data.positions.length })}
|
||||||
<HelpTip text={t('grille.footnote')} side="bottom" />
|
<HelpTip text={t('grille.footnote')} side="bottom" />
|
||||||
</div>
|
</div>
|
||||||
{shownDays.map((day) => (
|
{shownDays.map((day, di) => (
|
||||||
<div
|
<div
|
||||||
key={day.date}
|
key={day.date}
|
||||||
className="sticky top-0 z-[4] flex items-baseline gap-[7px] border-b-2 border-r-[3px] border-ink border-b-cream border-r-cream bg-ink px-[9px] py-[6px] text-cream"
|
className="sticky top-0 z-[4] flex items-baseline gap-[7px] border-b-2 border-r-[3px] border-ink border-b-cream border-r-cream bg-ink px-[9px] py-[6px] text-cream"
|
||||||
style={{ gridColumn: `span ${day.slotLabels.length}` }}
|
style={{
|
||||||
|
gridColumn: `${dayColumnStarts[di]} / span ${day.slotLabels.length}`,
|
||||||
|
gridRow: 1,
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span className="font-display text-[12px] font-black tracking-tight">
|
<span className="font-display text-[12px] font-black tracking-tight">
|
||||||
{shortDayLabel(`${day.date}T12:00`)}
|
{shortDayLabel(`${day.date}T12:00`)}
|
||||||
@@ -222,13 +243,17 @@ export function GrillePage() {
|
|||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
<div className="sticky left-0 top-[27px] z-[6] flex items-center border-b-[3px] border-r-[3px] border-ink bg-sand px-2 font-mono text-[8.5px] opacity-60">
|
<div
|
||||||
|
style={{ gridColumn: '1 / span 1', gridRow: 2 }}
|
||||||
|
className="sticky left-0 top-[27px] z-[6] flex items-center border-b-[3px] border-r-[3px] border-ink bg-sand px-2 font-mono text-[8.5px] opacity-60"
|
||||||
|
>
|
||||||
{t('grille.assignedRequired')}
|
{t('grille.assignedRequired')}
|
||||||
</div>
|
</div>
|
||||||
{shownDays.map((day) =>
|
{shownDays.map((day, di) =>
|
||||||
day.slotLabels.map((label, i) => (
|
day.slotLabels.map((label, i) => (
|
||||||
<div
|
<div
|
||||||
key={`${day.date}|${i}`}
|
key={`${day.date}|${i}`}
|
||||||
|
style={{ gridColumn: `${dayColumnStarts[di] + i} / span 1`, gridRow: 2 }}
|
||||||
className="sticky top-[27px] z-[3] overflow-hidden text-ellipsis whitespace-nowrap border-b-[3px] border-r border-ink bg-sand px-[2px] py-[5px] text-center font-mono text-[9.5px]"
|
className="sticky top-[27px] z-[3] overflow-hidden text-ellipsis whitespace-nowrap border-b-[3px] border-r border-ink bg-sand px-[2px] py-[5px] text-center font-mono text-[9.5px]"
|
||||||
>
|
>
|
||||||
{label}
|
{label}
|
||||||
@@ -236,11 +261,13 @@ export function GrillePage() {
|
|||||||
)),
|
)),
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{shownPositions.map((position) => (
|
{shownPositions.map((position, posIndex) => (
|
||||||
<PositionRow
|
<PositionRow
|
||||||
key={position.id}
|
key={position.id}
|
||||||
position={position}
|
position={position}
|
||||||
days={shownDays}
|
days={shownDays}
|
||||||
|
dayColumnStarts={dayColumnStarts}
|
||||||
|
rowIndex={3 + posIndex}
|
||||||
dayBoundary={dayBoundary}
|
dayBoundary={dayBoundary}
|
||||||
selectedShiftId={selectedShiftId}
|
selectedShiftId={selectedShiftId}
|
||||||
onSelect={setSelectedShiftId}
|
onSelect={setSelectedShiftId}
|
||||||
@@ -305,6 +332,8 @@ function LegendItem({
|
|||||||
function PositionRow({
|
function PositionRow({
|
||||||
position,
|
position,
|
||||||
days,
|
days,
|
||||||
|
dayColumnStarts,
|
||||||
|
rowIndex,
|
||||||
dayBoundary,
|
dayBoundary,
|
||||||
selectedShiftId,
|
selectedShiftId,
|
||||||
onSelect,
|
onSelect,
|
||||||
@@ -312,6 +341,8 @@ function PositionRow({
|
|||||||
}: {
|
}: {
|
||||||
position: MatrixPosition;
|
position: MatrixPosition;
|
||||||
days: TimeGridDay[];
|
days: TimeGridDay[];
|
||||||
|
dayColumnStarts: number[];
|
||||||
|
rowIndex: number;
|
||||||
dayBoundary: string;
|
dayBoundary: string;
|
||||||
selectedShiftId: string | null;
|
selectedShiftId: string | null;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
@@ -321,34 +352,62 @@ function PositionRow({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="sticky left-0 z-[2] flex h-9 flex-col justify-center gap-[1px] border-b border-r-[3px] border-ink bg-cream px-2">
|
<div
|
||||||
|
style={{ gridColumn: '1 / span 1', gridRow: rowIndex }}
|
||||||
|
className="sticky left-0 z-[2] flex h-9 flex-col justify-center gap-[1px] border-b border-r-[3px] border-ink bg-cream px-2"
|
||||||
|
>
|
||||||
<span className="truncate font-display text-[12px] font-bold tracking-tight">
|
<span className="truncate font-display text-[12px] font-bold tracking-tight">
|
||||||
{position.name}
|
{position.name}
|
||||||
</span>
|
</span>
|
||||||
<span className="truncate font-mono text-[8.5px] opacity-55">{position.location}</span>
|
<span className="truncate font-mono text-[8.5px] opacity-55">{position.location}</span>
|
||||||
</div>
|
</div>
|
||||||
{days.map((day) => {
|
{days.map((day, di) => {
|
||||||
|
const base = dayColumnStarts[di];
|
||||||
const dayPlacements = placements.get(day.date) ?? [];
|
const dayPlacements = placements.get(day.date) ?? [];
|
||||||
const cells: React.ReactNode[] = [];
|
const cells: React.ReactNode[] = [];
|
||||||
let cursor = 0;
|
let cursor = 0;
|
||||||
for (const { item: shift, columnStart, columnSpan } of dayPlacements) {
|
for (const { item: shift, columnStart, columnSpan } of dayPlacements) {
|
||||||
for (let i = cursor; i < Math.max(cursor, columnStart); i += 1) {
|
// `columnStart` peut retomber avant `cursor` : deux créneaux du
|
||||||
cells.push(<div key={`${day.date}|empty|${i}`} className="h-9 border-b border-r border-ink" />);
|
// même poste adjacents mais non alignés sur 30 min s'arrondissent
|
||||||
|
// chacun à la tranche englobante et peuvent se toucher/chevaucher
|
||||||
|
// légèrement à l'affichage — on raccourcit plutôt que de laisser
|
||||||
|
// deux cellules revendiquer la même colonne (chaque cellule ayant
|
||||||
|
// sa position de grille explicite, ce repli reste local à ce
|
||||||
|
// poste : il ne décale jamais les lignes suivantes).
|
||||||
|
const start = Math.min(Math.max(cursor, columnStart), day.slotLabels.length);
|
||||||
|
for (let i = cursor; i < start; i += 1) {
|
||||||
|
cells.push(
|
||||||
|
<div
|
||||||
|
key={`${day.date}|empty|${i}`}
|
||||||
|
style={{ gridColumn: `${base + i} / span 1`, gridRow: rowIndex }}
|
||||||
|
className="h-9 border-b border-r border-ink"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
cells.push(
|
const end = Math.min(Math.max(start + 1, columnStart + columnSpan), day.slotLabels.length);
|
||||||
<ShiftCell
|
if (start < day.slotLabels.length) {
|
||||||
key={shift.shift_id}
|
cells.push(
|
||||||
shift={shift}
|
<ShiftCell
|
||||||
span={columnSpan}
|
key={shift.shift_id}
|
||||||
selected={selectedShiftId === shift.shift_id}
|
shift={shift}
|
||||||
onSelect={onSelect}
|
gridColumn={`${base + start} / span ${end - start}`}
|
||||||
names={volunteerNamesByShift.get(shift.shift_id) ?? []}
|
gridRow={rowIndex}
|
||||||
/>,
|
selected={selectedShiftId === shift.shift_id}
|
||||||
);
|
onSelect={onSelect}
|
||||||
cursor = Math.max(cursor, columnStart + columnSpan);
|
names={volunteerNamesByShift.get(shift.shift_id) ?? []}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
cursor = Math.max(cursor, end);
|
||||||
}
|
}
|
||||||
for (let i = cursor; i < day.slotLabels.length; i += 1) {
|
for (let i = cursor; i < day.slotLabels.length; i += 1) {
|
||||||
cells.push(<div key={`${day.date}|empty|${i}`} className="h-9 border-b border-r border-ink" />);
|
cells.push(
|
||||||
|
<div
|
||||||
|
key={`${day.date}|empty|${i}`}
|
||||||
|
style={{ gridColumn: `${base + i} / span 1`, gridRow: rowIndex }}
|
||||||
|
className="h-9 border-b border-r border-ink"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
return cells;
|
return cells;
|
||||||
})}
|
})}
|
||||||
@@ -358,13 +417,15 @@ function PositionRow({
|
|||||||
|
|
||||||
function ShiftCell({
|
function ShiftCell({
|
||||||
shift,
|
shift,
|
||||||
span,
|
gridColumn,
|
||||||
|
gridRow,
|
||||||
selected,
|
selected,
|
||||||
onSelect,
|
onSelect,
|
||||||
names,
|
names,
|
||||||
}: {
|
}: {
|
||||||
shift: MatrixShift;
|
shift: MatrixShift;
|
||||||
span: number;
|
gridColumn: string;
|
||||||
|
gridRow: number;
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
onSelect: (id: string) => void;
|
onSelect: (id: string) => void;
|
||||||
names: string[];
|
names: string[];
|
||||||
@@ -380,7 +441,7 @@ function ShiftCell({
|
|||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
onClick={() => onSelect(shift.shift_id)}
|
onClick={() => onSelect(shift.shift_id)}
|
||||||
style={{ gridColumn: `span ${span}` }}
|
style={{ gridColumn, gridRow }}
|
||||||
className={cx(
|
className={cx(
|
||||||
'group relative flex h-9 cursor-pointer items-center justify-center gap-[3px] border-b border-r border-ink',
|
'group relative flex h-9 cursor-pointer items-center justify-center gap-[3px] border-b border-r border-ink',
|
||||||
undefinedNeed && 'bg-sand-dark hover:bg-sand',
|
undefinedNeed && 'bg-sand-dark hover:bg-sand',
|
||||||
|
|||||||
Reference in New Issue
Block a user