Redraw the calendar as an almanac: gridless cells, typed bands
Day cells lose their borders and fills; red days, klämdagar and Saturdays are told apart by the numeral's ink with a 12px R/K mark, today is a filled circle, overrides and exclusions mark the numeral. Bands carry their funding type by fill, ink and hatching and their owner by a left cap. The preset rail becomes a chip row, the legend a help popover, the period popover's three delete buttons fold behind one "Ta bort…", and solo users pick a type directly in the day modal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XBRLomdLK9jF7wU7a8AA6H
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { ReactNode, useEffect, useState } from 'react'
|
||||
import type { PlannedLeaveDay } from '@/types'
|
||||
import { useTranslation } from '@/i18n'
|
||||
import { Icon } from '@/components/common/Icon'
|
||||
import './planned-day-modal.css'
|
||||
|
||||
/** The default path — todo.md #10/#11: the planner decides which day type
|
||||
@@ -18,11 +19,22 @@ export interface PlannedDayModalSaveWishPayload {
|
||||
* an exact day type for an exact parent, writing `planned_leave_day` rows
|
||||
* directly. */
|
||||
export interface PlannedDayModalSavePinnedPayload {
|
||||
dayType: 'sjukpenning' | 'lagsta' | 'vacation'
|
||||
dayType: PinnedDayType
|
||||
parentId: string
|
||||
isDubbeldag: boolean
|
||||
}
|
||||
|
||||
type PinnedDayType = 'sjukpenning' | 'lagsta' | 'vacation'
|
||||
|
||||
/** What the type select can hold: a concrete type to pin, or — for a solo
|
||||
* household, where the select is the only question left — `'auto'`, the
|
||||
* planner-decides default. */
|
||||
type DayTypeChoice = PinnedDayType | 'auto'
|
||||
|
||||
function isPinnedDayType(value: string): value is PinnedDayType {
|
||||
return value === 'sjukpenning' || value === 'lagsta' || value === 'vacation'
|
||||
}
|
||||
|
||||
/** One day the user removed by hand, ready to be offered back. */
|
||||
export interface PlannedDayExclusionEntry {
|
||||
id: string
|
||||
@@ -83,8 +95,11 @@ export function PlannedDayModal({
|
||||
// Editing an existing pinned day always reopens the advanced/pinned path —
|
||||
// it must never be silently reinterpreted as a new planner-funded wish.
|
||||
const forcedAdvanced = !isRangeMode && !!existingDay
|
||||
// A solo household has nobody to choose between, so the "Advanced"
|
||||
// disclosure would hide exactly one control. The type select is shown
|
||||
// directly instead, with the planner-decides choice as its default.
|
||||
const soloDirect = !hasMultipleParents && !forcedAdvanced
|
||||
const [isAdvancedOpen, setIsAdvancedOpen] = useState(forcedAdvanced)
|
||||
const advancedOpen = forcedAdvanced || isAdvancedOpen
|
||||
|
||||
// --- Default ("planner decides") path state -----------------------------
|
||||
const [parentMode, setParentMode] = useState<'either' | 'both' | 'specific'>(
|
||||
@@ -94,9 +109,15 @@ export function PlannedDayModal({
|
||||
const [wantsDubbeldag, setWantsDubbeldag] = useState(false)
|
||||
|
||||
// --- Advanced ("pin exactly") path state ---------------------------------
|
||||
const [dayType, setDayType] = useState<'sjukpenning' | 'lagsta' | 'vacation'>('vacation')
|
||||
const [dayType, setDayType] = useState<DayTypeChoice>(soloDirect ? 'auto' : 'vacation')
|
||||
const [isDubbeldag, setIsDubbeldag] = useState(false)
|
||||
|
||||
// Whether saving pins an exact type (the escape hatch) or hands the dates
|
||||
// to the planner as a wish. Couples opt in through the disclosure; a solo
|
||||
// household opts in by picking a concrete type in the select.
|
||||
const pinnedPath = forcedAdvanced || (soloDirect ? dayType !== 'auto' : isAdvancedOpen)
|
||||
const showPinnedControls = forcedAdvanced || soloDirect || isAdvancedOpen
|
||||
|
||||
const [localError, setLocalError] = useState<string | null>(null)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
|
||||
@@ -115,11 +136,13 @@ export function PlannedDayModal({
|
||||
}, [isOpen, date, isRangeMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (existingDay) {
|
||||
setDayType(existingDay.dayType as 'sjukpenning' | 'lagsta' | 'vacation')
|
||||
// A range is never an edit of its first date's day, so that day's type
|
||||
// must not leak into the select as if it were being edited.
|
||||
if (existingDay && !isRangeMode) {
|
||||
setDayType(isPinnedDayType(existingDay.dayType) ? existingDay.dayType : 'vacation')
|
||||
setIsDubbeldag(existingDay.isDubbeldag)
|
||||
} else {
|
||||
setDayType(defaultDayType ?? 'vacation')
|
||||
setDayType(soloDirect ? 'auto' : (defaultDayType ?? 'vacation'))
|
||||
setIsDubbeldag(false)
|
||||
}
|
||||
// Deliberately NOT a separate "if dayType === 'vacation', clear
|
||||
@@ -131,7 +154,7 @@ export function PlannedDayModal({
|
||||
// vacation/dubbeldag rule is instead enforced right where the user
|
||||
// actually changes the type — see the advanced day-type `<select>`'s
|
||||
// `onChange` below.
|
||||
}, [isOpen, existingDay, defaultDayType])
|
||||
}, [isOpen, existingDay, defaultDayType, soloDirect, isRangeMode])
|
||||
|
||||
useEffect(() => {
|
||||
if (parentMode !== 'both') {
|
||||
@@ -142,7 +165,7 @@ export function PlannedDayModal({
|
||||
const handleSave = async () => {
|
||||
try {
|
||||
setLocalError(null)
|
||||
if (advancedOpen) {
|
||||
if (pinnedPath && dayType !== 'auto') {
|
||||
await onSavePinned({ dayType, parentId: selectedParentId, isDubbeldag })
|
||||
} else {
|
||||
await onSaveWish({
|
||||
@@ -217,7 +240,7 @@ export function PlannedDayModal({
|
||||
<div className="modal__header">
|
||||
<h2 data-testid="modal-title">{modalTitle}</h2>
|
||||
<button type="button" className="btn btn--icon" onClick={onClose} aria-label={t('plannedDayModal.closeAria')}>
|
||||
×
|
||||
<Icon name="close" size={20} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -297,10 +320,10 @@ export function PlannedDayModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!forcedAdvanced && (
|
||||
{!forcedAdvanced && hasMultipleParents && (
|
||||
<button
|
||||
type="button"
|
||||
className="btn btn--ghost planned-day-modal__advanced-toggle"
|
||||
className="btn btn--link planned-day-modal__advanced-toggle"
|
||||
onClick={() => setIsAdvancedOpen((open) => !open)}
|
||||
disabled={isSaving}
|
||||
data-testid="advanced-toggle"
|
||||
@@ -310,7 +333,7 @@ export function PlannedDayModal({
|
||||
</button>
|
||||
)}
|
||||
|
||||
{advancedOpen && (
|
||||
{showPinnedControls && (
|
||||
<div className="planned-day-modal__advanced" data-testid="advanced-section">
|
||||
<div className="field">
|
||||
<label htmlFor="day-type" className="field__label">
|
||||
@@ -320,14 +343,16 @@ export function PlannedDayModal({
|
||||
id="day-type"
|
||||
value={dayType}
|
||||
onChange={(e) => {
|
||||
const value = e.target.value as 'sjukpenning' | 'lagsta' | 'vacation'
|
||||
setDayType(value)
|
||||
if (value === 'vacation') setIsDubbeldag(false)
|
||||
const value = e.target.value
|
||||
const next: DayTypeChoice = isPinnedDayType(value) ? value : 'auto'
|
||||
setDayType(next)
|
||||
if (next === 'vacation' || next === 'auto') setIsDubbeldag(false)
|
||||
}}
|
||||
className="field__control"
|
||||
disabled={isSaving}
|
||||
data-testid="day-type-select"
|
||||
>
|
||||
{soloDirect && <option value="auto">{t('plannedDayModal.autoOption')}</option>}
|
||||
<option value="vacation">{t('plannedDayModal.vacationOption')}</option>
|
||||
<option value="sjukpenning">{t('plannedDayModal.sjukpenningOption')}</option>
|
||||
<option value="lagsta">{t('plannedDayModal.lagstaOption')}</option>
|
||||
@@ -356,27 +381,30 @@ export function PlannedDayModal({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="field">
|
||||
<label className="planned-day-modal__checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isDubbeldag}
|
||||
onChange={(e) => setIsDubbeldag(e.target.checked)}
|
||||
disabled={isSaving || dayType === 'vacation'}
|
||||
data-testid="dubbeldag-checkbox"
|
||||
/>
|
||||
{' '}{t('plannedDayModal.doubleDayLabel')}
|
||||
</label>
|
||||
{dayType === 'vacation' && (
|
||||
<p className="planned-day-modal__hint">
|
||||
{t('plannedDayModal.doubleDayHint')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{dayType !== 'auto' && (
|
||||
<div className="field">
|
||||
<label className="planned-day-modal__checkbox-label">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isDubbeldag}
|
||||
onChange={(e) => setIsDubbeldag(e.target.checked)}
|
||||
disabled={isSaving || dayType === 'vacation'}
|
||||
data-testid="dubbeldag-checkbox"
|
||||
/>
|
||||
{' '}{t('plannedDayModal.doubleDayLabel')}
|
||||
</label>
|
||||
{dayType === 'vacation' && (
|
||||
<p className="planned-day-modal__hint">
|
||||
{t('plannedDayModal.doubleDayHint')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{existingDay?.isPinned && (
|
||||
<p className="planned-day-modal__pinned-notice" data-testid="pinned-notice">
|
||||
📌 {t('plannedDayModal.pinnedNotice')}
|
||||
<Icon name="pin" size={16} />
|
||||
<span>{t('plannedDayModal.pinnedNotice')}</span>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user