Add status/paymentStatus/year filters to the order overview

Server-side, URL-search-param-driven filtering (?status=&paymentStatus=&year=)
so the page stays a Server Component — each filter chip is a plain Link
to the same route with one param changed, shareable/bookmarkable/back-
button-safe for free. getCustomerOrders() gained an optional filters
param translated into additional Payload where[] clauses; a new
getCustomerOrderYears() derives the year filter's option list from the
customer's actual orders instead of a hardcoded range. paymentStatus's
"Offen" filter option mirrors PaymentStatusBadge's own grouping
(pending + not_applicable as one option, not two).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Marco
2026-07-30 22:03:30 +00:00
parent e7d5dd8b1e
commit 2f8c184dc6
2 changed files with 168 additions and 5 deletions
+57 -1
View File
@@ -498,15 +498,61 @@ function excludeFailedPaymentAttemptsQuery(): Record<string, string> {
};
}
// "Offen" in the account UI's PaymentStatusBadge covers two distinct
// backend values (an unconfirmed Stripe payment vs. an Überweisung order
// awaiting manual reconciliation) — the filter chip mirrors that same
// grouping rather than exposing the internal distinction as two options.
const OPEN_PAYMENT_STATUSES = ["pending", "not_applicable"] as const;
export type CustomerOrderFilters = {
status?: string;
paymentStatus?: string;
/** Calendar year as a string, e.g. "2026" — matches createdAt within
* [Jan 1, Jan 1 of next year). */
year?: string;
};
function customerOrderFilterQuery(filters: CustomerOrderFilters | undefined, whereIndex: number): Record<string, string> {
if (!filters) return {};
const params: Record<string, string> = {};
let i = whereIndex;
if (filters.status) {
params[`where[and][${i}][status][equals]`] = filters.status;
i += 1;
}
if (filters.paymentStatus) {
if (filters.paymentStatus === "open") {
OPEN_PAYMENT_STATUSES.forEach((value, j) => {
params[`where[and][${i}][or][${j}][paymentStatus][equals]`] = value;
});
} else {
params[`where[and][${i}][paymentStatus][equals]`] = filters.paymentStatus;
}
i += 1;
}
if (filters.year && /^\d{4}$/.test(filters.year)) {
const year = Number(filters.year);
params[`where[and][${i}][createdAt][greater_than_equal]`] = new Date(Date.UTC(year, 0, 1)).toISOString();
params[`where[and][${i}][createdAt][less_than]`] = new Date(Date.UTC(year + 1, 0, 1)).toISOString();
}
return params;
}
// `excludeFailedPaymentAttempts` defaults to true (list views) — the one
// exception is /api/account/export/route.ts's GDPR data export, which
// passes false: a legal completeness export must include every order
// row that exists about this customer, not just the ones normally shown
// in "Meine Bestellungen".
export async function getCustomerOrders(token: string, customerId: number, excludeFailedPaymentAttempts = true): Promise<CustomerOrder[]> {
export async function getCustomerOrders(
token: string,
customerId: number,
excludeFailedPaymentAttempts = true,
filters?: CustomerOrderFilters,
): Promise<CustomerOrder[]> {
const params = new URLSearchParams({
"where[and][0][customer][equals]": String(customerId),
...(excludeFailedPaymentAttempts ? excludeFailedPaymentAttemptsQuery() : {}),
...customerOrderFilterQuery(filters, excludeFailedPaymentAttempts ? 2 : 1),
sort: "-createdAt",
depth: "0",
limit: "50",
@@ -537,6 +583,16 @@ export async function getCustomerOrders(token: string, customerId: number, exclu
}));
}
// All years that have at least one (non-filtered-out) order for this
// customer — powers the year filter's option list without hardcoding a
// range. Cheap: reuses the same excludeFailedPaymentAttempts query, no
// separate collection/aggregation endpoint needed for this order volume.
export async function getCustomerOrderYears(token: string, customerId: number): Promise<string[]> {
const orders = await getCustomerOrders(token, customerId, true);
const years = new Set(orders.map((o) => new Date(o.createdAt).getUTCFullYear().toString()));
return Array.from(years).sort((a, b) => Number(b) - Number(a));
}
export type CustomerOrderDetail = CustomerOrder & {
id: number;
// 'manual' (Überweisung) vs 'stripe' (Kreditkarte/PayPal) — see