diff --git a/docs/api/routes-all.md b/docs/api/routes-all.md index f5bee7f..6991a19 100644 --- a/docs/api/routes-all.md +++ b/docs/api/routes-all.md @@ -589,7 +589,7 @@ _none_ ### Section: /order #### List orders — `GET /order` -- Params: `query: offset?: int, limit?: int` +- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Returns: `200 { orders: T2[], total: int, offset: int, limit: int }` #### Create order — `POST /order` - Params: `body: { number: str, date: str, lines: T30[], entityId?: int|null }` diff --git a/docs/api/routes.md b/docs/api/routes.md index c705358..ceea607 100644 --- a/docs/api/routes.md +++ b/docs/api/routes.md @@ -490,7 +490,7 @@ _none_ ### Section: /order #### List orders — `GET /order` -- Params: `query: offset?: int, limit?: int` +- Params: `query: offset?: int, limit?: int, search?: str, onlyUnpaid?: bool, onlyUndelivered?: bool, seekMonths?: int` - Returns: `200 { orders: T2[], total: int, offset: int, limit: int }` #### Create order — `POST /order` - Params: `body: { number: str, date: str, lines: T27[], entityId?: int|null }` diff --git a/docs/api/routes/order/list.md b/docs/api/routes/order/list.md index 264856a..807464d 100644 --- a/docs/api/routes/order/list.md +++ b/docs/api/routes/order/list.md @@ -15,6 +15,10 @@ Results are paged, include line items, shipping, payment, and provide total unpa |-------|----------|------|-------------| | `offset` | no | integer | Number of orders to skip | | `limit` | no | integer | Number of orders to return | +| `search` | no | string | Free-text search matching order number, external number, or customer name | +| `onlyUnpaid` | no | boolean | When true, returns only unpaid orders (including overpaid orders) | +| `onlyUndelivered` | no | boolean | When true, returns only undelivered orders | +| `seekMonths` | no | integer | Negative integer for cutoff months ago (e.g. -1 for orders older than 1 month) | ## Response `200` @@ -135,7 +139,7 @@ Order list | Status | Description | |--------|-------------| -| 400 | Invalid offset, invalid limit, or no company selected — `{"error":"…"}` | +| 400 | Invalid query parameters, invalid offset, invalid limit, or no company selected — `{"error":"…"}` | | 401 | Unauthorized — `{"error":"…"}` | | 403 | Not authorized — `{"error":"…"}` | | 404 | Company not found — `{"error":"…"}` | diff --git a/src/api/routes/order.js b/src/api/routes/order.js index 90814df..fde6cd5 100644 --- a/src/api/routes/order.js +++ b/src/api/routes/order.js @@ -22,6 +22,7 @@ function orderErrorStatus(error) { if (error.message === 'Invalid lines') return 400; if (error.message === 'Invalid offset') return 400; if (error.message === 'Invalid limit') return 400; + if (error.message === 'Invalid seekMonths') return 400; if (error.message?.includes('parent article') || error.message?.includes('A parent article')) return 400; return 500; } @@ -58,10 +59,32 @@ export function register(app, basePath = '') { * name: limit * schema: * type: integer - * default: 20 + * default: 50 * minimum: 1 * maximum: 100 * description: Number of orders to return + * - in: query + * name: search + * schema: + * type: string + * description: Free-text search matching order number, external number, or customer name + * - in: query + * name: onlyUnpaid + * schema: + * type: boolean + * default: false + * description: When true, returns only unpaid orders (including overpaid orders) + * - in: query + * name: onlyUndelivered + * schema: + * type: boolean + * default: false + * description: When true, returns only undelivered orders + * - in: query + * name: seekMonths + * schema: + * type: integer + * description: Negative integer for cutoff months ago (e.g. -1 for orders older than 1 month) * x-markdown: order/list.md * responses: * 200: @@ -78,7 +101,7 @@ export function register(app, basePath = '') { * $ref: '#/components/schemas/Order' * total: * type: integer - * description: Total number of unpaged orders + * description: Total number of filtered orders * offset: * type: integer * description: Number of orders skipped @@ -86,7 +109,7 @@ export function register(app, basePath = '') { * type: integer * description: Number of orders returned * 400: - * description: Invalid offset, invalid limit, or no company selected + * description: Invalid query parameters, invalid offset, invalid limit, or no company selected * content: * application/json: * schema: @@ -200,7 +223,7 @@ export function register(app, basePath = '') { const rawLimit = req.query.limit; let offset = 0; - let limit = 20; + let limit = 50; if (rawOffset !== undefined) { offset = parseInt(rawOffset, 10); @@ -216,8 +239,31 @@ export function register(app, basePath = '') { } } + const search = typeof req.query.search === 'string' && req.query.search.trim() !== '' + ? req.query.search.trim() + : undefined; + + const onlyUnpaid = req.query.onlyUnpaid === 'true'; + const onlyUndelivered = req.query.onlyUndelivered === 'true'; + + let seekMonths = undefined; + if (req.query.seekMonths !== undefined && req.query.seekMonths !== '') { + const parsedSeek = parseInt(req.query.seekMonths, 10); + if (!Number.isInteger(parsedSeek)) { + return res.status(400).json({ error: 'seekMonths must be an integer' }); + } + seekMonths = parsedSeek; + } + try { - const result = await db.list(req.user.id, { offset, limit }); + const result = await db.list(req.user.id, { + offset, + limit, + search, + onlyUnpaid: onlyUnpaid ? true : undefined, + onlyUndelivered: onlyUndelivered ? true : undefined, + seekMonths, + }); res.status(200).json(result); } catch (error) { const status = orderErrorStatus(error); diff --git a/src/db/order.js b/src/db/order.js index 12db865..87a2cff 100644 --- a/src/db/order.js +++ b/src/db/order.js @@ -62,14 +62,15 @@ async function enrichSingleOrder(store, googleAuthId, order) { const orderedQuantity = calculateOrderedQuantity(orderLines); const shippedQuantity = calculateShippedQuantity(shipmentsList); + const allShippings = shippingsList.length > 0 ? shippingsList : shipmentsList.flatMap((s) => s.shippings || []); const isPaid = orderTotal > 0 && Math.abs(paidTotal - orderTotal) < 0.01; const isShipped = orderedQuantity > 0 && Math.abs(shippedQuantity - orderedQuantity) < 0.001; - const isDelivered = isShipped || shippingsList.some((s) => s.state === 'shipped' || s.state === 'delivered'); + const isDelivered = isShipped || allShippings.some((s) => s.state === 'shipped' || s.state === 'delivered'); return { ...order, shipping: shippingsList[0] ?? (shipmentsList[0]?.shippings?.[0] ?? null), - shippings: shippingsList.length > 0 ? shippingsList : shipmentsList.flatMap((s) => s.shippings || []), + shippings: allShippings, payment: paymentsList[0] ?? null, payments: paymentsList, shipments: shipmentsList,