diff --git a/backend/migrations/20260506_add_paid_confirmed_to_member_orders.sql b/backend/migrations/20260506_add_paid_confirmed_to_member_orders.sql new file mode 100644 index 00000000..47ef07c1 --- /dev/null +++ b/backend/migrations/20260506_add_paid_confirmed_to_member_orders.sql @@ -0,0 +1,5 @@ +ALTER TABLE `member_orders` + ADD COLUMN `paid_confirmed` TINYINT(1) NOT NULL DEFAULT 0 AFTER `budget`; + +ALTER TABLE `member_order_history` + ADD COLUMN `paid_confirmed` TINYINT(1) NOT NULL DEFAULT 0 AFTER `budget`; diff --git a/backend/models/MemberOrder.js b/backend/models/MemberOrder.js index bff2c2bd..d5f3c676 100644 --- a/backend/models/MemberOrder.js +++ b/backend/models/MemberOrder.js @@ -54,6 +54,12 @@ const MemberOrder = sequelize.define('MemberOrder', { type: DataTypes.DECIMAL(10, 2), allowNull: false, defaultValue: 0 + }, + paidConfirmed: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + field: 'paid_confirmed' } }, { underscored: true, diff --git a/backend/models/MemberOrderHistory.js b/backend/models/MemberOrderHistory.js index feba6d94..3b0bacf0 100644 --- a/backend/models/MemberOrderHistory.js +++ b/backend/models/MemberOrderHistory.js @@ -52,6 +52,12 @@ const MemberOrderHistory = sequelize.define('MemberOrderHistory', { type: DataTypes.DECIMAL(10, 2), allowNull: false, defaultValue: 0 + }, + paidConfirmed: { + type: DataTypes.BOOLEAN, + allowNull: false, + defaultValue: false, + field: 'paid_confirmed' } }, { underscored: true, diff --git a/backend/services/memberOrderService.js b/backend/services/memberOrderService.js index d78eb08a..5aac04b9 100644 --- a/backend/services/memberOrderService.js +++ b/backend/services/memberOrderService.js @@ -21,11 +21,13 @@ const serializeOrder = (order) => { const cost = normalizeAmount(plain.cost); const paidAmount = normalizeAmount(plain.paidAmount); const budget = normalizeAmount(plain.budget); + const paidConfirmed = Boolean(plain.paidConfirmed); return { ...plain, cost, paidAmount, budget, + paidConfirmed, openAmount: Math.max(0, Number((cost - paidAmount).toFixed(2))) }; }; @@ -35,11 +37,13 @@ const serializeHistoryEntry = (entry) => { const cost = normalizeAmount(plain.cost); const paidAmount = normalizeAmount(plain.paidAmount); const budget = normalizeAmount(plain.budget); + const paidConfirmed = Boolean(plain.paidConfirmed); return { ...plain, cost, paidAmount, budget, + paidConfirmed, openAmount: Math.max(0, Number((cost - paidAmount).toFixed(2))) }; }; @@ -69,7 +73,8 @@ class MemberOrderService { changedAt: new Date(), cost: normalizeAmount(order.cost), paidAmount: normalizeAmount(order.paidAmount), - budget: normalizeAmount(order.budget) + budget: normalizeAmount(order.budget), + paidConfirmed: Boolean(order.paidConfirmed) }, transaction ? { transaction } : undefined); } @@ -111,6 +116,7 @@ class MemberOrderService { const cost = normalizeAmount(payload?.cost); const paidAmount = normalizeAmount(payload?.paidAmount); const budget = normalizeAmount(payload?.budget); + const paidConfirmed = Boolean(payload?.paidConfirmed); if (!item) { return { @@ -128,7 +134,8 @@ class MemberOrderService { statusDate: new Date(), cost, paidAmount, - budget + budget, + paidConfirmed }); await this._createHistorySnapshot(order); @@ -179,6 +186,7 @@ class MemberOrderService { 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); + const nextPaidConfirmed = payload?.paidConfirmed != null ? Boolean(payload.paidConfirmed) : Boolean(order.paidConfirmed); if (nextItem && nextItem !== order.item) { order.item = nextItem; @@ -201,6 +209,10 @@ class MemberOrderService { order.budget = nextBudget; changed = true; } + if (nextPaidConfirmed !== Boolean(order.paidConfirmed)) { + order.paidConfirmed = nextPaidConfirmed; + 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 990def34..6d8baae6 100644 --- a/frontend/src/components/OrdersPanel.vue +++ b/frontend/src/components/OrdersPanel.vue @@ -53,6 +53,10 @@ {{ $t('orders.budget') }} + + {{ $t('orders.paidConfirmed') }} + + {{ $t('orders.status') }} @@ -88,6 +92,7 @@ {{ $t('orders.statusDate') }} {{ $t('orders.cost') }} {{ $t('orders.paid') }} + {{ $t('orders.paidConfirmed') }} {{ $t('orders.budget') }} {{ $t('orders.open') }} {{ $t('orders.history') }} @@ -116,6 +121,9 @@ + + + @@ -127,7 +135,7 @@ {{ statusLabel(entry.status) }} {{ formatDateTime(entry.changedAt) }} - {{ formatCurrency(entry.cost) }} / {{ formatCurrency(entry.paidAmount) }} / {{ formatCurrency(entry.budget) }} + {{ formatCurrency(entry.cost) }} / {{ formatCurrency(entry.paidAmount) }} / {{ entry.paidConfirmed ? 'bezahlt' : 'offen' }} / {{ formatCurrency(entry.budget) }} @@ -209,7 +217,8 @@ export default { status: 'requested', cost: '', paidAmount: '', - budget: '' + budget: '', + paidConfirmed: false } }; }, @@ -237,12 +246,9 @@ export default { if (this.clubFilter && String(order.clubId) !== String(this.clubFilter)) { return false; } - const cost = normalizeAmount(order.cost); - const paidAmount = normalizeAmount(order.paidAmount); - const openAmount = Math.max(0, Number((cost - paidAmount).toFixed(2))); - const isPaid = cost > 0 && openAmount <= 0; + const isPaid = Boolean(order.paidConfirmed); const isHandedOver = order.status === 'handed_over'; - const isCompleted = isPaid || isHandedOver; + const isCompleted = isPaid && isHandedOver; if (!this.showCompleted && isCompleted) { return false; } @@ -296,6 +302,7 @@ export default { draftStatus: order.status || 'requested', draftCost: String(normalizeAmount(order.cost)), draftPaidAmount: String(normalizeAmount(order.paidAmount)), + draftPaidConfirmed: Boolean(order.paidConfirmed), draftBudget: String(normalizeAmount(order.budget)) }; }, @@ -331,7 +338,8 @@ export default { status: this.newOrder.status, cost: normalizeAmount(this.newOrder.cost), paidAmount: normalizeAmount(this.newOrder.paidAmount), - budget: normalizeAmount(this.newOrder.budget) + budget: normalizeAmount(this.newOrder.budget), + paidConfirmed: Boolean(this.newOrder.paidConfirmed) }); if (response.data?.order) { this.orders.unshift(this.hydrateOrder(response.data.order)); @@ -340,7 +348,8 @@ export default { status: 'requested', cost: '', paidAmount: '', - budget: '' + budget: '', + paidConfirmed: false }; } } catch (error) { @@ -354,6 +363,7 @@ export default { || order.draftStatus !== order.status || normalizeAmount(order.draftCost) !== normalizeAmount(order.cost) || normalizeAmount(order.draftPaidAmount) !== normalizeAmount(order.paidAmount) + || Boolean(order.draftPaidConfirmed) !== Boolean(order.paidConfirmed) || normalizeAmount(order.draftBudget) !== normalizeAmount(order.budget); }, async saveOrder(order) { @@ -365,7 +375,8 @@ export default { status: order.draftStatus, cost: normalizeAmount(order.draftCost), paidAmount: normalizeAmount(order.draftPaidAmount), - budget: normalizeAmount(order.draftBudget) + budget: normalizeAmount(order.draftBudget), + paidConfirmed: Boolean(order.draftPaidConfirmed) }); if (response.data?.order) { const updated = this.hydrateOrder(response.data.order); @@ -434,6 +445,23 @@ export default { color: var(--text-color); } +.orders-checkbox-label { + display: flex; + flex-direction: column; + justify-content: flex-end; + gap: 0.35rem; +} + +.orders-checkbox-label input[type='checkbox'], +.orders-cell-center input[type='checkbox'] { + width: 1.05rem; + height: 1.05rem; +} + +.orders-cell-center { + text-align: center; +} + .orders-filters { justify-content: flex-start; flex: 1; diff --git a/frontend/src/i18n/locales/de.json b/frontend/src/i18n/locales/de.json index b386180a..9d369cd3 100644 --- a/frontend/src/i18n/locales/de.json +++ b/frontend/src/i18n/locales/de.json @@ -462,6 +462,7 @@ "statusHandedOver": "Artikel ausgehändigt", "cost": "Kosten", "paid": "Bezahlt", + "paidConfirmed": "Hat bezahlt", "budget": "Budget", "open": "Noch offen", "history": "Verlauf",