feat(MemberOrders): implement member orders feature
- Added new models and routes for managing member orders and order history. - Updated server.js to include member order routes and sync functionality. - Enhanced frontend with new components and dialogs for viewing and managing orders. - Integrated internationalization support for order-related texts across multiple languages. - Updated navigation and views to include access to the new orders feature, improving user experience.
This commit is contained in:
296
backend/services/memberOrderService.js
Normal file
296
backend/services/memberOrderService.js
Normal file
@@ -0,0 +1,296 @@
|
||||
import { Op } from 'sequelize';
|
||||
import Member from '../models/Member.js';
|
||||
import MemberOrder from '../models/MemberOrder.js';
|
||||
import MemberOrderHistory from '../models/MemberOrderHistory.js';
|
||||
import Club from '../models/Club.js';
|
||||
import UserClub from '../models/UserClub.js';
|
||||
import { checkAccess, getUserByToken } from '../utils/userUtils.js';
|
||||
|
||||
const ORDER_STATUSES = ['requested', 'ordered', 'arrived', 'handed_over'];
|
||||
|
||||
const normalizeAmount = (value) => {
|
||||
const parsed = Number.parseFloat(value ?? 0);
|
||||
if (!Number.isFinite(parsed)) {
|
||||
return 0;
|
||||
}
|
||||
return Math.max(0, Number(parsed.toFixed(2)));
|
||||
};
|
||||
|
||||
const serializeOrder = (order) => {
|
||||
const plain = typeof order.toJSON === 'function' ? order.toJSON() : { ...order };
|
||||
const cost = normalizeAmount(plain.cost);
|
||||
const paidAmount = normalizeAmount(plain.paidAmount);
|
||||
return {
|
||||
...plain,
|
||||
cost,
|
||||
paidAmount,
|
||||
openAmount: Math.max(0, Number((cost - paidAmount).toFixed(2)))
|
||||
};
|
||||
};
|
||||
|
||||
const serializeHistoryEntry = (entry) => {
|
||||
const plain = typeof entry.toJSON === 'function' ? entry.toJSON() : { ...entry };
|
||||
const cost = normalizeAmount(plain.cost);
|
||||
const paidAmount = normalizeAmount(plain.paidAmount);
|
||||
return {
|
||||
...plain,
|
||||
cost,
|
||||
paidAmount,
|
||||
openAmount: Math.max(0, Number((cost - paidAmount).toFixed(2)))
|
||||
};
|
||||
};
|
||||
|
||||
class MemberOrderService {
|
||||
async _ensureMemberAccess(userToken, clubId, memberId) {
|
||||
await checkAccess(userToken, clubId);
|
||||
const member = await Member.findOne({
|
||||
where: {
|
||||
id: memberId,
|
||||
clubId
|
||||
}
|
||||
});
|
||||
if (!member) {
|
||||
throw new Error('Mitglied nicht gefunden');
|
||||
}
|
||||
return member;
|
||||
}
|
||||
|
||||
async _createHistorySnapshot(order, transaction = null) {
|
||||
return MemberOrderHistory.create({
|
||||
memberOrderId: order.id,
|
||||
memberId: order.memberId,
|
||||
clubId: order.clubId,
|
||||
item: order.item,
|
||||
status: order.status,
|
||||
changedAt: new Date(),
|
||||
cost: normalizeAmount(order.cost),
|
||||
paidAmount: normalizeAmount(order.paidAmount)
|
||||
}, transaction ? { transaction } : undefined);
|
||||
}
|
||||
|
||||
async getMemberOrders(userToken, clubId, memberId) {
|
||||
await this._ensureMemberAccess(userToken, clubId, memberId);
|
||||
const orders = await MemberOrder.findAll({
|
||||
where: { clubId, memberId },
|
||||
include: [
|
||||
{
|
||||
model: MemberOrderHistory,
|
||||
as: 'historyEntries',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [
|
||||
['createdAt', 'DESC'],
|
||||
[{ model: MemberOrderHistory, as: 'historyEntries' }, 'changedAt', 'DESC']
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
orders: orders.map((order) => {
|
||||
const serialized = serializeOrder(order);
|
||||
serialized.historyEntries = (order.historyEntries || []).map(serializeHistoryEntry);
|
||||
return serialized;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async createMemberOrder(userToken, clubId, memberId, payload) {
|
||||
await this._ensureMemberAccess(userToken, clubId, memberId);
|
||||
|
||||
const item = String(payload?.item || '').trim();
|
||||
const status = ORDER_STATUSES.includes(payload?.status) ? payload.status : 'requested';
|
||||
const cost = normalizeAmount(payload?.cost);
|
||||
const paidAmount = normalizeAmount(payload?.paidAmount);
|
||||
|
||||
if (!item) {
|
||||
return {
|
||||
status: 400,
|
||||
response: { success: false, error: 'Artikel fehlt' }
|
||||
};
|
||||
}
|
||||
|
||||
const order = await MemberOrder.create({
|
||||
memberId,
|
||||
clubId,
|
||||
item,
|
||||
status,
|
||||
orderDate: new Date(),
|
||||
statusDate: new Date(),
|
||||
cost,
|
||||
paidAmount
|
||||
});
|
||||
|
||||
await this._createHistorySnapshot(order);
|
||||
|
||||
const fresh = await MemberOrder.findByPk(order.id, {
|
||||
include: [
|
||||
{
|
||||
model: MemberOrderHistory,
|
||||
as: 'historyEntries',
|
||||
required: false,
|
||||
order: [['changedAt', 'DESC']]
|
||||
}
|
||||
]
|
||||
});
|
||||
|
||||
const serialized = serializeOrder(fresh);
|
||||
serialized.historyEntries = (fresh.historyEntries || []).map(serializeHistoryEntry);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
order: serialized
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async updateMemberOrder(userToken, clubId, memberId, orderId, payload) {
|
||||
await this._ensureMemberAccess(userToken, clubId, memberId);
|
||||
const order = await MemberOrder.findOne({
|
||||
where: {
|
||||
id: orderId,
|
||||
clubId,
|
||||
memberId
|
||||
}
|
||||
});
|
||||
|
||||
if (!order) {
|
||||
return {
|
||||
status: 404,
|
||||
response: { success: false, error: 'Bestellung nicht gefunden' }
|
||||
};
|
||||
}
|
||||
|
||||
let changed = false;
|
||||
const nextItem = payload?.item != null ? String(payload.item).trim() : order.item;
|
||||
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);
|
||||
|
||||
if (nextItem && nextItem !== order.item) {
|
||||
order.item = nextItem;
|
||||
changed = true;
|
||||
}
|
||||
if (nextStatus !== order.status) {
|
||||
order.status = nextStatus;
|
||||
order.statusDate = new Date();
|
||||
changed = true;
|
||||
}
|
||||
if (nextCost !== normalizeAmount(order.cost)) {
|
||||
order.cost = nextCost;
|
||||
changed = true;
|
||||
}
|
||||
if (nextPaidAmount !== normalizeAmount(order.paidAmount)) {
|
||||
order.paidAmount = nextPaidAmount;
|
||||
changed = true;
|
||||
}
|
||||
|
||||
if (!changed) {
|
||||
const existing = await this.getMemberOrders(userToken, clubId, memberId);
|
||||
const current = existing.response.orders.find((entry) => String(entry.id) === String(orderId)) || null;
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
order: current
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
await order.save();
|
||||
await this._createHistorySnapshot(order);
|
||||
|
||||
const updated = await MemberOrder.findByPk(order.id, {
|
||||
include: [
|
||||
{
|
||||
model: MemberOrderHistory,
|
||||
as: 'historyEntries',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [[{ model: MemberOrderHistory, as: 'historyEntries' }, 'changedAt', 'DESC']]
|
||||
});
|
||||
|
||||
const serialized = serializeOrder(updated);
|
||||
serialized.historyEntries = (updated.historyEntries || []).map(serializeHistoryEntry);
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
order: serialized
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
async getGlobalOrders(userToken) {
|
||||
const user = await getUserByToken(userToken);
|
||||
const userClubs = await UserClub.findAll({
|
||||
where: {
|
||||
userId: user.id,
|
||||
approved: true
|
||||
},
|
||||
attributes: ['clubId']
|
||||
});
|
||||
const clubIds = userClubs.map((entry) => entry.clubId).filter(Boolean);
|
||||
|
||||
if (!clubIds.length) {
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
orders: []
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const orders = await MemberOrder.findAll({
|
||||
where: {
|
||||
clubId: {
|
||||
[Op.in]: clubIds
|
||||
}
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Member,
|
||||
as: 'member',
|
||||
attributes: ['id', 'firstName', 'lastName']
|
||||
},
|
||||
{
|
||||
model: Club,
|
||||
as: 'club',
|
||||
attributes: ['id', 'name']
|
||||
},
|
||||
{
|
||||
model: MemberOrderHistory,
|
||||
as: 'historyEntries',
|
||||
required: false
|
||||
}
|
||||
],
|
||||
order: [
|
||||
['statusDate', 'DESC'],
|
||||
['createdAt', 'DESC'],
|
||||
[{ model: MemberOrderHistory, as: 'historyEntries' }, 'changedAt', 'DESC']
|
||||
]
|
||||
});
|
||||
|
||||
return {
|
||||
status: 200,
|
||||
response: {
|
||||
success: true,
|
||||
orders: orders.map((order) => {
|
||||
const serialized = serializeOrder(order);
|
||||
serialized.historyEntries = (order.historyEntries || []).map(serializeHistoryEntry);
|
||||
return serialized;
|
||||
})
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default new MemberOrderService();
|
||||
Reference in New Issue
Block a user