feat(orders): month/year filter on both order lists + fix received-orders search

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
BOHA
2026-06-09 13:22:39 +02:00
parent 2b7b480144
commit 3b48bd0523
8 changed files with 186 additions and 5 deletions

View File

@@ -10,6 +10,7 @@ import {
computeIssuedOrderTotals,
createIssuedOrder,
getIssuedOrder,
listIssuedOrders,
updateIssuedOrder,
deleteIssuedOrder,
} from "../services/issued-orders.service";
@@ -219,6 +220,26 @@ describe("deleteIssuedOrder", () => {
});
});
describe("listIssuedOrders month filter", () => {
it("returns only orders whose order_date is in the given month", async () => {
const inMonth = await createIssuedOrder({ order_date: "2026-03-15" });
const outMonth = await createIssuedOrder({ order_date: "2026-04-15" });
createdIds.push(inMonth.id, outMonth.id);
const res = await listIssuedOrders({
page: 1,
limit: 50,
skip: 0,
sort: "id",
order: "desc",
month: 3,
year: 2026,
});
const ids = res.data.map((o) => o.id);
expect(ids).toContain(inMonth.id);
expect(ids).not.toContain(outMonth.id);
});
});
describe("renderIssuedOrderHtml", () => {
const order = {
po_number: "26720001",

View File

@@ -0,0 +1,114 @@
import { describe, it, expect, afterEach } from "vitest";
import prisma from "../config/database";
import { createOrder, listOrders } from "../services/orders.service";
const createdOrderIds: number[] = [];
const createdCustomerIds: number[] = [];
afterEach(async () => {
for (const id of createdOrderIds)
await prisma.orders.deleteMany({ where: { id } });
for (const id of createdCustomerIds)
await prisma.customers.deleteMany({ where: { id } });
createdOrderIds.length = 0;
createdCustomerIds.length = 0;
await prisma.number_sequences.deleteMany({ where: { type: "shared" } });
});
const baseOrder = {
status: "prijata" as const,
currency: "CZK",
language: "cs",
vat_rate: 21,
apply_vat: true,
exchange_rate: 1,
};
function failResult(res: unknown): never {
throw new Error(`expected a success result, got ${JSON.stringify(res)}`);
}
async function makeCustomer() {
const c = await prisma.customers.create({
data: { name: "Odběratel a.s." },
});
createdCustomerIds.push(c.id);
return c;
}
const baseListParams = {
page: 1,
limit: 50,
skip: 0,
sort: "id",
order: "desc" as const,
};
describe("listOrders search filter", () => {
it("returns the order when search matches its order_number", async () => {
const customer = await makeCustomer();
const res = await createOrder({
...baseOrder,
customer_id: customer.id,
create_project: false,
});
if (!("id" in res)) failResult(res);
createdOrderIds.push(res.id!);
const list = await listOrders({
...baseListParams,
search: res.order_number!,
});
expect(list.data.map((o) => o.id)).toContain(res.id);
});
it("excludes the order when search matches nothing", async () => {
const customer = await makeCustomer();
const res = await createOrder({
...baseOrder,
customer_id: customer.id,
create_project: false,
});
if (!("id" in res)) failResult(res);
createdOrderIds.push(res.id!);
const list = await listOrders({
...baseListParams,
search: "ZZZ-NO-SUCH-ORDER-ZZZ",
});
expect(list.data.map((o) => o.id)).not.toContain(res.id);
});
});
describe("listOrders month filter", () => {
it("returns only orders whose created_at is in the given month", async () => {
const customer = await makeCustomer();
const res = await createOrder({
...baseOrder,
customer_id: customer.id,
create_project: false,
});
if (!("id" in res)) failResult(res);
createdOrderIds.push(res.id!);
// Pin created_at to a known month so the range filter is deterministic.
await prisma.orders.update({
where: { id: res.id },
data: { created_at: new Date(2026, 2, 15, 12, 0, 0) },
});
const inMonth = await listOrders({
...baseListParams,
month: 3,
year: 2026,
});
expect(inMonth.data.map((o) => o.id)).toContain(res.id);
const otherMonth = await listOrders({
...baseListParams,
month: 4,
year: 2026,
});
expect(otherMonth.data.map((o) => o.id)).not.toContain(res.id);
});
});

View File

@@ -48,6 +48,8 @@ export const issuedOrderListOptions = (filters: {
page?: number;
perPage?: number;
status?: string;
month?: number;
year?: number;
}) =>
queryOptions({
queryKey: ["issued-orders", "list", filters],
@@ -59,6 +61,8 @@ export const issuedOrderListOptions = (filters: {
if (filters.page) params.set("page", String(filters.page));
if (filters.perPage) params.set("per_page", String(filters.perPage));
if (filters.status) params.set("status", filters.status);
if (filters.month) params.set("month", String(filters.month));
if (filters.year) params.set("year", String(filters.year));
const qs = params.toString();
return paginatedJsonQuery<IssuedOrder>(
`/api/admin/issued-orders${qs ? `?${qs}` : ""}`,

View File

@@ -61,6 +61,8 @@ export const orderListOptions = (filters: {
order?: string;
page?: number;
perPage?: number;
month?: number;
year?: number;
}) =>
queryOptions({
queryKey: ["orders", "list", filters],
@@ -71,6 +73,8 @@ export const orderListOptions = (filters: {
if (filters.order) params.set("order", filters.order);
if (filters.page) params.set("page", String(filters.page));
if (filters.perPage) params.set("per_page", String(filters.perPage));
if (filters.month) params.set("month", String(filters.month));
if (filters.year) params.set("year", String(filters.year));
const qs = params.toString();
return paginatedJsonQuery(`/api/admin/orders${qs ? `?${qs}` : ""}`);
},

View File

@@ -34,6 +34,8 @@ export default async function issuedOrdersRoutes(fastify: FastifyInstance) {
search,
status: query.status ? String(query.status) : undefined,
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
month: query.month ? Number(query.month) : undefined,
year: query.year ? Number(query.year) : undefined,
});
return paginated(
reply,

View File

@@ -45,7 +45,7 @@ export default async function ordersRoutes(
{ preHandler: requirePermission("orders.view") },
async (request, reply) => {
const query = request.query as Record<string, unknown>;
const { page, limit, skip, sort, order } = parsePagination(query);
const { page, limit, skip, sort, order, search } = parsePagination(query);
const result = await listOrders({
page,
@@ -53,8 +53,11 @@ export default async function ordersRoutes(
skip,
sort,
order,
search,
status: query.status ? String(query.status) : undefined,
customer_id: query.customer_id ? Number(query.customer_id) : undefined,
month: query.month ? Number(query.month) : undefined,
year: query.year ? Number(query.year) : undefined,
});
return paginated(

View File

@@ -45,6 +45,8 @@ interface ListIssuedOrdersParams {
search?: string;
status?: string;
customer_id?: number;
month?: number;
year?: number;
}
const VALID_TRANSITIONS: Record<string, string[]> = {
@@ -92,8 +94,18 @@ export function computeIssuedOrderTotals(
}
export async function listIssuedOrders(params: ListIssuedOrdersParams) {
const { page, limit, skip, sort, order, search, status, customer_id } =
params;
const {
page,
limit,
skip,
sort,
order,
search,
status,
customer_id,
month,
year,
} = params;
const sortField = ALLOWED_SORT_FIELDS.includes(sort) ? sort : "id";
const where: Record<string, unknown> = {};
@@ -106,6 +118,11 @@ export async function listIssuedOrders(params: ListIssuedOrdersParams) {
{ customers: { company_id: { contains: search } } },
];
}
if (month && year) {
const from = new Date(year, month - 1, 1);
const to = new Date(year, month, 1);
where.order_date = { gte: from, lt: to };
}
// Normalize the direction so an unexpected value can't reach Prisma at runtime.
const dir: "asc" | "desc" = order === "asc" ? "asc" : "desc";

View File

@@ -108,10 +108,13 @@ interface ListOrdersParams {
order: "asc" | "desc";
status?: string;
customer_id?: number;
search?: string;
month?: number;
year?: number;
}
export async function listOrders(params: ListOrdersParams) {
const { page, limit, skip, order } = params;
const { page, limit, skip, order, search, month, year } = params;
const sortField = ORDER_ALLOWED_SORT_FIELDS.includes(params.sort)
? params.sort
: "id";
@@ -119,13 +122,26 @@ export async function listOrders(params: ListOrdersParams) {
const where: Record<string, unknown> = {};
if (params.status) where.status = params.status;
if (params.customer_id) where.customer_id = params.customer_id;
if (search) {
where.OR = [
{ order_number: { contains: search } },
{ customer_order_number: { contains: search } },
{ customers: { name: { contains: search } } },
];
}
if (month && year) {
const from = new Date(year, month - 1, 1);
const to = new Date(year, month, 1);
where.created_at = { gte: from, lt: to };
}
const [orders, total] = await Promise.all([
prisma.orders.findMany({
where,
skip,
take: limit,
orderBy: { [sortField]: order },
// Tiebreak on id so same-second created_at rows sort deterministically.
orderBy: [{ [sortField]: order }, { id: order }],
include: {
customers: { select: { id: true, name: true } },
order_items: { orderBy: { position: "asc" } },