feat: add expense date and CSV export to budget

- New expense_date column on budget items (DB migration #42)
- Date column in budget table with custom date picker
- CSV export button with BOM, semicolon separator, localized dates,
  currency in header, per-person/day calculations
- CustomDatePicker compact/borderless modes for inline table use
- i18n keys for all 12 languages
This commit is contained in:
Maurice
2026-04-01 12:16:11 +02:00
parent 60906cf1d1
commit 4ebf9c5f11
17 changed files with 117 additions and 28 deletions
@@ -11,9 +11,11 @@ interface CustomDatePickerProps {
onChange: (value: string) => void
placeholder?: string
style?: React.CSSProperties
compact?: boolean
borderless?: boolean
}
export function CustomDatePicker({ value, onChange, placeholder, style = {} }: CustomDatePickerProps) {
export function CustomDatePicker({ value, onChange, placeholder, style = {}, compact = false, borderless = false }: CustomDatePickerProps) {
const { locale, t } = useTranslation()
const [open, setOpen] = useState(false)
const ref = useRef<HTMLDivElement>(null)
@@ -45,7 +47,7 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {} }: C
const startDay = (getWeekday(viewYear, viewMonth, 1) + 6) % 7
const weekdays = Array.from({ length: 7 }, (_, i) => new Date(2024, 0, i + 1).toLocaleDateString(locale, { weekday: 'narrow' }))
const displayValue = parsed ? parsed.toLocaleDateString(locale, { day: 'numeric', month: 'short', year: 'numeric' }) : null
const displayValue = parsed ? parsed.toLocaleDateString(locale, compact ? { day: '2-digit', month: '2-digit', year: '2-digit' } : { day: 'numeric', month: 'short', year: 'numeric' }) : null
const selectDay = (day: number) => {
const y = String(viewYear)
@@ -97,16 +99,16 @@ export function CustomDatePicker({ value, onChange, placeholder, style = {} }: C
) : (
<button type="button" onClick={() => setOpen(o => !o)} onDoubleClick={() => { setTextInput(value || ''); setIsTyping(true) }}
style={{
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
padding: '8px 14px', borderRadius: 10,
border: '1px solid var(--border-primary)',
background: 'var(--bg-input)', color: displayValue ? 'var(--text-primary)' : 'var(--text-faint)',
width: '100%', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: compact ? 4 : 8,
padding: compact ? '4px 6px' : '8px 14px', borderRadius: compact ? 4 : 10,
border: borderless ? 'none' : '1px solid var(--border-primary)',
background: borderless ? 'transparent' : 'var(--bg-input)', color: displayValue ? 'var(--text-primary)' : 'var(--text-faint)',
fontSize: 13, fontFamily: 'inherit', cursor: 'pointer', outline: 'none',
transition: 'border-color 0.15s',
}}
onMouseEnter={e => e.currentTarget.style.borderColor = 'var(--text-faint)'}
onMouseLeave={e => { if (!open) e.currentTarget.style.borderColor = 'var(--border-primary)' }}>
<Calendar size={14} style={{ color: 'var(--text-faint)', flexShrink: 0 }} />
{!compact && <Calendar size={14} style={{ color: 'var(--text-faint)', flexShrink: 0 }} />}
<span style={{ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis' }}>{displayValue || placeholder || t('common.date')}</span>
</button>
)}