diff --git a/backend/migrations/20260421_add_budget_to_member_orders.sql b/backend/migrations/20260421_add_budget_to_member_orders.sql new file mode 100644 index 00000000..1107b566 --- /dev/null +++ b/backend/migrations/20260421_add_budget_to_member_orders.sql @@ -0,0 +1,5 @@ +ALTER TABLE `member_orders` + ADD COLUMN `budget` DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER `paid_amount`; + +ALTER TABLE `member_order_history` + ADD COLUMN `budget` DECIMAL(10,2) NOT NULL DEFAULT 0.00 AFTER `paid_amount`; diff --git a/backend/models/MemberOrder.js b/backend/models/MemberOrder.js index d3d892ea..bff2c2bd 100644 --- a/backend/models/MemberOrder.js +++ b/backend/models/MemberOrder.js @@ -49,6 +49,11 @@ const MemberOrder = sequelize.define('MemberOrder', { allowNull: false, defaultValue: 0, field: 'paid_amount' + }, + budget: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false, + defaultValue: 0 } }, { underscored: true, diff --git a/backend/models/MemberOrderHistory.js b/backend/models/MemberOrderHistory.js index f8445a42..feba6d94 100644 --- a/backend/models/MemberOrderHistory.js +++ b/backend/models/MemberOrderHistory.js @@ -47,6 +47,11 @@ const MemberOrderHistory = sequelize.define('MemberOrderHistory', { allowNull: false, defaultValue: 0, field: 'paid_amount' + }, + budget: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false, + defaultValue: 0 } }, { underscored: true, diff --git a/backend/services/memberOrderService.js b/backend/services/memberOrderService.js index c9e717cd..d78eb08a 100644 --- a/backend/services/memberOrderService.js +++ b/backend/services/memberOrderService.js @@ -20,10 +20,12 @@ const serializeOrder = (order) => { const plain = typeof order.toJSON === 'function' ? order.toJSON() : { ...order }; const cost = normalizeAmount(plain.cost); const paidAmount = normalizeAmount(plain.paidAmount); + const budget = normalizeAmount(plain.budget); return { ...plain, cost, paidAmount, + budget, openAmount: Math.max(0, Number((cost - paidAmount).toFixed(2))) }; }; @@ -32,10 +34,12 @@ const serializeHistoryEntry = (entry) => { const plain = typeof entry.toJSON === 'function' ? entry.toJSON() : { ...entry }; const cost = normalizeAmount(plain.cost); const paidAmount = normalizeAmount(plain.paidAmount); + const budget = normalizeAmount(plain.budget); return { ...plain, cost, paidAmount, + budget, openAmount: Math.max(0, Number((cost - paidAmount).toFixed(2))) }; }; @@ -64,7 +68,8 @@ class MemberOrderService { status: order.status, changedAt: new Date(), cost: normalizeAmount(order.cost), - paidAmount: normalizeAmount(order.paidAmount) + paidAmount: normalizeAmount(order.paidAmount), + budget: normalizeAmount(order.budget) }, transaction ? { transaction } : undefined); } @@ -105,6 +110,7 @@ class MemberOrderService { const status = ORDER_STATUSES.includes(payload?.status) ? payload.status : 'requested'; const cost = normalizeAmount(payload?.cost); const paidAmount = normalizeAmount(payload?.paidAmount); + const budget = normalizeAmount(payload?.budget); if (!item) { return { @@ -121,7 +127,8 @@ class MemberOrderService { orderDate: new Date(), statusDate: new Date(), cost, - paidAmount + paidAmount, + budget }); await this._createHistorySnapshot(order); @@ -171,6 +178,7 @@ class MemberOrderService { const nextStatus = payload?.status && ORDER_STATUSES.includes(payload.status) ? payload.status : order.status; const nextCost = payload?.cost != null ? normalizeAmount(payload.cost) : normalizeAmount(order.cost); const nextPaidAmount = payload?.paidAmount != null ? normalizeAmount(payload.paidAmount) : normalizeAmount(order.paidAmount); + const nextBudget = payload?.budget != null ? normalizeAmount(payload.budget) : normalizeAmount(order.budget); if (nextItem && nextItem !== order.item) { order.item = nextItem; @@ -189,6 +197,10 @@ class MemberOrderService { order.paidAmount = nextPaidAmount; changed = true; } + if (nextBudget !== normalizeAmount(order.budget)) { + order.budget = nextBudget; + changed = true; + } if (!changed) { const existing = await this.getMemberOrders(userToken, clubId, memberId); diff --git a/frontend/src/components/OrdersPanel.vue b/frontend/src/components/OrdersPanel.vue index 140c7d9f..e42404dd 100644 --- a/frontend/src/components/OrdersPanel.vue +++ b/frontend/src/components/OrdersPanel.vue @@ -40,6 +40,10 @@ {{ $t('orders.paid') }} + + {{ $t('orders.budget') }} + + {{ $t('orders.status') }} @@ -75,6 +79,7 @@ {{ $t('orders.statusDate') }} {{ $t('orders.cost') }} {{ $t('orders.paid') }} + {{ $t('orders.budget') }} {{ $t('orders.open') }} {{ $t('orders.history') }} {{ $t('common.save') }} @@ -102,6 +107,9 @@ + + + {{ formatCurrency(calculateOpenAmount(order)) }} @@ -110,7 +118,7 @@ {{ statusLabel(entry.status) }} {{ formatDateTime(entry.changedAt) }} - {{ formatCurrency(entry.cost) }} / {{ formatCurrency(entry.paidAmount) }} + {{ formatCurrency(entry.cost) }} / {{ formatCurrency(entry.paidAmount) }} / {{ formatCurrency(entry.budget) }} @@ -181,7 +189,8 @@ export default { item: '', status: 'requested', cost: '', - paidAmount: '' + paidAmount: '', + budget: '' } }; }, @@ -251,7 +260,8 @@ export default { draftItem: order.item || '', draftStatus: order.status || 'requested', draftCost: String(normalizeAmount(order.cost)), - draftPaidAmount: String(normalizeAmount(order.paidAmount)) + draftPaidAmount: String(normalizeAmount(order.paidAmount)), + draftBudget: String(normalizeAmount(order.budget)) }; }, async loadOrders() { @@ -285,7 +295,8 @@ export default { item: this.newOrder.item.trim(), status: this.newOrder.status, cost: normalizeAmount(this.newOrder.cost), - paidAmount: normalizeAmount(this.newOrder.paidAmount) + paidAmount: normalizeAmount(this.newOrder.paidAmount), + budget: normalizeAmount(this.newOrder.budget) }); if (response.data?.order) { this.orders.unshift(this.hydrateOrder(response.data.order)); @@ -293,7 +304,8 @@ export default { item: '', status: 'requested', cost: '', - paidAmount: '' + paidAmount: '', + budget: '' }; } } catch (error) { @@ -306,7 +318,8 @@ export default { return order.draftItem !== (order.item || '') || order.draftStatus !== order.status || normalizeAmount(order.draftCost) !== normalizeAmount(order.cost) - || normalizeAmount(order.draftPaidAmount) !== normalizeAmount(order.paidAmount); + || normalizeAmount(order.draftPaidAmount) !== normalizeAmount(order.paidAmount) + || normalizeAmount(order.draftBudget) !== normalizeAmount(order.budget); }, async saveOrder(order) { this.savingOrderIds.push(order.id); @@ -316,7 +329,8 @@ export default { item: order.draftItem, status: order.draftStatus, cost: normalizeAmount(order.draftCost), - paidAmount: normalizeAmount(order.draftPaidAmount) + paidAmount: normalizeAmount(order.draftPaidAmount), + budget: normalizeAmount(order.draftBudget) }); if (response.data?.order) { const updated = this.hydrateOrder(response.data.order); diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index c0d4b14a..710a83dc 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -457,6 +457,7 @@ "statusHandedOver": "Artikel ausgehändigt", "cost": "Kosten", "paid": "Bezahlt", + "budget": "Budget", "open": "Noch offen", "history": "Verlauf", "orderDate": "Erfasst am",