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:
@@ -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",
|
||||
|
||||
114
src/__tests__/orders-list.test.ts
Normal file
114
src/__tests__/orders-list.test.ts
Normal 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);
|
||||
});
|
||||
});
|
||||
@@ -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}` : ""}`,
|
||||
|
||||
@@ -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}` : ""}`);
|
||||
},
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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" } },
|
||||
|
||||
Reference in New Issue
Block a user