feat: add admin inventory Excel export
Quality / test (push) Canceled after 0s

This commit is contained in:
2026-09-10 01:11:07 -04:00
parent d6b4dbec5f
commit 24f51212e5
8 changed files with 91 additions and 0 deletions
@@ -37,4 +37,15 @@ export async function registerAdminExportRoutes(app, options) {
.header('content-disposition', 'attachment; filename="inventory.csv"')
.send(data);
});
app.get('/exports/inventory.xls', async (request, reply) => {
const data = await adminExportService.inventoryExcel({
sellerId: request.admin.sellerId,
});
return reply
.type('application/vnd.ms-excel; charset=utf-8')
.header('content-disposition', 'attachment; filename="inventory.xls"')
.send(data);
});
}
@@ -19,9 +19,34 @@ export function createAdminExportService({ inventoryItemService }) {
...rows.map((row) => columns.map((column) => escapeCsvValue(row[column])).join(',')),
].join('\r\n');
},
async inventoryExcel({ sellerId }) {
const rows = await inventoryRows(sellerId);
const columns = ['id', 'productId', 'conditionId', 'status', 'notes', 'createdAt', 'updatedAt'];
const header = columns.map((column) => excelCell(column, true)).join('');
const body = rows.map((row) => (
`<Row>${columns.map((column) => excelCell(row[column])).join('')}</Row>`
)).join('');
return `<?xml version="1.0"?>\n<?mso-application progid="Excel.Sheet"?>\n<Workbook xmlns="urn:schemas-microsoft-com:office:spreadsheet" xmlns:ss="urn:schemas-microsoft-com:office:spreadsheet"><Styles><Style ss:ID="Header"><Font ss:Bold="1" /></Style></Styles><Worksheet ss:Name="Inventory"><Table><Row>${header}</Row>${body}</Table></Worksheet></Workbook>`;
},
};
}
function excelCell(value, header = false) {
const style = header ? ' ss:StyleID="Header"' : '';
return `<Cell${style}><Data ss:Type="String">${escapeXml(value)}</Data></Cell>`;
}
function escapeXml(value) {
return String(value ?? '')
.replaceAll('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&apos;');
}
function escapeCsvValue(value) {
const text = value == null ? '' : String(value);
return `"${text.replaceAll('"', '""')}"`;
@@ -52,3 +52,18 @@ test('exports inventory as escaped CSV rows', async () => {
'"item-1","product-1","condition-1","ACTIVE","Front, ""clean""\nback","",""',
].join('\r\n'));
});
test('exports inventory as an escaped Excel XML workbook', async () => {
const service = createAdminExportService({
inventoryItemService: {
async listBySellerId() {
return [{ id: 'item-1', productId: 'product-1', conditionId: 'condition-1', status: 'ACTIVE', notes: 'A <note>' }];
},
},
});
const workbook = await service.inventoryExcel({ sellerId: 'seller-1' });
assert.match(workbook, /Excel\.Sheet/);
assert.match(workbook, /A &lt;note&gt;/);
assert.match(workbook, /<Cell ss:StyleID="Header"><Data ss:Type="String">id<\/Data><\/Cell>/);
});
@@ -80,3 +80,23 @@ test('downloads seller-scoped inventory CSV for an authenticated admin', async (
assert.equal(response.headers['content-disposition'], 'attachment; filename="inventory.csv"');
assert.equal(response.body, 'id,status\r\n"item-1","ACTIVE"');
});
test('downloads seller-scoped inventory Excel XML for an authenticated admin', async (testContext) => {
const app = buildApp({
authenticateAdmin: async () => ({ sellerId: 'seller-1' }),
adminExportService: {
async inventoryExcel({ sellerId }) {
assert.equal(sellerId, 'seller-1');
return '<?xml version="1.0"?><Workbook />';
},
},
});
testContext.after(() => app.close());
const response = await app.inject({ method: 'GET', url: '/api/v1/admin/exports/inventory.xls' });
assert.equal(response.statusCode, 200);
assert.equal(response.headers['content-type'], 'application/vnd.ms-excel; charset=utf-8');
assert.equal(response.headers['content-disposition'], 'attachment; filename="inventory.xls"');
assert.equal(response.body, '<?xml version="1.0"?><Workbook />');
});
@@ -47,6 +47,17 @@ export const InventoryCsvExport = {
},
};
export const InventoryExcelExport = {
parameters: {
docs: {
description: {
story:
"The admin workspace exposes a seller-scoped Excel-compatible inventory download.",
},
},
},
};
export const BulkTagSelection = {
parameters: {
docs: {
+7
View File
@@ -508,6 +508,13 @@ onMounted(load);
>
Download inventory CSV
</a>
<a
class="button button--secondary"
href="/api/v1/admin/exports/inventory.xls"
download="inventory.xls"
>
Download inventory Excel
</a>
</p>
<p v-if="isLoading" class="state state--loading" role="status">
Loading inventory...