Add a note field to expenses (#1888)

Requested in discussion #1658: an expense carries an amount and a
category, and everything else about it ends up crammed into the title.

The column was already there. It just was not available, because the
costs UI stores an itemized receipt in it as TICKETJSON:{...}, so an
expense split by receipt could never carry a note - and saving one any
other way wrote note=null, which quietly wiped whatever had been typed
into the budget table. The receipt gets its own column and note goes
back to being text. Old clients that still send the blob as a note are
routed to the right column instead of clobbering the note.

On desktop the row now has fixed columns, so the amount does not drift
left when one row has a note and the next does not. The category moved
to the card edge as a tab like the mobile card does, which is what frees
the line the note sits on. Collapsed it is one line, and the same
element grows in place when opened.

The dialog is two columns now: what the expense was on the left, how it
is shared on the right, grouped into panels the way the trip dialog
does it.

On the phone the eleven category pills became a dropdown, which is where
the room for the note field came from, and a note on a card unfolds when
you tap it.
This commit is contained in:
Maurice
2026-08-11 18:13:07 +02:00
committed by GitHub
parent a39c31cb48
commit 435b782c85
36 changed files with 712 additions and 126 deletions
+4
View File
@@ -113,6 +113,8 @@ export const budgetItemSchema = z.object({
persons: z.number().nullable().optional(),
days: z.number().nullable().optional(),
note: z.string().nullable().optional(),
/** Itemized receipt behind a per-item split, as JSON. Its own column since #1658. */
ticket_json: z.string().nullable().optional(),
reservation_id: z.number().nullable().optional(),
paid_by_user_id: z.number().nullable().optional(),
expense_date: z.string().nullable().optional(),
@@ -148,6 +150,7 @@ export const budgetCreateItemRequestSchema = z.object({
persons: z.number().nullable().optional(),
days: z.number().nullable().optional(),
note: z.string().nullable().optional(),
ticket_json: z.string().nullable().optional(),
expense_date: z.string().nullable().optional(),
// Link this expense to a reservation (e.g. created from a booking's
// "add expense" flow). The server stores it on budget_items.reservation_id.
@@ -168,6 +171,7 @@ export const budgetUpdateItemRequestSchema = z.object({
persons: z.number().nullable().optional(),
days: z.number().nullable().optional(),
note: z.string().nullable().optional(),
ticket_json: z.string().nullable().optional(),
expense_date: z.string().nullable().optional(),
});
export type BudgetUpdateItemRequest = z.infer<typeof budgetUpdateItemRequestSchema>;
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'تراجع',
'costs.whatFor': 'لأجل ماذا كان؟',
'costs.namePlaceholder': 'مثل: عشاء، هدايا تذكارية، وقود…',
'costs.note': 'ملاحظة',
'costs.notePlaceholder': 'ما الذي شملته، ومن يدين بماذا…',
'costs.addNote': 'إضافة ملاحظة',
'costs.showNote': 'عرض الملاحظة',
'costs.hideNote': 'إخفاء الملاحظة',
'costs.totalAmount': 'المبلغ الإجمالي',
'costs.currency': 'العملة',
'costs.day': 'اليوم',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Desfazer',
'costs.whatFor': 'Para que foi?',
'costs.namePlaceholder': 'ex.: jantar, lembranças, gasolina…',
'costs.note': 'Nota',
'costs.notePlaceholder': 'O que incluía, quem deve o quê…',
'costs.addNote': 'Adicionar nota',
'costs.showNote': 'Mostrar nota',
'costs.hideNote': 'Ocultar nota',
'costs.totalAmount': 'Valor total',
'costs.currency': 'Moeda',
'costs.day': 'Dia',
+5
View File
@@ -87,6 +87,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Desfer',
'costs.whatFor': 'Per a què ha estat?',
'costs.namePlaceholder': 'p. ex. Sopar, records, benzina…',
'costs.note': 'Nota',
'costs.notePlaceholder': 'Què incloïa, qui deu què…',
'costs.addNote': 'Afegeix una nota',
'costs.showNote': 'Mostra la nota',
'costs.hideNote': 'Amaga la nota',
'costs.totalAmount': 'Import total',
'costs.currency': 'Divisa',
'costs.day': 'Dia',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Vrátit zpět',
'costs.whatFor': 'Za co to bylo?',
'costs.namePlaceholder': 'např. večeře, suvenýry, benzín…',
'costs.note': 'Poznámka',
'costs.notePlaceholder': 'Co zahrnovalo, kdo co dluží…',
'costs.addNote': 'Přidat poznámku',
'costs.showNote': 'Zobrazit poznámku',
'costs.hideNote': 'Skrýt poznámku',
'costs.totalAmount': 'Celková částka',
'costs.currency': 'Měna',
'costs.day': 'Den',
+5
View File
@@ -92,6 +92,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Rückgängig',
'costs.whatFor': 'Wofür war es?',
'costs.namePlaceholder': 'z.B. Abendessen, Souvenirs, Benzin…',
'costs.note': 'Notiz',
'costs.notePlaceholder': 'Was darin enthalten war, wer was zurückzahlt…',
'costs.addNote': 'Notiz hinzufügen',
'costs.showNote': 'Notiz anzeigen',
'costs.hideNote': 'Notiz ausblenden',
'costs.totalAmount': 'Gesamtbetrag',
'costs.currency': 'Währung',
'costs.day': 'Tag',
+5
View File
@@ -92,6 +92,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Undo',
'costs.whatFor': 'What was it for?',
'costs.namePlaceholder': 'e.g. Dinner, souvenirs, gas…',
'costs.note': 'Note',
'costs.notePlaceholder': 'What it covered, who owes what back…',
'costs.addNote': 'Add note',
'costs.showNote': 'Show note',
'costs.hideNote': 'Hide note',
'costs.totalAmount': 'Total amount',
'costs.currency': 'Currency',
'costs.day': 'Day',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Deshacer',
'costs.whatFor': '¿Para qué fue?',
'costs.namePlaceholder': 'p. ej. Cena, souvenirs, gasolina…',
'costs.note': 'Nota',
'costs.notePlaceholder': 'Qué incluía, quién debe qué…',
'costs.addNote': 'Añadir nota',
'costs.showNote': 'Mostrar nota',
'costs.hideNote': 'Ocultar nota',
'costs.totalAmount': 'Importe total',
'costs.currency': 'Moneda',
'costs.day': 'Día',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Annuler',
'costs.whatFor': "C'était pour quoi ?",
'costs.namePlaceholder': 'ex. dîner, souvenirs, essence…',
'costs.note': 'Note',
'costs.notePlaceholder': 'Ce que ça couvrait, qui doit quoi…',
'costs.addNote': 'Ajouter une note',
'costs.showNote': 'Afficher la note',
'costs.hideNote': 'Masquer la note',
'costs.totalAmount': 'Montant total',
'costs.currency': 'Devise',
'costs.day': 'Jour',
+5
View File
@@ -90,6 +90,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Αναίρεση',
'costs.whatFor': 'Για τι ήταν;',
'costs.namePlaceholder': 'π.χ. Δείπνο, σουβενίρ, βενζίνη…',
'costs.note': 'Σημείωση',
'costs.notePlaceholder': 'Τι κάλυπτε, ποιος χρωστάει τι…',
'costs.addNote': 'Προσθήκη σημείωσης',
'costs.showNote': 'Εμφάνιση σημείωσης',
'costs.hideNote': 'Απόκρυψη σημείωσης',
'costs.totalAmount': 'Συνολικό ποσό',
'costs.currency': 'Νόμισμα',
'costs.day': 'Ημέρα',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Visszavonás',
'costs.whatFor': 'Mire volt?',
'costs.namePlaceholder': 'pl. vacsora, ajándékok, benzin…',
'costs.note': 'Megjegyzés',
'costs.notePlaceholder': 'Mit fedezett, ki tartozik mivel…',
'costs.addNote': 'Megjegyzés hozzáadása',
'costs.showNote': 'Megjegyzés megjelenítése',
'costs.hideNote': 'Megjegyzés elrejtése',
'costs.totalAmount': 'Teljes összeg',
'costs.currency': 'Pénznem',
'costs.day': 'Nap',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Urungkan',
'costs.whatFor': 'Untuk apa?',
'costs.namePlaceholder': 'mis. Makan malam, oleh-oleh, bensin…',
'costs.note': 'Catatan',
'costs.notePlaceholder': 'Apa saja yang termasuk, siapa berutang apa…',
'costs.addNote': 'Tambah catatan',
'costs.showNote': 'Tampilkan catatan',
'costs.hideNote': 'Sembunyikan catatan',
'costs.totalAmount': 'Jumlah total',
'costs.currency': 'Mata uang',
'costs.day': 'Hari',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Annulla',
'costs.whatFor': 'Per cosa era?',
'costs.namePlaceholder': 'es. Cena, souvenir, benzina…',
'costs.note': 'Nota',
'costs.notePlaceholder': 'Cosa comprendeva, chi deve cosa…',
'costs.addNote': 'Aggiungi nota',
'costs.showNote': 'Mostra nota',
'costs.hideNote': 'Nascondi nota',
'costs.totalAmount': 'Importo totale',
'costs.currency': 'Valuta',
'costs.day': 'Giorno',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': '元に戻す',
'costs.whatFor': '何の支出ですか?',
'costs.namePlaceholder': '例:夕食、お土産、ガソリン…',
'costs.note': 'メモ',
'costs.notePlaceholder': '内訳や立て替えのメモなど…',
'costs.addNote': 'メモを追加',
'costs.showNote': 'メモを表示',
'costs.hideNote': 'メモを非表示',
'costs.totalAmount': '合計金額',
'costs.currency': '通貨',
'costs.day': '日',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': '실행 취소',
'costs.whatFor': '무엇을 위한 것인가요?',
'costs.namePlaceholder': '예: 저녁 식사, 기념품, 주유…',
'costs.note': '메모',
'costs.notePlaceholder': '무엇을 샀는지, 누가 갚아야 하는지…',
'costs.addNote': '메모 추가',
'costs.showNote': '메모 보기',
'costs.hideNote': '메모 숨기기',
'costs.totalAmount': '총 금액',
'costs.currency': '통화',
'costs.day': '날짜',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Ongedaan maken',
'costs.whatFor': 'Waar was het voor?',
'costs.namePlaceholder': 'bijv. Diner, souvenirs, benzine…',
'costs.note': 'Notitie',
'costs.notePlaceholder': 'Wat het omvatte, wie wat terugbetaalt…',
'costs.addNote': 'Notitie toevoegen',
'costs.showNote': 'Notitie tonen',
'costs.hideNote': 'Notitie verbergen',
'costs.totalAmount': 'Totaalbedrag',
'costs.currency': 'Valuta',
'costs.day': 'Dag',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Cofnij',
'costs.whatFor': 'Na co to było?',
'costs.namePlaceholder': 'np. kolacja, pamiątki, paliwo…',
'costs.note': 'Notatka',
'costs.notePlaceholder': 'Co obejmowało, kto ile oddaje…',
'costs.addNote': 'Dodaj notatkę',
'costs.showNote': 'Pokaż notatkę',
'costs.hideNote': 'Ukryj notatkę',
'costs.totalAmount': 'Łączna kwota',
'costs.currency': 'Waluta',
'costs.day': 'Dzień',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Отменить',
'costs.whatFor': 'За что это было?',
'costs.namePlaceholder': 'напр. ужин, сувениры, бензин…',
'costs.note': 'Заметка',
'costs.notePlaceholder': 'Что входило, кто сколько должен…',
'costs.addNote': 'Добавить заметку',
'costs.showNote': 'Показать заметку',
'costs.hideNote': 'Скрыть заметку',
'costs.totalAmount': 'Общая сумма',
'costs.currency': 'Валюта',
'costs.day': 'День',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Ångra',
'costs.whatFor': 'Vad var det till för?',
'costs.namePlaceholder': 't.ex. middag, souvenirer, bensin…',
'costs.note': 'Anteckning',
'costs.notePlaceholder': 'Vad det omfattade, vem som ska betala tillbaka…',
'costs.addNote': 'Lägg till anteckning',
'costs.showNote': 'Visa anteckning',
'costs.hideNote': 'Dölj anteckning',
'costs.totalAmount': 'Totalt belopp',
'costs.currency': 'Valuta',
'costs.day': 'Dag',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Geri al',
'costs.whatFor': 'Ne içindi?',
'costs.namePlaceholder': 'örn. Akşam yemeği, hediyelik, benzin…',
'costs.note': 'Not',
'costs.notePlaceholder': 'Neyi kapsadığı, kimin ne borcu olduğu…',
'costs.addNote': 'Not ekle',
'costs.showNote': 'Notu göster',
'costs.hideNote': 'Notu gizle',
'costs.totalAmount': 'Toplam tutar',
'costs.currency': 'Para birimi',
'costs.day': 'Gün',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Скасувати',
'costs.whatFor': 'За що це було?',
'costs.namePlaceholder': 'напр. вечеря, сувеніри, пальне…',
'costs.note': 'Нотатка',
'costs.notePlaceholder': 'Що входило, хто скільки винен…',
'costs.addNote': 'Додати нотатку',
'costs.showNote': 'Показати нотатку',
'costs.hideNote': 'Сховати нотатку',
'costs.totalAmount': 'Загальна сума',
'costs.currency': 'Валюта',
'costs.day': 'День',
+5
View File
@@ -89,6 +89,11 @@ const budget: TranslationStrings = {
'costs.undo': 'Hoàn tác',
'costs.whatFor': 'Nó dùng để làm gì?',
'costs.namePlaceholder': 'ví dụ. Ăn tối, quà lưu niệm, xăng…',
'costs.note': 'Ghi chú',
'costs.notePlaceholder': 'Khoản này gồm những gì, ai nợ bao nhiêu…',
'costs.addNote': 'Thêm ghi chú',
'costs.showNote': 'Hiện ghi chú',
'costs.hideNote': 'Ẩn ghi chú',
'costs.totalAmount': 'Tổng số tiền',
'costs.currency': 'Tiền tệ',
'costs.day': 'Ngày',
+5
View File
@@ -88,6 +88,11 @@ const budget: TranslationStrings = {
'costs.undo': '復原',
'costs.whatFor': '這筆是什麼支出?',
'costs.namePlaceholder': '例如:晚餐、紀念品、油費…',
'costs.note': '備註',
'costs.notePlaceholder': '包含哪些內容、誰需要還款…',
'costs.addNote': '新增備註',
'costs.showNote': '顯示備註',
'costs.hideNote': '隱藏備註',
'costs.totalAmount': '總金額',
'costs.currency': '貨幣',
'costs.day': '日期',
+5
View File
@@ -88,6 +88,11 @@ const budget: TranslationStrings = {
'costs.undo': '撤销',
'costs.whatFor': '这笔花在哪了?',
'costs.namePlaceholder': '例如:晚餐、纪念品、油费…',
'costs.note': '备注',
'costs.notePlaceholder': '包含哪些内容、谁需要还款…',
'costs.addNote': '添加备注',
'costs.showNote': '显示备注',
'costs.hideNote': '隐藏备注',
'costs.totalAmount': '总金额',
'costs.currency': '货币',
'costs.day': '日期',