@@ -564,14 +610,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')} |
|
@@ -623,6 +670,15 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
{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
);
|