Remove a period's days along with the period
Deleting a period left every day it had funded behind. The link from a day back to its period was set to null the instant the period was deleted, so the days became untraceable before anything could clean them up, and the next recalculation had no way to know they no longer belonged to anything. Undo had the same fault for the same reason: the period went, the days stayed, and a reload showed them still there. The days are now removed with the period, before the link can be broken. A day pinned by hand goes too — pinning keeps the planner from quietly retyping a day, not from being deleted when you delete the period it belongs to, which is already how an unplanned band behaves.
This commit is contained in:
@@ -127,16 +127,43 @@ function summarizeChanges(changes: PlanChange[], wishes: LeaveWish[], t: Transla
|
||||
* carrying `wish_name`/`wish_name_message_key`) — falling back to the plain
|
||||
* `shortfallDays` count when there's nothing richer to say. `funded`
|
||||
* distinguishes "nothing could be placed" from "only part of it fit", the
|
||||
* two phrasings the "sync that changes nothing must say why" fix needs. */
|
||||
function summarizeOutcomeShortfall(outcome: WindowOutcome, funded: boolean, t: Translate): string {
|
||||
* two phrasings the "sync that changes nothing must say why" fix needs.
|
||||
*
|
||||
* `wish` is the touched wish itself (not just the outcome), because the
|
||||
* outcome only ever carries `weeksWished`/`weeksFunded` — the backend's
|
||||
* week-rounded view. A calendar-drawn period sets `targetDays` (an exact
|
||||
* day count); `targetWeeks` on that same wish is just `Math.ceil(days / 7)`,
|
||||
* a leftover the API still requires, never what the user actually asked
|
||||
* for. Reporting `weeksWished` for those wishes says "3 week(s)" for a
|
||||
* 7-day drag. When `targetDays` is set, report days instead — the figure
|
||||
* the user actually asked for — and only fall back to weeks for wishes that
|
||||
* never had a day-granular target (e.g. the preset rail, which only ever
|
||||
* sends `targetWeeks`). */
|
||||
function summarizeOutcomeShortfall(
|
||||
outcome: WindowOutcome,
|
||||
funded: boolean,
|
||||
wish: LeaveWish | undefined,
|
||||
t: Translate,
|
||||
): string {
|
||||
const name = resolveWishNameFromData(outcome.data, t) || t('calendar.toastGenericPeriod')
|
||||
const params = {
|
||||
const base = {
|
||||
name,
|
||||
start: formatDateForDisplay(outcome.startDate),
|
||||
end: formatDateForDisplay(outcome.endDate),
|
||||
shortfallDays: outcome.shortfallDays,
|
||||
}
|
||||
if (wish?.targetDays != null) {
|
||||
const params = {
|
||||
...base,
|
||||
daysWished: wish.targetDays,
|
||||
daysFunded: wish.targetDays - outcome.shortfallDays,
|
||||
}
|
||||
return t(funded ? 'calendar.toastShortfallPartialDays' : 'calendar.toastShortfallNoneDays', params)
|
||||
}
|
||||
const params = {
|
||||
...base,
|
||||
weeksWished: outcome.weeksWished,
|
||||
weeksFunded: outcome.weeksFunded,
|
||||
shortfallDays: outcome.shortfallDays,
|
||||
}
|
||||
return t(funded ? 'calendar.toastShortfallPartial' : 'calendar.toastShortfallNone', params)
|
||||
}
|
||||
@@ -144,13 +171,22 @@ function summarizeOutcomeShortfall(outcome: WindowOutcome, funded: boolean, t: T
|
||||
/** Builds the full sync toast message: what changed (existing summary), why
|
||||
* the wish just touched (if any) fell short of what was wished, and any
|
||||
* rejections the apply step returned — a genuine no-op sync (nothing to
|
||||
* report on any of the three) still renders `null`, so it stays silent. */
|
||||
* report on any of the three) still renders `null`, so it stays silent.
|
||||
*
|
||||
* `watchWish`, when given, is used in place of looking `watchWishId` up in
|
||||
* `wishes` — needed right after creating a wish, where `wishes` is whatever
|
||||
* `wishesHook.data` was as of this closure's own render (captured before
|
||||
* the create's background refetch lands) and so never actually contains a
|
||||
* wish created moments ago. Passing the just-created wish object directly
|
||||
* sidesteps that; every other caller (editing/re-syncing an *existing*
|
||||
* wish) can leave it out and rely on the `wishes` lookup as before. */
|
||||
function buildSyncToastMessage(
|
||||
result: PlanSyncResponse,
|
||||
wishes: LeaveWish[],
|
||||
members: PersonalData[],
|
||||
watchWishId: string | undefined,
|
||||
t: Translate,
|
||||
watchWish?: LeaveWish,
|
||||
): string | null {
|
||||
const parts: string[] = []
|
||||
|
||||
@@ -161,7 +197,8 @@ function buildSyncToastMessage(
|
||||
const outcome = result.proposal.outcomes.find((o) => o.wishId === watchWishId)
|
||||
if (outcome && outcome.shortfallDays > 0) {
|
||||
const funded = result.changes.some((c) => c.newWishId === watchWishId || c.oldWishId === watchWishId)
|
||||
parts.push(summarizeOutcomeShortfall(outcome, funded, t))
|
||||
const wish = watchWish ?? wishes.find((w) => w.id === watchWishId)
|
||||
parts.push(summarizeOutcomeShortfall(outcome, funded, wish, t))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,6 +319,46 @@ export function Calendar(): ReactNode {
|
||||
void runSync(workspace.id, { silent: true })
|
||||
}, [workspace?.id, runSync])
|
||||
|
||||
// Runs the sync that persists a just-created wish, then reports it and
|
||||
// wires up undo — split out from a plain `runSync(..., { undo })` call
|
||||
// because undo here needs to pass the just-created wish's own fields
|
||||
// (`created`) to `buildSyncToastMessage` rather than looking `wishId` up
|
||||
// in `wishesHook.data`, which is whatever it was as of this closure's own
|
||||
// render (captured before the create's background refetch lands) and so
|
||||
// never actually contains a wish created moments ago.
|
||||
//
|
||||
// Undo itself is just "delete the wish, re-sync": the backend deletes
|
||||
// every `planned_leave_day` row the wish produced as part of deleting the
|
||||
// wish (`LeaveWishService.delete()`), so there is nothing left here for
|
||||
// the frontend to individually track or clean up.
|
||||
const runSyncAfterWishCreate = useCallback(
|
||||
async (workspaceId: string, created: LeaveWish): Promise<PlanSyncResponse | undefined> => {
|
||||
const wishId = created.id
|
||||
const result = await runSync(workspaceId, { silent: true })
|
||||
if (!result) return result
|
||||
|
||||
const message = buildSyncToastMessage(
|
||||
result,
|
||||
wishesHook.data,
|
||||
membersHook.data,
|
||||
wishId,
|
||||
t,
|
||||
created,
|
||||
)
|
||||
if (message) {
|
||||
showToast(message, {
|
||||
label: t('calendar.undoButton'),
|
||||
run: async () => {
|
||||
await wishesHook.remove(wishId)
|
||||
await runSync(workspaceId, { silent: true })
|
||||
},
|
||||
})
|
||||
}
|
||||
return result
|
||||
},
|
||||
[runSync, wishesHook, membersHook.data, t, showToast],
|
||||
)
|
||||
|
||||
const handleCreatePeriodFromDrag = useCallback(
|
||||
async (dates: string[]) => {
|
||||
setSelectedDates(new Set())
|
||||
@@ -316,23 +393,22 @@ export function Calendar(): ReactNode {
|
||||
parentPersonalDataUuid: personalDataUuid,
|
||||
windowMode: 'fixed',
|
||||
})
|
||||
await runSync(workspace.id, {
|
||||
watchWishId: created.id,
|
||||
undo: {
|
||||
label: t('calendar.undoButton'),
|
||||
run: async () => {
|
||||
await wishesHook.remove(created.id)
|
||||
await runSync(workspace.id, { silent: true })
|
||||
},
|
||||
},
|
||||
})
|
||||
await runSyncAfterWishCreate(workspace.id, created)
|
||||
} catch (err) {
|
||||
showToast(err instanceof Error ? err.message : t('calendar.periodCreateError'), null)
|
||||
} finally {
|
||||
setIsCreatingPeriod(false)
|
||||
}
|
||||
},
|
||||
[workspace?.id, selectedChildId, personalDataUuid, wishesHook, t, runSync, showToast],
|
||||
[
|
||||
workspace?.id,
|
||||
selectedChildId,
|
||||
personalDataUuid,
|
||||
wishesHook,
|
||||
t,
|
||||
runSyncAfterWishCreate,
|
||||
showToast,
|
||||
],
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -688,16 +764,7 @@ export function Calendar(): ReactNode {
|
||||
windowMode: 'flexible',
|
||||
})
|
||||
|
||||
const result = await runSync(workspace.id, {
|
||||
watchWishId: created.id,
|
||||
undo: {
|
||||
label: t('calendar.undoButton'),
|
||||
run: async () => {
|
||||
await wishesHook.remove(created.id)
|
||||
await runSync(workspace.id, { silent: true })
|
||||
},
|
||||
},
|
||||
})
|
||||
const result = await runSyncAfterWishCreate(workspace.id, created)
|
||||
|
||||
if (result) {
|
||||
const placedDates = result.changes
|
||||
|
||||
Reference in New Issue
Block a user