Let a day removed from a period stay removed
Taking one day out of a period the planner placed did nothing that lasted: the period still asked for a full week, so the next sync put the day straight back. The option was disabled rather than lying about it. A removed day is now recorded as its own thing — no leave for this parent, this child, this date — and the planner honours it on every later sync. It is kept apart from the working-day overrides, which answer a different question about the calendar rather than about the plan. The day belongs to the parent whose band was clicked, so removing one parent's day leaves their partner's alone; where both parents were to be off on one funding arrangement, the partner is simply funded on their own instead. A removed day shortens what the period is asking for rather than counting as a day the plan failed to find, so the advice about closing a shortfall does not start arguing for a day nobody wants. Nothing is removed without a way back: the toast undoes it, and the day itself is marked in the calendar and offers to restore. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Ei7gCZzdd751uZtPzb1Pep
This commit is contained in:
@@ -10,6 +10,7 @@ import {
|
||||
useLeaveWishes,
|
||||
useWorkspaceMembers,
|
||||
usePlanSync,
|
||||
usePlannedDayExclusions,
|
||||
} from '@/hooks'
|
||||
import type {
|
||||
PlannedLeaveDay,
|
||||
@@ -308,6 +309,9 @@ export function Calendar(): ReactNode {
|
||||
const wishesHook = useLeaveWishes(workspace?.id ?? null)
|
||||
const membersHook = useWorkspaceMembers(workspace?.id ?? null)
|
||||
const syncHook = usePlanSync()
|
||||
// The days the user removed by hand. Read for the calendar's badges, and
|
||||
// written by the popover's "this day" remove — see `excludeDay`.
|
||||
const exclusionsHook = usePlannedDayExclusions(workspace?.id ?? null)
|
||||
|
||||
const calendarDaysByDate = useMemo(() => {
|
||||
const map = new Map<string, CalendarDayEntry>()
|
||||
@@ -317,6 +321,22 @@ export function Calendar(): ReactNode {
|
||||
return map
|
||||
}, [calendarHook.data])
|
||||
|
||||
// Any parent's removal marks the date: the mark says "someone deliberately
|
||||
// took a day out here", which is what stops the hole reading as a bug. Whose
|
||||
// day it was is shown in the day popover, where there is room to say it.
|
||||
const excludedDates = useMemo(
|
||||
() => new Set(exclusionsHook.data.map((e) => e.date)),
|
||||
[exclusionsHook.data],
|
||||
)
|
||||
|
||||
/** The display name for a band's owner. Used wherever the UI has to say
|
||||
* WHOSE day something is — above all the "this day" remove, which takes one
|
||||
* parent's day and leaves the partner's. Falls back the same way
|
||||
* CalendarWeek's band labels do. */
|
||||
const resolveParentName = (uuid: string): string =>
|
||||
membersHook.data.find((m) => m.uuid === uuid)?.name ??
|
||||
(uuid === personalDataUuid ? (parentName ?? t('calendar.you')) : t('calendar.partner'))
|
||||
|
||||
const showToast = useCallback((message: string, undo: CalendarToast['undo']) => {
|
||||
setToast({ message, undo })
|
||||
}, [])
|
||||
@@ -836,6 +856,72 @@ export function Calendar(): ReactNode {
|
||||
})
|
||||
}
|
||||
|
||||
/** §"removing a single day should stay permanent" — the planner-placed
|
||||
* counterpart of `deleteAdhocRange`. An auto-placed day cannot simply be
|
||||
* deleted: the wish behind it still asks for the whole window, so the next
|
||||
* sync would re-derive it. Recording a `PlannedDayExclusion` is what makes
|
||||
* the removal outlive the sync.
|
||||
*
|
||||
* Keyed on the band's own parent, so a both-parents period loses only the
|
||||
* band that was clicked and the partner keeps their day — the toast names
|
||||
* the parent so that choice is never a silent one. Undo deletes the
|
||||
* exclusion and re-syncs, which puts the day back where it was.
|
||||
*/
|
||||
const excludeDay = async (
|
||||
workspaceId: string,
|
||||
band: BandClickInfo,
|
||||
parentLabel: string,
|
||||
) => {
|
||||
const childId =
|
||||
(plannedDaysHook.data ?? []).find(
|
||||
(pd) => pd.date === band.clickedDate && pd.parentPersonalDataUuid === band.parentPersonalDataUuid,
|
||||
)?.childId ?? selectedChildId
|
||||
if (!childId) {
|
||||
closePeriodPopover()
|
||||
return
|
||||
}
|
||||
|
||||
const created = await exclusionsHook.exclude({
|
||||
workspaceId,
|
||||
childId,
|
||||
parentPersonalDataUuid: band.parentPersonalDataUuid,
|
||||
date: band.clickedDate,
|
||||
})
|
||||
closePeriodPopover()
|
||||
await runSync(workspaceId, { silent: true })
|
||||
|
||||
const dateLabel = formatDateForDisplay(band.clickedDate, locale)
|
||||
showToast(t('calendar.toastDayExcluded', { date: dateLabel, parent: parentLabel }), {
|
||||
label: t('calendar.undoButton'),
|
||||
run: async () => {
|
||||
await exclusionsHook.restore(created.id)
|
||||
await runSync(workspaceId, { silent: true })
|
||||
showToast(t('calendar.toastDayRestored', { date: dateLabel, parent: parentLabel }), null)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
/** The day cell's way back, for when the undo toast is long gone: the day
|
||||
* popover lists this date's removals (one per parent) with a restore
|
||||
* button. See `excludeDay` for the other direction. */
|
||||
const handleRestoreExcluded = async (exclusionId: string) => {
|
||||
if (!workspace?.id) return
|
||||
const entry = exclusionsHook.data.find((e) => e.id === exclusionId)
|
||||
await exclusionsHook.restore(exclusionId)
|
||||
setIsModalOpen(false)
|
||||
setSelectedDates(new Set())
|
||||
await runSync(workspace.id, { silent: true })
|
||||
if (entry) {
|
||||
showToast(
|
||||
t('calendar.toastDayRestored', {
|
||||
date: formatDateForDisplay(entry.date, locale),
|
||||
parent: resolveParentName(entry.parentPersonalDataUuid),
|
||||
}),
|
||||
null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const handleDeletePeriod = async (scope: PeriodDeleteScope) => {
|
||||
if (!workspace?.id || !activeBand) return
|
||||
|
||||
@@ -851,11 +937,20 @@ export function Calendar(): ReactNode {
|
||||
|
||||
if (scope === 'day') {
|
||||
if (activeBand.wishId) {
|
||||
// Disabled in the UI (would just reappear on the next sync).
|
||||
closePeriodPopover()
|
||||
// A planner-placed day: deleting the row would not survive the next
|
||||
// sync, so record the removal instead.
|
||||
await excludeDay(
|
||||
workspace.id,
|
||||
activeBand,
|
||||
resolveParentName(activeBand.parentPersonalDataUuid),
|
||||
)
|
||||
return
|
||||
}
|
||||
await deleteAdhocRange(activeBand.startDate, activeBand.startDate, activeBand.parentPersonalDataUuid)
|
||||
await deleteAdhocRange(
|
||||
activeBand.clickedDate,
|
||||
activeBand.clickedDate,
|
||||
activeBand.parentPersonalDataUuid,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -1100,6 +1195,7 @@ export function Calendar(): ReactNode {
|
||||
onPointerDown={handlePointerDown}
|
||||
onPointerEnter={handlePointerEnter}
|
||||
calendarDaysByDate={calendarDaysByDate}
|
||||
excludedDates={excludedDates}
|
||||
onBandClick={handleBandClick}
|
||||
flashWishId={flashWishId}
|
||||
monthMaxLaneCount={monthMaxLaneCount}
|
||||
@@ -1156,6 +1252,10 @@ export function Calendar(): ReactNode {
|
||||
onSaveWish={handleSaveWish}
|
||||
onSavePinned={handleSavePinnedDay}
|
||||
onDelete={getExistingDay(firstSelectedDate, advancedParentId) ? handleDeletePlannedDay : undefined}
|
||||
excludedEntries={exclusionsHook.data
|
||||
.filter((e) => e.date === firstSelectedDate)
|
||||
.map((e) => ({ id: e.id, parentName: resolveParentName(e.parentPersonalDataUuid) }))}
|
||||
onRestoreExcluded={handleRestoreExcluded}
|
||||
onClose={() => {
|
||||
setIsModalOpen(false)
|
||||
setSelectedDates(new Set())
|
||||
@@ -1174,6 +1274,8 @@ export function Calendar(): ReactNode {
|
||||
wish={activeWish}
|
||||
startDate={activeBand.startDate}
|
||||
endDate={activeBand.endDate}
|
||||
removeDayLabel={formatDateForDisplay(activeBand.clickedDate, locale)}
|
||||
removeDayParentName={resolveParentName(activeBand.parentPersonalDataUuid)}
|
||||
parents={parentsForModals}
|
||||
isSaving={isPeriodSaving}
|
||||
error={periodError}
|
||||
|
||||
Reference in New Issue
Block a user