From 4ebf9c5f119c22637d7dcaef87d9ce13183a6d77 Mon Sep 17 00:00:00 2001 From: Maurice Date: Wed, 1 Apr 2026 12:16:11 +0200 Subject: [PATCH] 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 --- client/src/components/Budget/BudgetPanel.tsx | 88 +++++++++++++++---- .../shared/CustomDateTimePicker.tsx | 16 ++-- client/src/i18n/translations/ar.ts | 2 + client/src/i18n/translations/br.ts | 2 + client/src/i18n/translations/cs.ts | 2 + client/src/i18n/translations/de.ts | 2 + client/src/i18n/translations/en.ts | 2 + client/src/i18n/translations/es.ts | 2 + client/src/i18n/translations/fr.ts | 2 + client/src/i18n/translations/hu.ts | 2 + client/src/i18n/translations/it.ts | 2 + client/src/i18n/translations/nl.ts | 2 + client/src/i18n/translations/ru.ts | 2 + client/src/i18n/translations/zh.ts | 2 + client/src/types.ts | 1 + server/src/db/migrations.ts | 3 + server/src/routes/budget.ts | 13 +-- 17 files changed, 117 insertions(+), 28 deletions(-) diff --git a/client/src/components/Budget/BudgetPanel.tsx b/client/src/components/Budget/BudgetPanel.tsx index e95d990..b5dba5a 100644 --- a/client/src/components/Budget/BudgetPanel.tsx +++ b/client/src/components/Budget/BudgetPanel.tsx @@ -4,9 +4,10 @@ import DOM from 'react-dom' import { useTripStore } from '../../store/tripStore' import { useCanDo } from '../../store/permissionsStore' import { useTranslation } from '../../i18n' -import { Plus, Trash2, Calculator, Wallet, Pencil, Users, Check, Info, ChevronDown, ChevronRight } from 'lucide-react' +import { Plus, Trash2, Calculator, Wallet, Pencil, Users, Check, Info, ChevronDown, ChevronRight, Download } from 'lucide-react' import CustomSelect from '../shared/CustomSelect' import { budgetApi } from '../../api/client' +import { CustomDatePicker } from '../shared/CustomDateTimePicker' import type { BudgetItem, BudgetMember } from '../../types' import { currencyDecimals } from '../../utils/formatters' @@ -88,7 +89,7 @@ function InlineEditCell({ value, onSave, type = 'text', style = {}, placeholder return (
{ if (readOnly) return; setEditValue(value ?? ''); setEditing(true) }} title={readOnly ? undefined : editTooltip} - style={{ cursor: readOnly ? 'default' : 'pointer', padding: '4px 6px', borderRadius: 4, minHeight: 28, display: 'flex', alignItems: 'center', + style={{ cursor: readOnly ? 'default' : 'pointer', padding: '2px 4px', borderRadius: 4, minHeight: 22, display: 'flex', alignItems: 'center', justifyContent: style?.textAlign === 'center' ? 'center' : 'flex-start', transition: 'background 0.15s', color: display ? 'var(--text-primary)' : 'var(--text-faint)', fontSize: 13, ...style }} onMouseEnter={e => { if (!readOnly) e.currentTarget.style.background = 'var(--bg-hover)' }} @@ -100,7 +101,7 @@ function InlineEditCell({ value, onSave, type = 'text', style = {}, placeholder // ── Add Item Row ───────────────────────────────────────────────────────────── interface AddItemRowProps { - onAdd: (data: { name: string; total_price: number; persons: number | null; days: number | null; note: string | null }) => void + onAdd: (data: { name: string; total_price: number; persons: number | null; days: number | null; note: string | null; expense_date: string | null }) => void t: (key: string) => string } @@ -110,12 +111,13 @@ function AddItemRow({ onAdd, t }: AddItemRowProps) { const [persons, setPersons] = useState('') const [days, setDays] = useState('') const [note, setNote] = useState('') + const [expenseDate, setExpenseDate] = useState('') const nameRef = useRef(null) const handleAdd = () => { if (!name.trim()) return - onAdd({ name: name.trim(), total_price: parseFloat(String(price).replace(',', '.')) || 0, persons: parseInt(persons) || null, days: parseInt(days) || null, note: note.trim() || null }) - setName(''); setPrice(''); setPersons(''); setDays(''); setNote('') + onAdd({ name: name.trim(), total_price: parseFloat(String(price).replace(',', '.')) || 0, persons: parseInt(persons) || null, days: parseInt(days) || null, note: note.trim() || null, expense_date: expenseDate || null }) + setName(''); setPrice(''); setPersons(''); setDays(''); setNote(''); setExpenseDate('') setTimeout(() => nameRef.current?.focus(), 50) } @@ -133,15 +135,20 @@ function AddItemRow({ onAdd, t }: AddItemRowProps) { setPersons(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 50, margin: '0 auto' }} /> + placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 60, margin: '0 auto' }} /> setDays(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} - placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 50, margin: '0 auto' }} /> + placeholder="-" inputMode="numeric" style={{ ...inp, textAlign: 'center', maxWidth: 60, margin: '0 auto' }} /> - - - + +
+ +
+ setNote(e.target.value)} onKeyDown={e => e.key === 'Enter' && handleAdd()} placeholder={t('budget.table.note')} style={inp} /> @@ -476,6 +483,41 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro setNewCategoryName('') } + const handleExportCsv = () => { + const sep = ';' + const esc = (v: any) => { const s = String(v ?? ''); return s.includes(sep) || s.includes('"') || s.includes('\n') ? '"' + s.replace(/"/g, '""') + '"' : s } + const d = currencyDecimals(currency) + const fmtPrice = (v: number | null | undefined) => v != null ? v.toFixed(d) : '' + + const fmtDate = (iso: string) => { if (!iso) return ''; const d = new Date(iso + 'T00:00:00'); return d.toLocaleDateString(locale, { day: '2-digit', month: '2-digit', year: 'numeric' }) } + const header = ['Category', 'Name', 'Date', 'Total (' + currency + ')', 'Persons', 'Days', 'Per Person', 'Per Day', 'Per Person/Day', 'Note'] + const rows = [header.join(sep)] + + for (const cat of categoryNames) { + for (const item of (grouped[cat] || [])) { + const pp = calcPP(item.total_price, item.persons) + const pd = calcPD(item.total_price, item.days) + const ppd = calcPPD(item.total_price, item.persons, item.days) + rows.push([ + esc(item.category), esc(item.name), esc(fmtDate(item.expense_date || '')), + fmtPrice(item.total_price), item.persons ?? '', item.days ?? '', + fmtPrice(pp), fmtPrice(pd), fmtPrice(ppd), + esc(item.note || ''), + ].join(sep)) + } + } + + const bom = '\uFEFF' + const blob = new Blob([bom + rows.join('\r\n')], { type: 'text/csv;charset=utf-8;' }) + const url = URL.createObjectURL(blob) + const a = document.createElement('a') + a.href = url + const safeName = (trip?.title || 'trip').replace(/[^a-zA-Z0-9\u00C0-\u024F _-]/g, '').trim() + a.download = `budget-${safeName}.csv` + a.click() + URL.revokeObjectURL(url) + } + const th = { padding: '6px 8px', textAlign: 'center', fontSize: 11, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: '0.05em', borderBottom: '2px solid var(--border-primary)', whiteSpace: 'nowrap', background: 'var(--bg-secondary)' } const td = { padding: '2px 6px', borderBottom: '1px solid var(--border-secondary)', fontSize: 13, verticalAlign: 'middle', color: 'var(--text-primary)' } @@ -512,6 +554,10 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro

{t('budget.title')}

+
@@ -564,14 +610,15 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro - - - - - - + + + + + + - + + @@ -623,6 +670,15 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro +
{t('budget.table.name')}{t('budget.table.total')}{t('budget.table.persons')}{t('budget.table.days')}{t('budget.table.perPerson')}{t('budget.table.perDay')}{t('budget.table.name')}{t('budget.table.total')}{t('budget.table.persons')}{t('budget.table.days')}{t('budget.table.perPerson')}{t('budget.table.perDay')} {t('budget.table.perPersonDay')}{t('budget.table.note')}{t('budget.table.date')}{t('budget.table.note')}
{pp != null ? fmt(pp, currency) : '-'} {pd != null ? fmt(pd, currency) : '-'} {ppd != null ? fmt(ppd, currency) : '-'} + {canEdit ? ( +
+ handleUpdateField(item.id, 'expense_date', v || null)} placeholder="—" compact borderless /> +
+ ) : ( + {item.expense_date || '—'} + )} +
handleUpdateField(item.id, 'note', v)} placeholder={t('budget.table.note')} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} /> {canEdit && ( @@ -645,7 +701,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro })} -
+
{t('budget.totalBudget')}
-
+
{Number(grandTotal).toLocaleString(locale, { minimumFractionDigits: currencyDecimals(currency), maximumFractionDigits: currencyDecimals(currency) })}
{SYMBOLS[currency] || currency} {currency}
diff --git a/client/src/components/shared/CustomDateTimePicker.tsx b/client/src/components/shared/CustomDateTimePicker.tsx index ccbf089..e764d21 100644 --- a/client/src/components/shared/CustomDateTimePicker.tsx +++ b/client/src/components/shared/CustomDateTimePicker.tsx @@ -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(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 ) : ( )} diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index dbb14b1..cd3fe11 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -931,6 +931,7 @@ const ar: Record = { // Budget 'budget.title': 'الميزانية', + 'budget.exportCsv': 'تصدير CSV', 'budget.emptyTitle': 'لم يتم إنشاء ميزانية بعد', 'budget.emptyText': 'أنشئ فئات وإدخالات لتخطيط ميزانية سفرك', 'budget.emptyPlaceholder': 'أدخل اسم الفئة...', @@ -945,6 +946,7 @@ const ar: Record = { 'budget.table.perDay': 'لكل يوم', 'budget.table.perPersonDay': 'لكل شخص / يوم', 'budget.table.note': 'ملاحظة', + 'budget.table.date': 'التاريخ', 'budget.newEntry': 'إدخال جديد', 'budget.defaultEntry': 'إدخال جديد', 'budget.defaultCategory': 'فئة جديدة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index f4e7c69..a0ba58d 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -910,6 +910,7 @@ const br: Record = { // Budget 'budget.title': 'Orçamento', + 'budget.exportCsv': 'Exportar CSV', 'budget.emptyTitle': 'Nenhum orçamento criado ainda', 'budget.emptyText': 'Crie categorias e lançamentos para planejar o orçamento da viagem', 'budget.emptyPlaceholder': 'Nome da categoria...', @@ -924,6 +925,7 @@ const br: Record = { 'budget.table.perDay': 'Por dia', 'budget.table.perPersonDay': 'P. p. / dia', 'budget.table.note': 'Obs.', + 'budget.table.date': 'Data', 'budget.newEntry': 'Novo lançamento', 'budget.defaultEntry': 'Novo lançamento', 'budget.defaultCategory': 'Nova categoria', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 3b69a1c..4d23467 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -931,6 +931,7 @@ const cs: Record = { // Rozpočet (Budget) 'budget.title': 'Rozpočet', + 'budget.exportCsv': 'Exportovat CSV', 'budget.emptyTitle': 'Zatím nebyl vytvořen žádný rozpočet', 'budget.emptyText': 'Vytvořte kategorie a položky pro plánování cestovního rozpočtu', 'budget.emptyPlaceholder': 'Zadejte název kategorie...', @@ -945,6 +946,7 @@ const cs: Record = { 'budget.table.perDay': 'Za den', 'budget.table.perPersonDay': 'Os. / den', 'budget.table.note': 'Poznámka', + 'budget.table.date': 'Datum', 'budget.newEntry': 'Nová položka', 'budget.defaultEntry': 'Nová položka', 'budget.defaultCategory': 'Nová kategorie', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index eb0ffa4..c2d13be 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -928,6 +928,7 @@ const de: Record = { // Budget 'budget.title': 'Budget', + 'budget.exportCsv': 'CSV exportieren', 'budget.emptyTitle': 'Noch kein Budget erstellt', 'budget.emptyText': 'Erstelle Kategorien und Einträge, um dein Reisebudget zu planen', 'budget.emptyPlaceholder': 'Kategoriename eingeben...', @@ -942,6 +943,7 @@ const de: Record = { 'budget.table.perDay': 'Pro Tag', 'budget.table.perPersonDay': 'P. p / Tag', 'budget.table.note': 'Notiz', + 'budget.table.date': 'Datum', 'budget.newEntry': 'Neuer Eintrag', 'budget.defaultEntry': 'Neuer Eintrag', 'budget.defaultCategory': 'Neue Kategorie', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 989e8cc..e0b7087 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -924,6 +924,7 @@ const en: Record = { // Budget 'budget.title': 'Budget', + 'budget.exportCsv': 'Export CSV', 'budget.emptyTitle': 'No budget created yet', 'budget.emptyText': 'Create categories and entries to plan your travel budget', 'budget.emptyPlaceholder': 'Enter category name...', @@ -938,6 +939,7 @@ const en: Record = { 'budget.table.perDay': 'Per Day', 'budget.table.perPersonDay': 'P. p / Day', 'budget.table.note': 'Note', + 'budget.table.date': 'Date', 'budget.newEntry': 'New Entry', 'budget.defaultEntry': 'New Entry', 'budget.defaultCategory': 'New Category', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index de28bef..102e343 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -888,6 +888,7 @@ const es: Record = { // Budget 'budget.title': 'Presupuesto', + 'budget.exportCsv': 'Exportar CSV', 'budget.emptyTitle': 'Aún no se ha creado ningún presupuesto', 'budget.emptyText': 'Crea categorías y entradas para planificar el presupuesto de tu viaje', 'budget.emptyPlaceholder': 'Introduce el nombre de la categoría...', @@ -902,6 +903,7 @@ const es: Record = { 'budget.table.perDay': 'Por día', 'budget.table.perPersonDay': 'Por pers. / día', 'budget.table.note': 'Nota', + 'budget.table.date': 'Fecha', 'budget.newEntry': 'Nueva entrada', 'budget.defaultEntry': 'Nueva entrada', 'budget.defaultCategory': 'Nueva categoría', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index c335ae4..9bcb2d3 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -927,6 +927,7 @@ const fr: Record = { // Budget 'budget.title': 'Budget', + 'budget.exportCsv': 'Exporter CSV', 'budget.emptyTitle': 'Aucun budget créé', 'budget.emptyText': 'Créez des catégories et des entrées pour planifier votre budget de voyage', 'budget.emptyPlaceholder': 'Nom de la catégorie…', @@ -941,6 +942,7 @@ const fr: Record = { 'budget.table.perDay': 'Par jour', 'budget.table.perPersonDay': 'P. p / Jour', 'budget.table.note': 'Note', + 'budget.table.date': 'Date', 'budget.newEntry': 'Nouvelle entrée', 'budget.defaultEntry': 'Nouvelle entrée', 'budget.defaultCategory': 'Nouvelle catégorie', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index c183678..fc64a99 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -926,6 +926,7 @@ const hu: Record = { // Költségvetés 'budget.title': 'Költségvetés', + 'budget.exportCsv': 'CSV exportálás', 'budget.emptyTitle': 'Még nincs költségvetés létrehozva', 'budget.emptyText': 'Hozz létre kategóriákat és bejegyzéseket az utazási költségvetés tervezéséhez', 'budget.emptyPlaceholder': 'Kategória neve...', @@ -940,6 +941,7 @@ const hu: Record = { 'budget.table.perDay': 'Naponta', 'budget.table.perPersonDay': 'Fő / Nap', 'budget.table.note': 'Megjegyzés', + 'budget.table.date': 'Dátum', 'budget.newEntry': 'Új bejegyzés', 'budget.defaultEntry': 'Új bejegyzés', 'budget.defaultCategory': 'Új kategória', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index bc5fac4..629db66 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -926,6 +926,7 @@ const it: Record = { // Budget 'budget.title': 'Budget', + 'budget.exportCsv': 'Esporta CSV', 'budget.emptyTitle': 'Ancora nessun budget creato', 'budget.emptyText': 'Crea categorie e voci per pianificare il budget del tuo viaggio', 'budget.emptyPlaceholder': 'Inserisci nome categoria...', @@ -940,6 +941,7 @@ const it: Record = { 'budget.table.perDay': 'Per giorno', 'budget.table.perPersonDay': 'P. p / gio.', 'budget.table.note': 'Nota', + 'budget.table.date': 'Data', 'budget.newEntry': 'Nuova voce', 'budget.defaultEntry': 'Nuova voce', 'budget.defaultCategory': 'Nuova categoria', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index 8285963..e87e4eb 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -927,6 +927,7 @@ const nl: Record = { // Budget 'budget.title': 'Budget', + 'budget.exportCsv': 'CSV exporteren', 'budget.emptyTitle': 'Nog geen budget aangemaakt', 'budget.emptyText': 'Maak categorieën en invoeren aan om je reisbudget te plannen', 'budget.emptyPlaceholder': 'Categorienaam invoeren...', @@ -941,6 +942,7 @@ const nl: Record = { 'budget.table.perDay': 'Per dag', 'budget.table.perPersonDay': 'P. p. / dag', 'budget.table.note': 'Notitie', + 'budget.table.date': 'Datum', 'budget.newEntry': 'Nieuwe invoer', 'budget.defaultEntry': 'Nieuwe invoer', 'budget.defaultCategory': 'Nieuwe categorie', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 5454458..20886f2 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -927,6 +927,7 @@ const ru: Record = { // Budget 'budget.title': 'Бюджет', + 'budget.exportCsv': 'Экспорт CSV', 'budget.emptyTitle': 'Бюджет ещё не создан', 'budget.emptyText': 'Создайте категории и записи для планирования бюджета поездки', 'budget.emptyPlaceholder': 'Введите название категории...', @@ -941,6 +942,7 @@ const ru: Record = { 'budget.table.perDay': 'В день', 'budget.table.perPersonDay': 'Чел. / день', 'budget.table.note': 'Заметка', + 'budget.table.date': 'Дата', 'budget.newEntry': 'Новая запись', 'budget.defaultEntry': 'Новая запись', 'budget.defaultCategory': 'Новая категория', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 9608ceb..bd613cf 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -927,6 +927,7 @@ const zh: Record = { // Budget 'budget.title': '预算', + 'budget.exportCsv': '导出 CSV', 'budget.emptyTitle': '尚未创建预算', 'budget.emptyText': '创建分类和条目来规划旅行预算', 'budget.emptyPlaceholder': '输入分类名称...', @@ -941,6 +942,7 @@ const zh: Record = { 'budget.table.perDay': '日均', 'budget.table.perPersonDay': '人日均', 'budget.table.note': '备注', + 'budget.table.date': '日期', 'budget.newEntry': '新建条目', 'budget.defaultEntry': '新建条目', 'budget.defaultCategory': '新分类', diff --git a/client/src/types.ts b/client/src/types.ts index 59159f5..8141888 100644 --- a/client/src/types.ts +++ b/client/src/types.ts @@ -110,6 +110,7 @@ export interface BudgetItem { paid_by: number | null persons: number members: BudgetMember[] + expense_date: string | null } export interface BudgetMember { diff --git a/server/src/db/migrations.ts b/server/src/db/migrations.ts index cb5c256..e6f2929 100644 --- a/server/src/db/migrations.ts +++ b/server/src/db/migrations.ts @@ -436,6 +436,9 @@ function runMigrations(db: Database.Database): void { () => { try { db.exec('ALTER TABLE trips ADD COLUMN reminder_days INTEGER DEFAULT 3'); } catch {} }, + () => { + try { db.exec('ALTER TABLE budget_items ADD COLUMN expense_date TEXT DEFAULT NULL'); } catch {} + }, ]; if (currentVersion < migrations.length) { diff --git a/server/src/routes/budget.ts b/server/src/routes/budget.ts index 9befbc4..201790e 100644 --- a/server/src/routes/budget.ts +++ b/server/src/routes/budget.ts @@ -79,7 +79,7 @@ router.get('/summary/per-person', authenticate, (req: Request, res: Response) => router.post('/', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId } = req.params; - const { category, name, total_price, persons, days, note } = req.body; + const { category, name, total_price, persons, days, note, expense_date } = req.body; const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); @@ -93,7 +93,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { const sortOrder = (maxOrder.max !== null ? maxOrder.max : -1) + 1; const result = db.prepare( - 'INSERT INTO budget_items (trip_id, category, name, total_price, persons, days, note, sort_order) VALUES (?, ?, ?, ?, ?, ?, ?, ?)' + 'INSERT INTO budget_items (trip_id, category, name, total_price, persons, days, note, sort_order, expense_date) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)' ).run( tripId, category || 'Other', @@ -102,7 +102,8 @@ router.post('/', authenticate, (req: Request, res: Response) => { persons != null ? persons : null, days !== undefined && days !== null ? days : null, note || null, - sortOrder + sortOrder, + expense_date || null ); const item = db.prepare('SELECT * FROM budget_items WHERE id = ?').get(result.lastInsertRowid) as BudgetItem & { members?: BudgetItemMember[] }; @@ -114,7 +115,7 @@ router.post('/', authenticate, (req: Request, res: Response) => { router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; - const { category, name, total_price, persons, days, note, sort_order } = req.body; + const { category, name, total_price, persons, days, note, sort_order, expense_date } = req.body; const trip = verifyTripOwnership(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); @@ -133,7 +134,8 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { persons = CASE WHEN ? IS NOT NULL THEN ? ELSE persons END, days = CASE WHEN ? THEN ? ELSE days END, note = CASE WHEN ? THEN ? ELSE note END, - sort_order = CASE WHEN ? IS NOT NULL THEN ? ELSE sort_order END + sort_order = CASE WHEN ? IS NOT NULL THEN ? ELSE sort_order END, + expense_date = CASE WHEN ? THEN ? ELSE expense_date END WHERE id = ? `).run( category || null, @@ -143,6 +145,7 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { days !== undefined ? 1 : 0, days !== undefined ? days : null, note !== undefined ? 1 : 0, note !== undefined ? note : null, sort_order !== undefined ? 1 : null, sort_order !== undefined ? sort_order : 0, + expense_date !== undefined ? 1 : 0, expense_date !== undefined ? (expense_date || null) : null, id );