Skip to content

feat(work-orders): add bulk CSV export endpoint for selected work orders - #189

Merged
tekwani merged 5 commits into
tetherto:developfrom
rob-aslanian:feat/add-bulk-export-wo
Aug 19, 2026
Merged

feat(work-orders): add bulk CSV export endpoint for selected work orders#189
tekwani merged 5 commits into
tetherto:developfrom
rob-aslanian:feat/add-bulk-export-wo

Conversation

@rob-aslanian

Copy link
Copy Markdown
Contributor

Summary

  • Adds GET /auth/work-orders/export/bulk?ids=..., returning one combined
    CSV for many work orders in a single response — the general-purpose
    sibling of the existing MicroBT-only export/rma bulk endpoint.
  • Work order types (register/repair/move) carry different info shapes, so
    work.order.export.js is refactored to share row-building between the
    existing single-WO renderWorkOrderCsv and the new
    renderWorkOrdersBulkCsv, which unions headers across all selected WOs
    and blank-pads rows missing a given column.
  • Backs the moria-app-ui fix for "Export Selected" downloading only 10 of
    N selected work orders (browsers block bursts of automatic downloads;
    one combined file sidesteps that entirely).

@arif-dewi arif-dewi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The refactor into _rowsForWorkOrder + _renderCsvRows is a clean way to share the row building, and the union-header comment explains the why nicely. A few thoughts, mostly about duplication with the RMA sibling and the edges.

// Bulk sibling of exportWorkOrder: N ids, one combined CSV, so a large list-page
// selection downloads as a single file instead of one browser download per WO.
async function exportWorkOrdersBulk (ctx, req, rep) {
const ids = req.query.ids.split(',').map(s => s.trim()).filter(Boolean)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this block — ids parsing, the $or query, flattenRpcResults — is the same as exportWorkOrdersRma just above. Could pull it into a small helper like _loadWorkOrdersByIdsOrCodes(ctx, req.query.ids) and let each handler only filter and render.

}

module.exports = { create, createBatch, list, byId, update, close, cancel, reopen, assign, audit, log, export: exportRoute, exportRma }
const exportBulk = {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

exportBulk is identical to exportRma. Could share one definition (a named idsQuery used by both, or simply const exportBulk = exportRma) so the two cannot drift apart later.

required: ['ids'],
additionalProperties: false,
properties: {
ids: { type: 'string', minLength: 1, maxLength: 4000 }

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

work order ids are randomUUID(), so 4000 chars caps this at roughly 108 ids. Selection accumulates across pages on the list screen, so a large selection could hit the cap and fail with a 400 that the UI shows as a generic "Could not export the selected work orders". Worth checking that ceiling is comfortable, or giving that case a clearer message.


// Bulk sibling of renderWorkOrderCsv: many work orders, one CSV, so a large
// selection downloads as a single file instead of one browser download per WO.
function renderWorkOrdersBulkCsv (wos) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

when none of the ids match, wos is empty and _renderCsvRows([]) returns just a blank line — the file has no header row at all. renderRmaCsv always writes its header row. A 404, or at least a header-only CSV, would give the user some signal instead of an empty download.

t.is(rep._headers['content-type'], 'text/csv; charset=utf-8')
t.ok(rep._headers['content-disposition'].includes('work-orders.csv'))
const lines = rep._body.trim().split('\r\n')
t.is(lines.length, 3, 'header + one row per WO')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

small thing: _rowsForWorkOrder emits one row per parts move, so "one row per WO" only holds here because these fixtures have 0 and 1 moves. A case with a WO carrying 2+ partsMoves would pin the real behaviour — several rows sharing the same code.

@tekwani
tekwani merged commit 8617080 into tetherto:develop Aug 19, 2026
6 checks passed
@rob-aslanian
rob-aslanian deleted the feat/add-bulk-export-wo branch August 20, 2026 13:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants