Redesign the setup flow and consolidate the CSS into one primitives layer
Onboarding asks explicitly for one or two parents and shows one card per parent for balances; the balance form is per parent with inline validation; the partner can be filled from the FK deduction or added from it. Settings and Dashboard reuse the same form. CSS: bare buttons carry no colour any more; buttons, cards, choice cards, fields, alerts, badges, eyebrow and modal chrome are defined once in primitives.css. Media queries live only in responsive.css. A style guard test blocks hex, !important and stray media queries. Bilingual fixes: calendar selects size by content, header no longer wraps. Vitest is capped at four workers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { ReactNode, useState, useEffect } from 'react'
|
||||
import type { CalendarDayEntry, PlannedLeaveDay } from '@/types'
|
||||
import type { PlannedLeaveDay } from '@/types'
|
||||
import { useTranslation } from '@/i18n'
|
||||
import './planned-day-modal.css'
|
||||
|
||||
@@ -21,14 +21,6 @@ export interface PlannedDayModalProps {
|
||||
isRangeMode?: boolean | undefined
|
||||
dayCount?: number | undefined
|
||||
defaultDayType?: 'sjukpenning' | 'lagsta' | 'vacation' | undefined
|
||||
// §Task7 day popover — this date's calendar metadata (röda dagar/weekend/
|
||||
// overrides), driving the "arbetsdag/ledig" switch below. `undefined`
|
||||
// while the calendar window hasn't loaded metadata for this date yet;
|
||||
// `null`/absent `onToggleOverride` hides the switch entirely (e.g. in
|
||||
// range mode, where there is no single day to toggle).
|
||||
dayInfo?: CalendarDayEntry | null | undefined
|
||||
onToggleOverride?: (() => Promise<void>) | undefined
|
||||
overrideError?: string | null | undefined
|
||||
}
|
||||
|
||||
export function PlannedDayModal({
|
||||
@@ -45,16 +37,12 @@ export function PlannedDayModal({
|
||||
isRangeMode = false,
|
||||
dayCount = 1,
|
||||
defaultDayType,
|
||||
dayInfo,
|
||||
onToggleOverride,
|
||||
overrideError = null,
|
||||
}: PlannedDayModalProps): ReactNode {
|
||||
const { t, locale } = useTranslation()
|
||||
const [dayType, setDayType] = useState<'sjukpenning' | 'lagsta' | 'vacation'>('vacation')
|
||||
const [parentId, setParentId] = useState(selectedParentId)
|
||||
const [isDubbeldag, setIsDubbeldag] = useState(false)
|
||||
const [localError, setLocalError] = useState<string | null>(null)
|
||||
const [isTogglingOverride, setIsTogglingOverride] = useState(false)
|
||||
const [isConfirmingDelete, setIsConfirmingDelete] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
@@ -111,22 +99,6 @@ export function PlannedDayModal({
|
||||
}
|
||||
}
|
||||
|
||||
// §Task7 — the "Den här dagen är: arbetsdag / ledig" switch. There are
|
||||
// only two effective states, so flipping to whichever option ISN'T
|
||||
// currently selected is exactly the toggle `onToggleOverride` performs —
|
||||
// no separate "set to X" call is needed. A no-op click on the
|
||||
// already-selected option is ignored.
|
||||
const handleSelectDayStatus = async (wantFree: boolean) => {
|
||||
if (!onToggleOverride || !dayInfo) return
|
||||
if (dayInfo.isFree === wantFree) return
|
||||
try {
|
||||
setIsTogglingOverride(true)
|
||||
await onToggleOverride()
|
||||
} finally {
|
||||
setIsTogglingOverride(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (!isOpen) return null
|
||||
|
||||
const displayDate = new Date(date + 'T00:00:00Z').toLocaleDateString(locale === 'sv' ? 'sv-SE' : 'en-US', {
|
||||
@@ -143,137 +115,104 @@ export function PlannedDayModal({
|
||||
: t('plannedDayModal.newTitle')
|
||||
|
||||
return (
|
||||
<div className="planned-day-modal__overlay" onClick={onClose} data-testid="planned-day-modal__overlay" role="dialog">
|
||||
<div className="planned-day-modal__content" onClick={(e) => e.stopPropagation()} data-testid="planned-day-modal__content">
|
||||
<div className="planned-day-modal__header">
|
||||
<div className="modal__overlay" onClick={onClose} data-testid="planned-day-modal__overlay" role="dialog">
|
||||
<div
|
||||
className="modal__content modal__content--sm"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
data-testid="planned-day-modal__content"
|
||||
>
|
||||
<div className="modal__header">
|
||||
<h2 data-testid="modal-title">{modalTitle}</h2>
|
||||
<button className="planned-day-modal__close" onClick={onClose} aria-label={t('plannedDayModal.closeAria')}>
|
||||
<button type="button" className="btn btn--icon" onClick={onClose} aria-label={t('plannedDayModal.closeAria')}>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="planned-day-modal__body">
|
||||
<div className="modal__body">
|
||||
{!isRangeMode && <p className="planned-day-modal__date">{displayDate}</p>}
|
||||
|
||||
{(error || localError) && (
|
||||
<div className="planned-day-modal__error" role="alert">
|
||||
<div className="alert alert--error" role="alert">
|
||||
{error || localError}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="planned-day-modal__field">
|
||||
<label htmlFor="day-type" className="planned-day-modal__label">
|
||||
{t('plannedDayModal.leaveTypeLabel')}
|
||||
</label>
|
||||
<select
|
||||
id="day-type"
|
||||
value={dayType}
|
||||
onChange={(e) => setDayType(e.target.value as 'sjukpenning' | 'lagsta' | 'vacation')}
|
||||
className="planned-day-modal__select"
|
||||
disabled={isSaving}
|
||||
data-testid="day-type-select"
|
||||
>
|
||||
<option value="vacation">{t('plannedDayModal.vacationOption')}</option>
|
||||
<option value="sjukpenning">{t('plannedDayModal.sjukpenningOption')}</option>
|
||||
<option value="lagsta">{t('plannedDayModal.lagstaOption')}</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{parents.length > 1 && (
|
||||
<div className="planned-day-modal__field">
|
||||
<label htmlFor="parent-select" className="planned-day-modal__label">
|
||||
{t('plannedDayModal.parentLabel')}
|
||||
<div className="stack">
|
||||
<div className="field">
|
||||
<label htmlFor="day-type" className="field__label">
|
||||
{t('plannedDayModal.leaveTypeLabel')}
|
||||
</label>
|
||||
<select
|
||||
id="parent-select"
|
||||
value={parentId}
|
||||
onChange={(e) => setParentId(e.target.value)}
|
||||
className="planned-day-modal__select"
|
||||
id="day-type"
|
||||
value={dayType}
|
||||
onChange={(e) => setDayType(e.target.value as 'sjukpenning' | 'lagsta' | 'vacation')}
|
||||
className="field__control"
|
||||
disabled={isSaving}
|
||||
data-testid="parent-select"
|
||||
data-testid="day-type-select"
|
||||
>
|
||||
{parents.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
<option value="vacation">{t('plannedDayModal.vacationOption')}</option>
|
||||
<option value="sjukpenning">{t('plannedDayModal.sjukpenningOption')}</option>
|
||||
<option value="lagsta">{t('plannedDayModal.lagstaOption')}</option>
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="planned-day-modal__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')}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{!isRangeMode && dayInfo && onToggleOverride && (
|
||||
<div className="planned-day-modal__field">
|
||||
<span className="planned-day-modal__label">{t('plannedDayModal.dayStatusLabel')}</span>
|
||||
<div
|
||||
className="planned-day-modal__override-switch"
|
||||
role="radiogroup"
|
||||
aria-label={t('plannedDayModal.dayStatusLabel')}
|
||||
data-testid="day-status-switch"
|
||||
>
|
||||
<label className="planned-day-modal__checkbox-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="day-status"
|
||||
checked={!dayInfo.isFree}
|
||||
onChange={() => void handleSelectDayStatus(false)}
|
||||
disabled={isSaving || isTogglingOverride}
|
||||
data-testid="day-status-workday"
|
||||
/>
|
||||
{' '}{t('plannedDayModal.workdayOption')}
|
||||
</label>
|
||||
<label className="planned-day-modal__checkbox-label">
|
||||
<input
|
||||
type="radio"
|
||||
name="day-status"
|
||||
checked={dayInfo.isFree}
|
||||
onChange={() => void handleSelectDayStatus(true)}
|
||||
disabled={isSaving || isTogglingOverride}
|
||||
data-testid="day-status-off"
|
||||
/>
|
||||
{' '}{t('plannedDayModal.dayOffOption')}
|
||||
{parents.length > 1 && (
|
||||
<div className="field">
|
||||
<label htmlFor="parent-select" className="field__label">
|
||||
{t('plannedDayModal.parentLabel')}
|
||||
</label>
|
||||
<select
|
||||
id="parent-select"
|
||||
value={parentId}
|
||||
onChange={(e) => setParentId(e.target.value)}
|
||||
className="field__control"
|
||||
disabled={isSaving}
|
||||
data-testid="parent-select"
|
||||
>
|
||||
{parents.map((p) => (
|
||||
<option key={p.id} value={p.id}>
|
||||
{p.name}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
{overrideError && (
|
||||
<p className="planned-day-modal__error" role="alert" data-testid="day-status-error">
|
||||
{overrideError}
|
||||
)}
|
||||
|
||||
<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')}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="planned-day-modal__footer">
|
||||
<div className="modal__footer">
|
||||
{!isRangeMode && existingDay && onDelete && isConfirmingDelete && (
|
||||
<div className="planned-day-modal__delete-confirm" data-testid="delete-confirm">
|
||||
<span className="planned-day-modal__delete-confirm-text">
|
||||
<div className="modal__delete-confirm" data-testid="delete-confirm">
|
||||
<span className="modal__delete-confirm-text">
|
||||
{t('plannedDayModal.confirmDelete')}
|
||||
</span>
|
||||
<button
|
||||
className="planned-day-modal__button planned-day-modal__button--secondary"
|
||||
type="button"
|
||||
className="btn btn--secondary"
|
||||
onClick={handleCancelDelete}
|
||||
disabled={isSaving}
|
||||
data-testid="delete-cancel-button"
|
||||
@@ -281,7 +220,8 @@ export function PlannedDayModal({
|
||||
{t('plannedDayModal.cancelButton')}
|
||||
</button>
|
||||
<button
|
||||
className="planned-day-modal__button planned-day-modal__button--danger"
|
||||
type="button"
|
||||
className="btn btn--danger"
|
||||
onClick={() => void handleConfirmDelete()}
|
||||
disabled={isSaving}
|
||||
data-testid="delete-confirm-button"
|
||||
@@ -294,7 +234,8 @@ export function PlannedDayModal({
|
||||
<>
|
||||
{!isRangeMode && existingDay && onDelete && (
|
||||
<button
|
||||
className="planned-day-modal__button planned-day-modal__button--danger"
|
||||
type="button"
|
||||
className="btn btn--danger btn--start"
|
||||
onClick={handleDeleteClick}
|
||||
disabled={isSaving}
|
||||
data-testid="delete-button"
|
||||
@@ -303,7 +244,8 @@ export function PlannedDayModal({
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="planned-day-modal__button planned-day-modal__button--secondary"
|
||||
type="button"
|
||||
className="btn btn--secondary"
|
||||
onClick={onClose}
|
||||
disabled={isSaving}
|
||||
data-testid="cancel-button"
|
||||
@@ -311,7 +253,8 @@ export function PlannedDayModal({
|
||||
{t('plannedDayModal.cancelButton')}
|
||||
</button>
|
||||
<button
|
||||
className="planned-day-modal__button planned-day-modal__button--primary"
|
||||
type="button"
|
||||
className="btn btn--primary"
|
||||
onClick={handleSave}
|
||||
disabled={isSaving}
|
||||
data-testid="save-button"
|
||||
|
||||
Reference in New Issue
Block a user