From b8058a2755687295cfc2adad9359775bdd0d8015 Mon Sep 17 00:00:00 2001 From: mauriceboe Date: Sun, 5 Apr 2026 00:13:07 +0200 Subject: [PATCH] fix(reservations): budget category dropdown, localized auto-category, price input cleanup - Budget category uses dropdown with existing categories instead of freetext - Auto category uses translated booking type names (e.g. "Volo" in Italian) - Remove number input spinner arrows, use decimal inputMode - Add budget entry creation to PUT handler (update), not just POST (create) - Error logging for failed budget entry creation - i18n keys for all 13 languages --- .../components/Planner/ReservationModal.tsx | 25 +++++++++++++++---- client/src/i18n/translations/ar.ts | 5 ++++ client/src/i18n/translations/br.ts | 5 ++++ client/src/i18n/translations/cs.ts | 5 ++++ client/src/i18n/translations/de.ts | 1 + client/src/i18n/translations/en.ts | 1 + client/src/i18n/translations/es.ts | 5 ++++ client/src/i18n/translations/fr.ts | 5 ++++ client/src/i18n/translations/hu.ts | 5 ++++ client/src/i18n/translations/it.ts | 5 ++++ client/src/i18n/translations/nl.ts | 5 ++++ client/src/i18n/translations/pl.ts | 5 ++++ client/src/i18n/translations/ru.ts | 5 ++++ client/src/i18n/translations/zh.ts | 5 ++++ server/src/routes/reservations.ts | 21 ++++++++++++++-- 15 files changed, 96 insertions(+), 7 deletions(-) diff --git a/client/src/components/Planner/ReservationModal.tsx b/client/src/components/Planner/ReservationModal.tsx index f1827a8..c00780b 100644 --- a/client/src/components/Planner/ReservationModal.tsx +++ b/client/src/components/Planner/ReservationModal.tsx @@ -71,6 +71,13 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p const { t, locale } = useTranslation() const fileInputRef = useRef(null) + const budgetItems = useTripStore(s => s.budgetItems) + const budgetCategories = useMemo(() => { + const cats = new Set() + budgetItems.forEach(i => { if (i.category) cats.add(i.category) }) + return Array.from(cats).sort() + }, [budgetItems]) + const [form, setForm] = useState({ title: '', type: 'other', status: 'pending', reservation_time: '', reservation_end_time: '', end_date: '', location: '', confirmation_number: '', @@ -204,7 +211,7 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p if (form.price && parseFloat(form.price) > 0) { saveData.create_budget_entry = { total_price: parseFloat(form.price), - category: form.budget_category || form.type || 'Other', + category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other', } } // If hotel with place + days, pass hotel data for auto-creation or update @@ -643,15 +650,23 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
- set('price', e.target.value)} + { const v = e.target.value; if (v === '' || /^\d*\.?\d{0,2}$/.test(v)) set('price', v) }} placeholder="0.00" style={inputStyle} />
- set('budget_category', e.target.value)} - placeholder={t('reservations.budgetCategoryPlaceholder')} - style={inputStyle} /> + set('budget_category', v)} + options={[ + { value: '', label: t('reservations.budgetCategoryAuto') }, + ...budgetCategories.map(c => ({ value: c, label: c })), + ]} + placeholder={t('reservations.budgetCategoryAuto')} + size="sm" + />
{form.price && parseFloat(form.price) > 0 && ( diff --git a/client/src/i18n/translations/ar.ts b/client/src/i18n/translations/ar.ts index 44d7e13..f0b6596 100644 --- a/client/src/i18n/translations/ar.ts +++ b/client/src/i18n/translations/ar.ts @@ -938,6 +938,11 @@ const ar: Record = { 'reservations.linkAssignment': 'ربط بخطة اليوم', 'reservations.pickAssignment': 'اختر عنصرًا من خطتك...', 'reservations.noAssignment': 'بلا ربط', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'المغادرة', 'reservations.arrivalDate': 'الوصول', 'reservations.departureTime': 'وقت المغادرة', diff --git a/client/src/i18n/translations/br.ts b/client/src/i18n/translations/br.ts index 48c5a4c..aad3bc0 100644 --- a/client/src/i18n/translations/br.ts +++ b/client/src/i18n/translations/br.ts @@ -919,6 +919,11 @@ const br: Record = { 'reservations.linkAssignment': 'Vincular à atribuição do dia', 'reservations.pickAssignment': 'Selecione uma atribuição do seu plano...', 'reservations.noAssignment': 'Sem vínculo (avulsa)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Partida', 'reservations.arrivalDate': 'Chegada', 'reservations.departureTime': 'Hora partida', diff --git a/client/src/i18n/translations/cs.ts b/client/src/i18n/translations/cs.ts index 54a1ace..97c2014 100644 --- a/client/src/i18n/translations/cs.ts +++ b/client/src/i18n/translations/cs.ts @@ -936,6 +936,11 @@ const cs: Record = { 'reservations.linkAssignment': 'Propojit s přiřazením dne', 'reservations.pickAssignment': 'Vyberte přiřazení z vašeho plánu...', 'reservations.noAssignment': 'Bez propojení (samostatné)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Odlet', 'reservations.arrivalDate': 'Přílet', 'reservations.departureTime': 'Čas odletu', diff --git a/client/src/i18n/translations/de.ts b/client/src/i18n/translations/de.ts index cc358d6..1185128 100644 --- a/client/src/i18n/translations/de.ts +++ b/client/src/i18n/translations/de.ts @@ -938,6 +938,7 @@ const de: Record = { 'reservations.price': 'Preis', 'reservations.budgetCategory': 'Budgetkategorie', 'reservations.budgetCategoryPlaceholder': 'z.B. Transport, Unterkunft', + 'reservations.budgetCategoryAuto': 'Auto (aus Buchungstyp)', 'reservations.budgetHint': 'Beim Speichern wird automatisch ein Budgeteintrag erstellt.', 'reservations.departureDate': 'Abflug', 'reservations.arrivalDate': 'Ankunft', diff --git a/client/src/i18n/translations/en.ts b/client/src/i18n/translations/en.ts index 10a879e..91077b0 100644 --- a/client/src/i18n/translations/en.ts +++ b/client/src/i18n/translations/en.ts @@ -935,6 +935,7 @@ const en: Record = { 'reservations.price': 'Price', 'reservations.budgetCategory': 'Budget category', 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Departure', 'reservations.arrivalDate': 'Arrival', diff --git a/client/src/i18n/translations/es.ts b/client/src/i18n/translations/es.ts index a88aaeb..3c5ee41 100644 --- a/client/src/i18n/translations/es.ts +++ b/client/src/i18n/translations/es.ts @@ -895,6 +895,11 @@ const es: Record = { 'reservations.linkAssignment': 'Vincular a una asignación del día', 'reservations.pickAssignment': 'Selecciona una asignación de tu plan...', 'reservations.noAssignment': 'Sin vínculo (independiente)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Salida', 'reservations.arrivalDate': 'Llegada', 'reservations.departureTime': 'Hora salida', diff --git a/client/src/i18n/translations/fr.ts b/client/src/i18n/translations/fr.ts index 02eb852..5334882 100644 --- a/client/src/i18n/translations/fr.ts +++ b/client/src/i18n/translations/fr.ts @@ -934,6 +934,11 @@ const fr: Record = { 'reservations.linkAssignment': 'Lier à l\'affectation du jour', 'reservations.pickAssignment': 'Sélectionnez une affectation de votre plan…', 'reservations.noAssignment': 'Aucun lien (autonome)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Départ', 'reservations.arrivalDate': 'Arrivée', 'reservations.departureTime': 'Heure dép.', diff --git a/client/src/i18n/translations/hu.ts b/client/src/i18n/translations/hu.ts index edd57ea..7017d67 100644 --- a/client/src/i18n/translations/hu.ts +++ b/client/src/i18n/translations/hu.ts @@ -935,6 +935,11 @@ const hu: Record = { 'reservations.linkAssignment': 'Összekapcsolás napi tervvel', 'reservations.pickAssignment': 'Válassz hozzárendelést a tervedből...', 'reservations.noAssignment': 'Nincs összekapcsolás (önálló)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Indulás', 'reservations.arrivalDate': 'Érkezés', 'reservations.departureTime': 'Indulási idő', diff --git a/client/src/i18n/translations/it.ts b/client/src/i18n/translations/it.ts index f3e9767..f74456b 100644 --- a/client/src/i18n/translations/it.ts +++ b/client/src/i18n/translations/it.ts @@ -935,6 +935,11 @@ const it: Record = { 'reservations.linkAssignment': 'Collega all\'assegnazione del giorno', 'reservations.pickAssignment': 'Seleziona un\'assegnazione dal tuo programma...', 'reservations.noAssignment': 'Nessun collegamento (autonomo)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Partenza', 'reservations.arrivalDate': 'Arrivo', 'reservations.departureTime': 'Ora part.', diff --git a/client/src/i18n/translations/nl.ts b/client/src/i18n/translations/nl.ts index d693286..3bcd4e7 100644 --- a/client/src/i18n/translations/nl.ts +++ b/client/src/i18n/translations/nl.ts @@ -934,6 +934,11 @@ const nl: Record = { 'reservations.linkAssignment': 'Koppelen aan dagtoewijzing', 'reservations.pickAssignment': 'Selecteer een toewijzing uit je plan...', 'reservations.noAssignment': 'Geen koppeling (zelfstandig)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Vertrek', 'reservations.arrivalDate': 'Aankomst', 'reservations.departureTime': 'Vertrektijd', diff --git a/client/src/i18n/translations/pl.ts b/client/src/i18n/translations/pl.ts index 99574b9..031dbc9 100644 --- a/client/src/i18n/translations/pl.ts +++ b/client/src/i18n/translations/pl.ts @@ -890,6 +890,11 @@ const pl: Record = { 'reservations.linkAssignment': 'Przypisz do miejsca', 'reservations.pickAssignment': 'Wybierz miejsce z planu...', 'reservations.noAssignment': 'Brak przypisania (samodzielna)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Wylot', 'reservations.arrivalDate': 'Przylot', 'reservations.departureTime': 'Godz. wylotu', diff --git a/client/src/i18n/translations/ru.ts b/client/src/i18n/translations/ru.ts index 87f19b0..f961d08 100644 --- a/client/src/i18n/translations/ru.ts +++ b/client/src/i18n/translations/ru.ts @@ -934,6 +934,11 @@ const ru: Record = { 'reservations.linkAssignment': 'Привязать к назначению дня', 'reservations.pickAssignment': 'Выберите назначение из вашего плана...', 'reservations.noAssignment': 'Без привязки (самостоятельное)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': 'Вылет', 'reservations.arrivalDate': 'Прилёт', 'reservations.departureTime': 'Время вылета', diff --git a/client/src/i18n/translations/zh.ts b/client/src/i18n/translations/zh.ts index 9099aac..58026c6 100644 --- a/client/src/i18n/translations/zh.ts +++ b/client/src/i18n/translations/zh.ts @@ -934,6 +934,11 @@ const zh: Record = { 'reservations.linkAssignment': '关联日程分配', 'reservations.pickAssignment': '从计划中选择一个分配...', 'reservations.noAssignment': '无关联(独立)', + 'reservations.price': 'Price', + 'reservations.budgetCategory': 'Budget category', + 'reservations.budgetCategoryPlaceholder': 'e.g. Transport, Accommodation', + 'reservations.budgetCategoryAuto': 'Auto (from booking type)', + 'reservations.budgetHint': 'A budget entry will be created automatically when saving.', 'reservations.departureDate': '出发', 'reservations.arrivalDate': '到达', 'reservations.departureTime': '出发时间', diff --git a/server/src/routes/reservations.ts b/server/src/routes/reservations.ts index 8982af0..b606811 100644 --- a/server/src/routes/reservations.ts +++ b/server/src/routes/reservations.ts @@ -60,7 +60,9 @@ router.post('/', authenticate, (req: Request, res: Response) => { total_price: create_budget_entry.total_price, }); broadcast(tripId, 'budget:created', { item: budgetItem }, req.headers['x-socket-id'] as string); - } catch {} + } catch (err) { + console.error('[reservations] Failed to create budget entry:', err); + } } res.status(201).json({ reservation }); @@ -96,7 +98,7 @@ router.put('/positions', authenticate, (req: Request, res: Response) => { router.put('/:id', authenticate, (req: Request, res: Response) => { const authReq = req as AuthRequest; const { tripId, id } = req.params; - const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation } = req.body; + const { title, reservation_time, reservation_end_time, location, confirmation_number, notes, day_id, place_id, assignment_id, status, type, accommodation_id, metadata, create_accommodation, create_budget_entry } = req.body; const trip = verifyTripAccess(tripId, authReq.user.id); if (!trip) return res.status(404).json({ error: 'Trip not found' }); @@ -117,6 +119,21 @@ router.put('/:id', authenticate, (req: Request, res: Response) => { broadcast(tripId, 'accommodation:updated', {}, req.headers['x-socket-id'] as string); } + // Auto-create budget entry if price was provided + if (create_budget_entry && create_budget_entry.total_price > 0) { + try { + const { createBudgetItem } = require('../services/budgetService'); + const budgetItem = createBudgetItem(tripId, { + name: title || current.title, + category: create_budget_entry.category || type || current.type || 'Other', + total_price: create_budget_entry.total_price, + }); + broadcast(tripId, 'budget:created', { item: budgetItem }, req.headers['x-socket-id'] as string); + } catch (err) { + console.error('[reservations] Failed to create budget entry:', err); + } + } + res.json({ reservation }); broadcast(tripId, 'reservation:updated', { reservation }, req.headers['x-socket-id'] as string);