ae6db7e16f
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>
179 lines
6.2 KiB
TypeScript
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>
|
|
)
|
|
}
|