feat(budget): bidirectional sync between reservations and budget items

- Link budget items to reservations via reservation_id column
- Update budget entry when reservation price changes (not create duplicate)
- Delete budget entry when reservation price is cleared
- Sync price back to reservation when edited in budget panel
- Lock budget item name when linked to a reservation
- Add migration 73 for reservation_id on budget_items
This commit is contained in:
Maurice
2026-04-05 18:16:02 +02:00
parent cd5a6c7491
commit 38206883ff
8 changed files with 65 additions and 17 deletions

View File

@@ -633,7 +633,7 @@ export default function BudgetPanel({ tripId, tripMembers = [] }: BudgetPanelPro
onMouseEnter={e => e.currentTarget.style.background = 'var(--bg-hover)'}
onMouseLeave={e => e.currentTarget.style.background = 'transparent'}>
<td style={td}>
<InlineEditCell value={item.name} onSave={v => handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={t('budget.editTooltip')} readOnly={!canEdit} />
<InlineEditCell value={item.name} onSave={v => handleUpdateField(item.id, 'name', v)} placeholder={t('budget.table.name')} locale={locale} editTooltip={item.reservation_id ? t('budget.linkedToReservation') : t('budget.editTooltip')} readOnly={!canEdit || !!item.reservation_id} />
{/* Mobile: larger chips under name since Persons column is hidden */}
{hasMultipleMembers && (
<div className="sm:hidden" style={{ marginTop: 4 }}>

View File

@@ -207,13 +207,10 @@ export function ReservationModal({ isOpen, onClose, onSave, reservation, days, p
accommodation_id: form.type === 'hotel' ? (form.accommodation_id || null) : null,
metadata: Object.keys(metadata).length > 0 ? metadata : null,
}
// Auto-create budget entry if price is set
if (form.price && parseFloat(form.price) > 0) {
saveData.create_budget_entry = {
total_price: parseFloat(form.price),
category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other',
}
}
// Auto-create/update budget entry if price is set, or signal removal if cleared
saveData.create_budget_entry = form.price && parseFloat(form.price) > 0
? { total_price: parseFloat(form.price), category: form.budget_category || t(`reservations.type.${form.type}`) || 'Other' }
: { total_price: 0 }
// If hotel with place + days, pass hotel data for auto-creation or update
if (form.type === 'hotel' && form.hotel_place_id && form.hotel_start_day && form.hotel_end_day) {
saveData.create_accommodation = {

View File

@@ -1005,6 +1005,7 @@ const de: Record<string, string | { name: string; category: string }[]> = {
'budget.totalBudget': 'Gesamtbudget',
'budget.byCategory': 'Nach Kategorie',
'budget.editTooltip': 'Klicken zum Bearbeiten',
'budget.linkedToReservation': 'Verknüpft mit einer Buchung — Name dort bearbeiten',
'budget.confirm.deleteCategory': 'Möchtest du die Kategorie "{name}" mit {count} Einträgen wirklich löschen?',
'budget.deleteCategory': 'Kategorie löschen',
'budget.perPerson': 'Pro Person',

View File

@@ -1024,6 +1024,7 @@ const en: Record<string, string | { name: string; category: string }[]> = {
'budget.totalBudget': 'Total Budget',
'budget.byCategory': 'By Category',
'budget.editTooltip': 'Click to edit',
'budget.linkedToReservation': 'Linked to a reservation — edit the name there',
'budget.confirm.deleteCategory': 'Are you sure you want to delete the category "{name}" with {count} entries?',
'budget.deleteCategory': 'Delete Category',
'budget.perPerson': 'Per Person',

View File

@@ -42,6 +42,9 @@ export const createBudgetSlice = (set: SetState, get: GetState): BudgetSlice =>
set(state => ({
budgetItems: state.budgetItems.map(item => item.id === id ? result.item : item)
}))
if (result.item.reservation_id && data.total_price !== undefined) {
get().loadReservations(tripId)
}
return result.item
} catch (err: unknown) {
throw new Error(getApiErrorMessage(err, 'Error updating budget item'))