This commit is contained in:
seb
2026-10-14 15:32:54 +02:00
parent 90b782e500
commit 04e10cffdb

View File

@@ -1,12 +1,14 @@
// MARK: - Imports
// #region Imports
import JSZip from 'jszip';
import { getStore } from '../db/store.js';
import { getStore, getProvider } from '../db/store.js';
import { createLogger } from '../util/logger.js';
// #endregion
const log = createLogger('export');
const UTF8_BOM = '\uFEFF';
// MARK: - CSV Helpers
// #region CSV Helpers
@@ -22,6 +24,21 @@ function formatCsvField(val) {
return `"${str.replace(/"/g, '""')}"`;
}
/**
* Formats a value as an Excel string formula (e.g. ="4270001588593") so that
* spreadsheet tools treat it strictly as text rather than converting to a number / scientific notation.
* Empty or null values return '""'.
*
* @param {any} val
* @returns {string}
*/
function formatCsvStringFormula(val) {
if (val == null) return '""';
const str = String(val).trim();
if (str === '') return '""';
return `="${str.replace(/"/g, '""')}"`;
}
/**
* Formats a number in German decimal format (e.g. 8.36 -> "8,36").
*
@@ -297,16 +314,17 @@ export async function executeCompanyExport({
zip.file(picRel, hashToBuffer.get(c.fullHash));
}
const catName = c.name || (c.parentId == null ? 'Haupt' : `Kategorie ${c.id}`);
const row = [
formatCsvField(c.id),
formatCsvField(c.parentId == null ? '0' : String(c.parentId)),
formatCsvField(c.sort ?? 0),
formatCsvField(c.name ?? ''),
formatCsvField(catName),
formatCsvField(picRel),
];
categoryCsvRows.push(row.join(';'));
}
zip.file('categories.csv', categoryCsvRows.join('\r\n'));
zip.file('categories.csv', UTF8_BOM + categoryCsvRows.join('\r\n'));
// 6. Place brand images in ZIP
for (const b of brands) {
@@ -399,13 +417,13 @@ export async function executeCompanyExport({
const row = [
formatCsvField(a.id),
formatCsvField(a.sku),
formatCsvStringFormula(a.sku),
formatCsvField(a.name ?? ''),
formatCsvField(formatGermanFloat(netPrice)),
formatCsvField(formatGermanFloat(taxRate, '19')),
formatCsvField('1'), // active
formatCsvField(a.stock ?? 0),
formatCsvField(ean),
formatCsvStringFormula(ean),
formatCsvField(formatGermanFloat(a.weight ?? 0)),
formatCsvField(a.track_stock ? '1' : '0'),
formatCsvField(a.createdAt || new Date().toISOString()),
@@ -422,9 +440,136 @@ export async function executeCompanyExport({
];
articleCsvRows.push(row.join(';'));
}
zip.file('articles.csv', articleCsvRows.join('\r\n'));
zip.file('articles.csv', UTF8_BOM + articleCsvRows.join('\r\n'));
// 8. Generate ZIP buffer
// 8. Orders / Manifests (if present)
let exportedReceiptsCount = 0;
try {
const orderListRes = await store.order.list(googleAuthId, { offset: 0, limit: 1 });
const totalOrders = orderListRes?.total ?? (Array.isArray(orderListRes) ? orderListRes.length : 0);
if (totalOrders > 0) {
onStatus({ message: `Exportiere ${totalOrders} Aufträge / Belege...` });
// Prefetch payment types and shipping types by manifest ID
const paymentMap = new Map();
const shippingMap = new Map();
try {
const provider = await getProvider();
const isSqlite = provider?.dialect === 'sqlite3';
const typeCol = isSqlite ? '"type"' : '[type]';
const [paymentsRes, shippingsRes] = await Promise.all([
provider.query(`
SELECT p.manifest_id AS manifestId, p.${typeCol} AS type
FROM payment p
INNER JOIN manifest m ON m.id = p.manifest_id
WHERE m.company_id = @companyId
ORDER BY p.id ASC
`, { companyId }),
provider.query(`
SELECT s.manifest_id AS manifestId, s.${typeCol} AS type
FROM shipping s
INNER JOIN manifest m ON m.id = s.manifest_id
WHERE m.company_id = @companyId
ORDER BY s.id ASC
`, { companyId }),
]);
for (const row of paymentsRes.rows || []) {
if (!paymentMap.has(row.manifestId)) {
paymentMap.set(row.manifestId, row.type);
}
}
for (const row of shippingsRes.rows || []) {
if (!shippingMap.has(row.manifestId)) {
shippingMap.set(row.manifestId, row.type);
}
}
} catch (err) {
log.warn('Could not prefetch payment/shipping types:', err.message);
}
const manifestCsvHeaders = [
'"id"',
'"nr"',
'"extnr"',
'"created_at"',
'"paymentType"',
'"shippingType"',
'"payed"',
'"shipped"',
];
const manifestItemCsvHeaders = [
'"id"',
'"manifestid"',
'"articleid"',
'"name"',
'"amount"',
'"amountShipped"',
'"net"',
'"taxrate"',
'"sort"',
];
const manifestCsvRows = [manifestCsvHeaders.join(';')];
const manifestItemCsvRows = [manifestItemCsvHeaders.join(';')];
let orderOffset = 0;
const orderLimit = 200;
let itemSeq = 0;
while (true) {
const batchRes = await store.order.list(googleAuthId, { offset: orderOffset, limit: orderLimit });
const batch = Array.isArray(batchRes) ? batchRes : (batchRes?.orders || []);
if (batch.length === 0) break;
for (const ord of batch) {
const payType = paymentMap.get(ord.id) || ord.payment?.type || ord.payments?.[0]?.type || 'Bar';
const shipType = shippingMap.get(ord.id) || ord.shipping?.type || ord.shippings?.[0]?.type || 'Selbstabholer';
manifestCsvRows.push([
formatCsvField(ord.id),
formatCsvField(ord.number),
formatCsvField(ord.externalNumber || ''),
formatCsvField(ord.createdAt || ord.date || new Date().toISOString()),
formatCsvField(payType),
formatCsvField(shipType),
formatCsvField(ord.isPaid || ord.paid ? '1' : '0'),
formatCsvField(ord.isShipped || ord.shipped ? '1' : '0'),
].join(';'));
for (const line of ord.lines || []) {
const artId = (line.sku && articleBySku.get(line.sku)?.id) || line.articleId || '';
manifestItemCsvRows.push([
formatCsvField(line.id || ++itemSeq),
formatCsvField(ord.id),
formatCsvField(artId),
formatCsvField(line.name || ''),
formatCsvField(line.amount ?? 1),
formatCsvField(ord.isShipped || ord.shipped ? (line.amount ?? 1) : 0),
formatCsvField(formatGermanFloat(line.net ?? 0)),
formatCsvField(formatGermanFloat(line.taxrate ?? 19)),
formatCsvField(line.sort ?? 0),
].join(';'));
}
}
orderOffset += batch.length;
if (batch.length < orderLimit || orderOffset >= totalOrders) {
break;
}
}
if (manifestCsvRows.length > 1) {
zip.file('manifests.csv', UTF8_BOM + manifestCsvRows.join('\r\n'));
zip.file('manifestitems.csv', UTF8_BOM + manifestItemCsvRows.join('\r\n'));
exportedReceiptsCount = manifestCsvRows.length - 1;
}
}
} catch (orderErr) {
log.warn('Could not export orders / manifests:', orderErr.message);
}
// 9. Generate ZIP buffer
onStatus({ message: 'Erstelle ZIP-Archiv...' });
onProgress({
stage: 'packaging',
@@ -448,6 +593,7 @@ export async function executeCompanyExport({
brands: brands.length,
articles: fullArticles.length,
images: hashToBuffer.size,
orders: exportedReceiptsCount,
zipSize: zipBuffer.length,
};