Merge branch 'worktree-make-planner-explaining-itself'
Clicking a planned day raises the calendar toast with a plain-language
reason (sv + en) taken from the live in-memory proposal; the backend
field-parity walk now covers ProposedDay.
Conflicts in Calendar.tsx / Calendar.test.tsx against the day-assertions
restructure and the two earlier merges; both intents kept:
- usePlannedDayExclusions stays replaced by useDayAssertions; the branch's
usePlanProposal watch, reasonProposal state, reasonByDay map and the
handleBandClick reason toast are added alongside main's band logic.
- runSync keeps the security branch's background refresh and also records
the sync's proposal for the reason toast. main's applyWishSyncResult
(every wish save on this page) records result.sync.proposal the same way,
so an edit's fresh reasons reach the toast — the stale-response guards
in usePlanSync/usePlanProposal only gate hook state, never the returned
response the reason rides in on.
- One main test ("no success toast when sync-with-wish throws") now
dismisses the click's own reason toast first; its assertion is unchanged.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T4wjgbVL4HAC5Rdaw75VbV
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
import { describe, it, expect } from 'vitest'
|
||||
import { reasonMessageKey } from './planReasons'
|
||||
import { translate } from '@/i18n'
|
||||
|
||||
const ALL_REASON_CODES = [
|
||||
'free_weekend',
|
||||
'free_red_day',
|
||||
'vacation_wrap_red_day',
|
||||
'vacation_wrap_weekend',
|
||||
'fp_interior',
|
||||
'fp_lagsta_stretch',
|
||||
'swap_first_half',
|
||||
'swap_second_half',
|
||||
'dubbeldag',
|
||||
'fp_red_day',
|
||||
]
|
||||
|
||||
describe('reasonMessageKey', () => {
|
||||
it('maps every one of the allocator\'s 10 reason codes to a planReasons.* key', () => {
|
||||
for (const code of ALL_REASON_CODES) {
|
||||
const key = reasonMessageKey(code)
|
||||
expect(key).not.toBeNull()
|
||||
expect(key).toBe(`planReasons.${code}`)
|
||||
}
|
||||
})
|
||||
|
||||
it('resolves every mapped key to a real, non-empty string in both locales', () => {
|
||||
for (const code of ALL_REASON_CODES) {
|
||||
const key = reasonMessageKey(code)
|
||||
expect(key).not.toBeNull()
|
||||
if (!key) continue
|
||||
expect(translate('sv', key)).not.toBe(key)
|
||||
expect(translate('en', key)).not.toBe(key)
|
||||
expect(translate('sv', key).length).toBeGreaterThan(0)
|
||||
expect(translate('en', key).length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('returns null for an unrecognised reason code', () => {
|
||||
expect(reasonMessageKey('some_future_reason_not_yet_mapped')).toBeNull()
|
||||
})
|
||||
|
||||
it('returns null for an empty reason', () => {
|
||||
expect(reasonMessageKey('')).toBeNull()
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* Maps an allocator `reason` code (`ProposedDay.reason` — the
|
||||
* `REASON_*` constants in `backend/app/services/allocation_service.py:62-83`,
|
||||
* §3.5, "machine-readable so the UI can explain itself") to the
|
||||
* `planReasons.<code>` translation key that explains it in the calendar
|
||||
* toast (see `Calendar.tsx`'s `handleBandClick`).
|
||||
*
|
||||
* Follows `frontend/src/utils/rejectionMessages.ts`'s precedent: an
|
||||
* exhaustive `Record` from the backend-defined code to a `TranslationKey`,
|
||||
* so an unrecognised or empty reason returns `null` rather than ever
|
||||
* putting a raw code on screen. Every one of the allocator's 10 reason
|
||||
* codes has an entry here even though only the non-free ones are reachable
|
||||
* in practice (a free day renders no band, so it can never be clicked) —
|
||||
* kept exhaustive so the map can't quietly fall behind the backend's list.
|
||||
*/
|
||||
import type { TranslationKey } from '@/i18n'
|
||||
|
||||
const REASON_KEYS: Record<string, TranslationKey> = {
|
||||
free_weekend: 'planReasons.free_weekend',
|
||||
free_red_day: 'planReasons.free_red_day',
|
||||
vacation_wrap_red_day: 'planReasons.vacation_wrap_red_day',
|
||||
vacation_wrap_weekend: 'planReasons.vacation_wrap_weekend',
|
||||
fp_interior: 'planReasons.fp_interior',
|
||||
fp_lagsta_stretch: 'planReasons.fp_lagsta_stretch',
|
||||
swap_first_half: 'planReasons.swap_first_half',
|
||||
swap_second_half: 'planReasons.swap_second_half',
|
||||
dubbeldag: 'planReasons.dubbeldag',
|
||||
fp_red_day: 'planReasons.fp_red_day',
|
||||
}
|
||||
|
||||
/** Empty or unrecognised `reason` -> `null`; callers (`Calendar.tsx`) render
|
||||
* nothing rather than a raw code or a generic "unknown" string. */
|
||||
export function reasonMessageKey(reason: string): TranslationKey | null {
|
||||
if (!reason) return null
|
||||
return REASON_KEYS[reason] ?? null
|
||||
}
|
||||
Reference in New Issue
Block a user