Répéter un créneau sur plusieurs jours
Le panneau créait un créneau, pour un jour. Or un même horaire se répète d'un jour à l'autre bien plus souvent qu'il ne varie : ressaisir cinq fois « 9 h–17 h, pause 30 » était le geste le plus répété de la semaine, et le plus exposé à la faute de frappe. Les sept jours sont désormais cochables. Le jour ouvert est coché et verrouillé — on ne crée pas ailleurs qu'ici depuis la case d'un jour donné — et voyage dans un champ caché, puisqu'une case désactivée n'est pas envoyée. La création est **tout ou rien**. Un jour refusé, pour période close ou chevauchement, annule les autres : une répétition à demi appliquée laisserait un planning que personne n'a voulu, et qu'il faudrait défaire à la main pour le refaire. Le contrôle de période porte donc sur tous les jours demandés avant la première écriture. Trois champs du modèle étaient inatteignables depuis l'écran : le nombre de repas, la note du créneau, et l'étiquette en modification. Cette dernière était un vrai défaut — une étiquette posée de travers se corrigeait en supprimant le créneau pour le refaire, ce qui perdait sa validation et son historique. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
804e14bf04
commit
aa7336d749
6 files changed
+236
-74
No files matched your search
@@ -7,6 +7,7 @@ import { WeekGrid } from '@/components/planning/WeekGrid';
|
|||||||
import { Badge } from '@/components/ui/Badge';
|
import { Badge } from '@/components/ui/Badge';
|
||||||
import { Button } from '@/components/ui/Button';
|
import { Button } from '@/components/ui/Button';
|
||||||
import { PersistentForm } from '@/components/ui/PersistentForm';
|
import { PersistentForm } from '@/components/ui/PersistentForm';
|
||||||
|
import { cx } from '@/lib/cx';
|
||||||
import { formatMinutes } from '@/domain/counters/week';
|
import { formatMinutes } from '@/domain/counters/week';
|
||||||
import {
|
import {
|
||||||
displayName,
|
displayName,
|
||||||
@@ -205,6 +206,8 @@ export function TeamSection({
|
|||||||
rows={section.rows}
|
rows={section.rows}
|
||||||
localDate={dates[target.dayIndex] ?? ''}
|
localDate={dates[target.dayIndex] ?? ''}
|
||||||
dayLabel={days[target.dayIndex] ?? ''}
|
dayLabel={days[target.dayIndex] ?? ''}
|
||||||
|
dates={dates}
|
||||||
|
days={days}
|
||||||
target={target}
|
target={target}
|
||||||
onClose={() => setTarget(null)}
|
onClose={() => setTarget(null)}
|
||||||
/>
|
/>
|
||||||
@@ -350,6 +353,8 @@ function ShiftComposer({
|
|||||||
rows,
|
rows,
|
||||||
localDate,
|
localDate,
|
||||||
dayLabel,
|
dayLabel,
|
||||||
|
dates,
|
||||||
|
days,
|
||||||
target,
|
target,
|
||||||
onClose,
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
@@ -359,6 +364,8 @@ function ShiftComposer({
|
|||||||
rows: BoardRow[];
|
rows: BoardRow[];
|
||||||
localDate: string;
|
localDate: string;
|
||||||
dayLabel: string;
|
dayLabel: string;
|
||||||
|
dates: string[];
|
||||||
|
days: string[];
|
||||||
target: Target;
|
target: Target;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}) {
|
}) {
|
||||||
@@ -450,20 +457,51 @@ function ShiftComposer({
|
|||||||
/>
|
/>
|
||||||
</Field>
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Repas" htmlFor="composer-meals">
|
||||||
|
<input
|
||||||
|
id="composer-meals"
|
||||||
|
name="mealCount"
|
||||||
|
type="number"
|
||||||
|
min={0}
|
||||||
|
max={5}
|
||||||
|
defaultValue={editing ? target.shift.mealCount : 0}
|
||||||
|
className="h-8 w-16 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
{/* L'étiquette est modifiable à la correction aussi : posée de travers,
|
||||||
|
elle se corrigeait en supprimant le créneau pour le refaire — ce qui
|
||||||
|
perdait sa validation et son historique. */}
|
||||||
|
<Field label="Poste" htmlFor="composer-label">
|
||||||
|
<select
|
||||||
|
id="composer-label"
|
||||||
|
name="labelId"
|
||||||
|
defaultValue={editing ? (target.shift.labelId ?? '') : ''}
|
||||||
|
className="h-8 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||||
|
>
|
||||||
|
<option value="">Aucune étiquette</option>
|
||||||
|
{labels.map((label) => (
|
||||||
|
<option key={label.id} value={label.id}>
|
||||||
|
{label.name}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</Field>
|
||||||
|
|
||||||
|
<Field label="Notes" htmlFor="composer-note">
|
||||||
|
<input
|
||||||
|
id="composer-note"
|
||||||
|
name="note"
|
||||||
|
type="text"
|
||||||
|
maxLength={500}
|
||||||
|
defaultValue={editing ? (target.shift.note ?? '') : ''}
|
||||||
|
placeholder="Consigne du jour"
|
||||||
|
className="h-8 w-56 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
||||||
|
/>
|
||||||
|
</Field>
|
||||||
|
|
||||||
{editing ? null : (
|
{editing ? null : (
|
||||||
<Field label="Poste" htmlFor="composer-label">
|
<RepeatDays dates={dates} days={days} selected={localDate} />
|
||||||
<select
|
|
||||||
id="composer-label"
|
|
||||||
name="labelId"
|
|
||||||
className="h-8 rounded-2 border border-line-2 bg-surface px-2 text-sm text-ink-1"
|
|
||||||
>
|
|
||||||
{labels.map((label) => (
|
|
||||||
<option key={label.id} value={label.id}>
|
|
||||||
{label.name}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</Field>
|
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
@@ -484,6 +522,67 @@ function ShiftComposer({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Répétition du créneau sur plusieurs jours.
|
||||||
|
*
|
||||||
|
* Le jour ouvert est coché et **verrouillé** : on ne crée pas « ailleurs qu'ici
|
||||||
|
* » depuis la case d'un jour donné, et le décocher laisserait un formulaire qui
|
||||||
|
* ne produit rien tout en paraissant valide.
|
||||||
|
*
|
||||||
|
* La création est tout ou rien côté serveur : un jour refusé — période close,
|
||||||
|
* chevauchement — annule les autres. Une répétition à demi appliquée laisserait
|
||||||
|
* un planning que personne n'a voulu.
|
||||||
|
*/
|
||||||
|
function RepeatDays({
|
||||||
|
dates,
|
||||||
|
days,
|
||||||
|
selected,
|
||||||
|
}: {
|
||||||
|
dates: string[];
|
||||||
|
days: string[];
|
||||||
|
selected: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<fieldset className="flex flex-col gap-1">
|
||||||
|
<legend className="text-micro font-medium tracking-[0.04em] text-ink-3 uppercase">
|
||||||
|
Répéter sur
|
||||||
|
</legend>
|
||||||
|
<div className="flex flex-wrap gap-1">
|
||||||
|
{dates.map((date, index) => {
|
||||||
|
const locked = date === selected;
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={date}
|
||||||
|
className={cx(
|
||||||
|
'flex cursor-pointer items-center gap-1 rounded-2 border px-2 py-1 text-micro',
|
||||||
|
locked
|
||||||
|
? 'border-accent bg-accent-soft text-accent-soft-ink'
|
||||||
|
: 'border-line-2 bg-surface text-ink-2 hover:bg-surface-2',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
name="localDates"
|
||||||
|
value={date}
|
||||||
|
defaultChecked={locked}
|
||||||
|
disabled={locked}
|
||||||
|
className="size-3"
|
||||||
|
/>
|
||||||
|
{/* Un champ désactivé n'est pas envoyé : le jour ouvert voyage
|
||||||
|
donc dans un champ caché, sans quoi cocher zéro case
|
||||||
|
produirait une création vide. */}
|
||||||
|
{locked ? (
|
||||||
|
<input type="hidden" name="localDates" value={date} />
|
||||||
|
) : null}
|
||||||
|
{(days[index] ?? date).slice(0, 3)}
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
function Field({
|
function Field({
|
||||||
label,
|
label,
|
||||||
htmlFor,
|
htmlFor,
|
||||||
|
|||||||
@@ -18,7 +18,10 @@ export interface BoardShiftInput {
|
|||||||
startAt: Date;
|
startAt: Date;
|
||||||
endAt: Date;
|
endAt: Date;
|
||||||
breakMinutes: number;
|
breakMinutes: number;
|
||||||
|
mealCount: number;
|
||||||
poste: PosteCode;
|
poste: PosteCode;
|
||||||
|
/** Étiquette telle qu'enregistrée, pour rouvrir le créneau sur la sienne. */
|
||||||
|
labelId: string | null;
|
||||||
isValidated: boolean;
|
isValidated: boolean;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
}
|
}
|
||||||
@@ -30,6 +33,8 @@ export interface BoardShift {
|
|||||||
time: string;
|
time: string;
|
||||||
minutes: number;
|
minutes: number;
|
||||||
breakMinutes: number;
|
breakMinutes: number;
|
||||||
|
mealCount: number;
|
||||||
|
labelId: string | null;
|
||||||
state: ShiftState;
|
state: ShiftState;
|
||||||
note: string | null;
|
note: string | null;
|
||||||
}
|
}
|
||||||
@@ -139,6 +144,8 @@ export function buildRows(
|
|||||||
time: `${zonedClock(shift.startAt, timeZone)}–${zonedClock(shift.endAt, timeZone)}`,
|
time: `${zonedClock(shift.startAt, timeZone)}–${zonedClock(shift.endAt, timeZone)}`,
|
||||||
minutes,
|
minutes,
|
||||||
breakMinutes: shift.breakMinutes,
|
breakMinutes: shift.breakMinutes,
|
||||||
|
mealCount: shift.mealCount,
|
||||||
|
labelId: shift.labelId,
|
||||||
state: shiftState(shift.isValidated, isPublished, shift.membershipId),
|
state: shiftState(shift.isValidated, isPublished, shift.membershipId),
|
||||||
note: shift.note,
|
note: shift.note,
|
||||||
};
|
};
|
||||||
|
|||||||
+108
-61
@@ -46,14 +46,27 @@ class ValidationError extends Error {}
|
|||||||
|
|
||||||
const HOUR = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
const HOUR = /^([01]\d|2[0-3]):([0-5]\d)$/;
|
||||||
|
|
||||||
|
const ISO_DATE = /^\d{4}-\d{2}-\d{2}$/;
|
||||||
|
|
||||||
const shiftInput = z.object({
|
const shiftInput = z.object({
|
||||||
teamId: z.string().min(1),
|
teamId: z.string().min(1),
|
||||||
week: z.string().min(1),
|
week: z.string().min(1),
|
||||||
membershipId: z.string().optional(),
|
membershipId: z.string().optional(),
|
||||||
localDate: z.string().regex(/^\d{4}-\d{2}-\d{2}$/, 'Date invalide'),
|
/**
|
||||||
|
* Jours à créer, un créneau par jour.
|
||||||
|
*
|
||||||
|
* Un même horaire se répète d'un jour à l'autre bien plus souvent qu'il ne
|
||||||
|
* varie : ressaisir cinq fois « 9 h–17 h, pause 30 » est le geste le plus
|
||||||
|
* répété de la semaine, et le plus exposé à la faute de frappe.
|
||||||
|
*/
|
||||||
|
localDates: z
|
||||||
|
.array(z.string().regex(ISO_DATE, 'Date invalide'))
|
||||||
|
.min(1, 'Sélectionnez au moins un jour')
|
||||||
|
.max(7),
|
||||||
start: z.string().regex(HOUR, 'Heure de début invalide'),
|
start: z.string().regex(HOUR, 'Heure de début invalide'),
|
||||||
end: z.string().regex(HOUR, 'Heure de fin invalide'),
|
end: z.string().regex(HOUR, 'Heure de fin invalide'),
|
||||||
breakMinutes: z.coerce.number().int().min(0).max(600).default(0),
|
breakMinutes: z.coerce.number().int().min(0).max(600).default(0),
|
||||||
|
mealCount: z.coerce.number().int().min(0).max(5).default(0),
|
||||||
labelId: z.string().optional(),
|
labelId: z.string().optional(),
|
||||||
note: z.string().trim().max(500).optional(),
|
note: z.string().trim().max(500).optional(),
|
||||||
});
|
});
|
||||||
@@ -99,14 +112,23 @@ export async function createShiftAction(
|
|||||||
_previous: PlanningActionState,
|
_previous: PlanningActionState,
|
||||||
formData: FormData,
|
formData: FormData,
|
||||||
): Promise<PlanningActionState> {
|
): Promise<PlanningActionState> {
|
||||||
|
// Les cases de répétition portent le même nom : la case du jour ouvert est
|
||||||
|
// cochée d'office, si bien qu'un envoi sans répétition renvoie exactement une
|
||||||
|
// date. Le repli sur `localDate` couvre le formulaire d'un client qui aurait
|
||||||
|
// désactivé JavaScript.
|
||||||
|
const checked = formData.getAll('localDates').map(String).filter(Boolean);
|
||||||
|
const single = String(formData.get('localDate') ?? '');
|
||||||
|
const localDates = checked.length > 0 ? [...new Set(checked)] : single ? [single] : [];
|
||||||
|
|
||||||
const parsed = shiftInput.safeParse({
|
const parsed = shiftInput.safeParse({
|
||||||
teamId: formData.get('teamId'),
|
teamId: formData.get('teamId'),
|
||||||
week: formData.get('week'),
|
week: formData.get('week'),
|
||||||
membershipId: formData.get('membershipId') || undefined,
|
membershipId: formData.get('membershipId') || undefined,
|
||||||
localDate: formData.get('localDate'),
|
localDates,
|
||||||
start: formData.get('start'),
|
start: formData.get('start'),
|
||||||
end: formData.get('end'),
|
end: formData.get('end'),
|
||||||
breakMinutes: formData.get('breakMinutes') || 0,
|
breakMinutes: formData.get('breakMinutes') || 0,
|
||||||
|
mealCount: formData.get('mealCount') || 0,
|
||||||
labelId: formData.get('labelId') || undefined,
|
labelId: formData.get('labelId') || undefined,
|
||||||
note: formData.get('note') || undefined,
|
note: formData.get('note') || undefined,
|
||||||
});
|
});
|
||||||
@@ -122,37 +144,11 @@ export async function createShiftAction(
|
|||||||
await mutate('planning.create', async (db, actor) => {
|
await mutate('planning.create', async (db, actor) => {
|
||||||
const { team, location } = await loadTeamContext(db, parsed.data.teamId);
|
const { team, location } = await loadTeamContext(db, parsed.data.teamId);
|
||||||
|
|
||||||
const startAt = zonedInstant(
|
|
||||||
parsed.data.localDate,
|
|
||||||
parsed.data.start,
|
|
||||||
location.timezone,
|
|
||||||
);
|
|
||||||
let endAt = zonedInstant(
|
|
||||||
parsed.data.localDate,
|
|
||||||
parsed.data.end,
|
|
||||||
location.timezone,
|
|
||||||
);
|
|
||||||
// Fin avant début = créneau de nuit : il finit le lendemain. Le refuser
|
|
||||||
// interdirait de planifier un inventaire 22 h–02 h.
|
|
||||||
if (endAt <= startAt) endAt = new Date(endAt.getTime() + 86_400_000);
|
|
||||||
|
|
||||||
// Un mois transmis au cabinet ne se modifie pas par inadvertance : le
|
// Un mois transmis au cabinet ne se modifie pas par inadvertance : le
|
||||||
// contrôle passe **avant** l'écriture.
|
// contrôle passe **avant** toute écriture, et porte sur tous les jours
|
||||||
await assertPeriodOpen(db, location.id, [parsed.data.localDate]);
|
// demandés — répéter sur cinq jours ne doit pas en glisser un dans une
|
||||||
|
// période close.
|
||||||
const worked = shiftMinutes(startAt, endAt, parsed.data.breakMinutes);
|
await assertPeriodOpen(db, location.id, parsed.data.localDates);
|
||||||
if (worked <= 0) {
|
|
||||||
throw new ValidationError(
|
|
||||||
'La pause dépasse la durée du créneau — rien ne serait travaillé.',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { from, to } = weekBounds(week, location.timezone);
|
|
||||||
if (startAt < from || startAt >= to) {
|
|
||||||
throw new ValidationError(
|
|
||||||
"Ce créneau ne commence pas dans la semaine affichée.",
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const schedule = await ensureSchedule(
|
const schedule = await ensureSchedule(
|
||||||
db,
|
db,
|
||||||
@@ -166,37 +162,76 @@ export async function createShiftAction(
|
|||||||
assertMayEditPublished(actor);
|
assertMayEditPublished(actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (parsed.data.membershipId) {
|
const { from, to } = weekBounds(week, location.timezone);
|
||||||
await assertNoOverlap(db, parsed.data.membershipId, startAt, endAt, null);
|
|
||||||
|
// La répétition est **tout ou rien**. Un jour refusé annule la
|
||||||
|
// transaction entière : une répétition à demi appliquée laisserait un
|
||||||
|
// planning que personne n'a voulu, et qu'il faudrait défaire à la main
|
||||||
|
// pour le refaire.
|
||||||
|
for (const localDate of parsed.data.localDates) {
|
||||||
|
const startAt = zonedInstant(
|
||||||
|
localDate,
|
||||||
|
parsed.data.start,
|
||||||
|
location.timezone,
|
||||||
|
);
|
||||||
|
let endAt = zonedInstant(localDate, parsed.data.end, location.timezone);
|
||||||
|
// Fin avant début = créneau de nuit : il finit le lendemain. Le refuser
|
||||||
|
// interdirait de planifier un inventaire 22 h–02 h.
|
||||||
|
if (endAt <= startAt) endAt = new Date(endAt.getTime() + 86_400_000);
|
||||||
|
|
||||||
|
const worked = shiftMinutes(startAt, endAt, parsed.data.breakMinutes);
|
||||||
|
if (worked <= 0) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'La pause dépasse la durée du créneau — rien ne serait travaillé.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startAt < from || startAt >= to) {
|
||||||
|
throw new ValidationError(
|
||||||
|
'Un des jours demandés ne commence pas dans la semaine affichée.',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parsed.data.membershipId) {
|
||||||
|
await assertNoOverlap(
|
||||||
|
db,
|
||||||
|
parsed.data.membershipId,
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const created = await db.shift.create({
|
||||||
|
data: {
|
||||||
|
weeklyScheduleId: schedule.id,
|
||||||
|
membershipId: parsed.data.membershipId ?? null,
|
||||||
|
localDate: new Date(`${localDate}T00:00:00Z`),
|
||||||
|
startAt,
|
||||||
|
endAt,
|
||||||
|
breakMinutes: parsed.data.breakMinutes,
|
||||||
|
mealCount: parsed.data.mealCount,
|
||||||
|
labelId: parsed.data.labelId ?? null,
|
||||||
|
note: parsed.data.note ?? null,
|
||||||
|
} as never,
|
||||||
|
});
|
||||||
|
|
||||||
|
await recordAudit(db, {
|
||||||
|
actorMembershipId: actor.membershipId,
|
||||||
|
action: 'planning.shift.create',
|
||||||
|
entityType: 'Shift',
|
||||||
|
entityId: created.id,
|
||||||
|
after: {
|
||||||
|
teamId: team.id,
|
||||||
|
membershipId: created.membershipId,
|
||||||
|
startAt: startAt.toISOString(),
|
||||||
|
endAt: endAt.toISOString(),
|
||||||
|
minutes: worked,
|
||||||
|
mealCount: created.mealCount,
|
||||||
|
},
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const created = await db.shift.create({
|
|
||||||
data: {
|
|
||||||
weeklyScheduleId: schedule.id,
|
|
||||||
membershipId: parsed.data.membershipId ?? null,
|
|
||||||
localDate: new Date(`${parsed.data.localDate}T00:00:00Z`),
|
|
||||||
startAt,
|
|
||||||
endAt,
|
|
||||||
breakMinutes: parsed.data.breakMinutes,
|
|
||||||
labelId: parsed.data.labelId ?? null,
|
|
||||||
note: parsed.data.note ?? null,
|
|
||||||
} as never,
|
|
||||||
});
|
|
||||||
|
|
||||||
await recordAudit(db, {
|
|
||||||
actorMembershipId: actor.membershipId,
|
|
||||||
action: 'planning.shift.create',
|
|
||||||
entityType: 'Shift',
|
|
||||||
entityId: created.id,
|
|
||||||
after: {
|
|
||||||
teamId: team.id,
|
|
||||||
membershipId: created.membershipId,
|
|
||||||
startAt: startAt.toISOString(),
|
|
||||||
endAt: endAt.toISOString(),
|
|
||||||
minutes: worked,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
await assertNoBlocking(db, schedule.id);
|
await assertNoBlocking(db, schedule.id);
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -214,6 +249,12 @@ const moveInput = z.object({
|
|||||||
start: z.string().regex(HOUR, 'Heure de début invalide'),
|
start: z.string().regex(HOUR, 'Heure de début invalide'),
|
||||||
end: z.string().regex(HOUR, 'Heure de fin invalide'),
|
end: z.string().regex(HOUR, 'Heure de fin invalide'),
|
||||||
breakMinutes: z.coerce.number().int().min(0).max(600).default(0),
|
breakMinutes: z.coerce.number().int().min(0).max(600).default(0),
|
||||||
|
mealCount: z.coerce.number().int().min(0).max(5).default(0),
|
||||||
|
// Modifiables à la correction comme à la création : une étiquette posée de
|
||||||
|
// travers se corrigeait jusqu'ici en supprimant le créneau pour le refaire,
|
||||||
|
// ce qui perdait sa validation et son historique.
|
||||||
|
labelId: z.string().optional(),
|
||||||
|
note: z.string().trim().max(500).optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export async function updateShiftAction(
|
export async function updateShiftAction(
|
||||||
@@ -227,6 +268,9 @@ export async function updateShiftAction(
|
|||||||
start: formData.get('start'),
|
start: formData.get('start'),
|
||||||
end: formData.get('end'),
|
end: formData.get('end'),
|
||||||
breakMinutes: formData.get('breakMinutes') || 0,
|
breakMinutes: formData.get('breakMinutes') || 0,
|
||||||
|
mealCount: formData.get('mealCount') || 0,
|
||||||
|
labelId: formData.get('labelId') || undefined,
|
||||||
|
note: formData.get('note') || undefined,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!parsed.success) {
|
if (!parsed.success) {
|
||||||
@@ -287,6 +331,9 @@ export async function updateShiftAction(
|
|||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
breakMinutes: parsed.data.breakMinutes,
|
breakMinutes: parsed.data.breakMinutes,
|
||||||
|
mealCount: parsed.data.mealCount,
|
||||||
|
labelId: parsed.data.labelId ?? null,
|
||||||
|
note: parsed.data.note ?? null,
|
||||||
version: { increment: 1 },
|
version: { increment: 1 },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -177,6 +177,7 @@ export async function getWeekBoard(
|
|||||||
startAt: true,
|
startAt: true,
|
||||||
endAt: true,
|
endAt: true,
|
||||||
breakMinutes: true,
|
breakMinutes: true,
|
||||||
|
mealCount: true,
|
||||||
isValidated: true,
|
isValidated: true,
|
||||||
note: true,
|
note: true,
|
||||||
labelId: true,
|
labelId: true,
|
||||||
@@ -240,6 +241,8 @@ export async function getWeekBoard(
|
|||||||
startAt: shift.startAt,
|
startAt: shift.startAt,
|
||||||
endAt: shift.endAt,
|
endAt: shift.endAt,
|
||||||
breakMinutes: shift.breakMinutes,
|
breakMinutes: shift.breakMinutes,
|
||||||
|
mealCount: shift.mealCount,
|
||||||
|
labelId: shift.labelId,
|
||||||
poste: posteOf(paletteById.get(shift.labelId ?? '')),
|
poste: posteOf(paletteById.get(shift.labelId ?? '')),
|
||||||
isValidated: shift.isValidated,
|
isValidated: shift.isValidated,
|
||||||
note: shift.note,
|
note: shift.note,
|
||||||
|
|||||||
@@ -33,7 +33,9 @@ const shift = (
|
|||||||
startAt: zonedInstant(date, start, TZ),
|
startAt: zonedInstant(date, start, TZ),
|
||||||
endAt: zonedInstant(date, end, TZ),
|
endAt: zonedInstant(date, end, TZ),
|
||||||
breakMinutes: 0,
|
breakMinutes: 0,
|
||||||
|
mealCount: 0,
|
||||||
poste: 'vte',
|
poste: 'vte',
|
||||||
|
labelId: null,
|
||||||
isValidated: false,
|
isValidated: false,
|
||||||
note: null,
|
note: null,
|
||||||
...over,
|
...over,
|
||||||
@@ -89,7 +91,9 @@ describe('buildRows', () => {
|
|||||||
startAt,
|
startAt,
|
||||||
endAt,
|
endAt,
|
||||||
breakMinutes: 0,
|
breakMinutes: 0,
|
||||||
|
mealCount: 0,
|
||||||
poste: 'inv',
|
poste: 'inv',
|
||||||
|
labelId: null,
|
||||||
isValidated: false,
|
isValidated: false,
|
||||||
note: null,
|
note: null,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ const shift: BoardRow['days'][number][number] = {
|
|||||||
time: '09:00–17:00',
|
time: '09:00–17:00',
|
||||||
minutes: 480,
|
minutes: 480,
|
||||||
breakMinutes: 30,
|
breakMinutes: 30,
|
||||||
|
mealCount: 0,
|
||||||
|
labelId: null,
|
||||||
state: 'published',
|
state: 'published',
|
||||||
note: null,
|
note: null,
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in new issue
Block a user