Files
F-r-ldraledigplannerare/frontend/src/components/inputs/DayOverridesInput.tsx
T
jacobh2 ae6db7e16f 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>
2026-09-19 12:27:29 +02:00

179 lines
6.2 KiB
TypeScript

import { ReactNode, useState } from 'react'
import { formatDateForDisplay } from '@/utils/dates'
import { useTranslation } from '@/i18n'
import type { DayOverride, DayOverrideCreateRequest } from '@/types'
export interface DayOverridesInputProps {
overrides: DayOverride[]
isLoading?: boolean
onAdd: (req: DayOverrideCreateRequest) => Promise<void>
onRemove: (id: string) => Promise<void>
workspaceId: string
}
export function DayOverridesInput({
overrides,
isLoading = false,
onAdd,
onRemove,
workspaceId,
}: DayOverridesInputProps): ReactNode {
const { t, locale } = useTranslation()
const [date, setDate] = useState('')
const [isWorkingDay, setIsWorkingDay] = useState(true)
const [note, setNote] = useState('')
const [isSubmitting, setIsSubmitting] = useState(false)
const handleAddOverride = async (e: React.FormEvent) => {
e.preventDefault()
if (!date) return
try {
setIsSubmitting(true)
await onAdd({
workspaceId,
date,
isWorkingDay,
note: note || null,
})
setDate('')
setIsWorkingDay(true)
setNote('')
} finally {
setIsSubmitting(false)
}
}
const handleRemove = async (id: string) => {
try {
await onRemove(id)
} catch {
// Error handling is done in parent
}
}
const sortedOverrides = [...overrides].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime())
return (
<div data-testid="day-overrides-input" className="day-overrides-input">
{overrides.length === 0 ? (
<div className="day-overrides-input__empty">
<p>{t('dayOverridesInput.empty')}</p>
</div>
) : (
<div className="day-overrides-input__list">
<ul className="day-overrides-input__items">
{sortedOverrides.map((override) => (
<li key={override.id} className="day-overrides-input__item" data-testid={`override-item-${override.id}`}>
<div className="day-overrides-input__item-header">
<div className="day-overrides-input__item-date">
<strong>{formatDateForDisplay(override.date, locale)}</strong>
</div>
<div className="day-overrides-input__item-status">
<span className={`day-overrides-input__status day-overrides-input__status--${override.isWorkingDay ? 'working' : 'off'}`}>
{override.isWorkingDay ? t('dayOverridesInput.workingStatus') : t('dayOverridesInput.offStatus')}
</span>
</div>
</div>
{override.note && (
<div className="day-overrides-input__item-note">
{t('dayOverridesInput.noteLabel', { note: override.note })}
</div>
)}
<button
type="button"
onClick={() => handleRemove(override.id)}
disabled={isSubmitting || isLoading}
className="btn btn--danger btn--sm btn--block"
data-testid={`remove-override-${override.id}`}
>
{t('common.delete')}
</button>
</li>
))}
</ul>
</div>
)}
<form onSubmit={handleAddOverride} className="day-overrides-input__form" data-testid="add-override-form">
<h3>{t('dayOverridesInput.addSectionTitle')}</h3>
<div className="field day-overrides-input__form-group">
<label htmlFor="override-date" className="field__label">
{t('dayOverridesInput.dateLabel')}
</label>
<input
id="override-date"
type="date"
value={date}
onChange={(e) => setDate(e.target.value)}
required
disabled={isSubmitting || isLoading}
data-testid="override-date-input"
className="field__control"
/>
</div>
<div className="day-overrides-input__form-group">
<fieldset className="day-overrides-input__radio-group">
<legend>{t('dayOverridesInput.statusLegend')}</legend>
<label className="day-overrides-input__radio-label">
<input
type="radio"
name="status"
value="working"
checked={isWorkingDay}
onChange={() => setIsWorkingDay(true)}
disabled={isSubmitting || isLoading}
data-testid="override-working-radio"
/>
<span>{t('dayOverridesInput.workingStatus')}</span>
</label>
<label className="day-overrides-input__radio-label">
<input
type="radio"
name="status"
value="off"
checked={!isWorkingDay}
onChange={() => setIsWorkingDay(false)}
disabled={isSubmitting || isLoading}
data-testid="override-off-radio"
/>
<span>{t('dayOverridesInput.offStatus')}</span>
</label>
</fieldset>
</div>
<div className="field day-overrides-input__form-group">
<label htmlFor="override-note" className="field__label">
{t('dayOverridesInput.noteFieldLabel')}
</label>
<input
id="override-note"
type="text"
placeholder={t('dayOverridesInput.notePlaceholder')}
value={note}
onChange={(e) => setNote(e.target.value)}
maxLength={280}
disabled={isSubmitting || isLoading}
data-testid="override-note-input"
className="field__control"
/>
<small className="day-overrides-input__char-count">
{t('dayOverridesInput.charCount', { count: note.length })}
</small>
</div>
<button
type="submit"
disabled={isSubmitting || isLoading || !date}
className="btn btn--primary btn--block"
data-testid="add-override-button"
>
{isSubmitting ? t('dayOverridesInput.addingButton') : t('dayOverridesInput.addButton')}
</button>
</form>
</div>
)
}