349 lines
12 KiB
JavaScript
349 lines
12 KiB
JavaScript
import pool, { run } from '../db/index.js';
|
|
import { sendSupplierVouchers, buildSupplierVoucherPayload } from '../services/supplierVoucher.js';
|
|
import path from 'path';
|
|
import fs from 'fs';
|
|
import { fileURLToPath } from 'url';
|
|
import { generateCuttingList, formatCuttingListForExport } from '../services/cuttingEngine.js';
|
|
import { generatePackingList } from '../services/packingEngine.js';
|
|
import { generateAllPlans } from '../services/planGenerator.js';
|
|
import { generateOrderDossierPdf } from '../services/exportPdf.js';
|
|
import { validateConfig } from '../services/configValidation.js';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const planExportsRoot = path.join(__dirname, '..', 'data', 'plan-exports');
|
|
|
|
function ensurePlanExportDir(orderId) {
|
|
const dir = path.join(planExportsRoot, `order-${orderId}`);
|
|
fs.mkdirSync(dir, { recursive: true });
|
|
return dir;
|
|
}
|
|
|
|
async function saveOrderPlanExportRecord(orderId, exportType, content, mimeType = 'text/plain', fileExtension = 'txt') {
|
|
const safeExt = (fileExtension || 'txt').toLowerCase().replace(/[^a-z0-9]/g, '') || 'txt';
|
|
const safeType = String(exportType || '').toLowerCase().replace(/[^a-z0-9_-]/g, '');
|
|
if (!safeType) throw new Error('exportType invalide.');
|
|
if (!(typeof content === 'string' || Buffer.isBuffer(content))) throw new Error('content requis.');
|
|
if ((typeof content === 'string' && content.length === 0) || (Buffer.isBuffer(content) && content.length === 0)) {
|
|
throw new Error('content requis.');
|
|
}
|
|
|
|
const exportDir = ensurePlanExportDir(orderId);
|
|
const stamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
const fileName = `${stamp}-${safeType}.${safeExt}`;
|
|
const absolutePath = path.join(exportDir, fileName);
|
|
const relativePath = path.join('plan-exports', `order-${orderId}`, fileName).replace(/\\/g, '/');
|
|
if (Buffer.isBuffer(content)) {
|
|
fs.writeFileSync(absolutePath, content);
|
|
} else {
|
|
fs.writeFileSync(absolutePath, content, 'utf8');
|
|
}
|
|
|
|
await run(
|
|
`INSERT INTO plan_exports (order_id, export_type, file_name, file_path, mime_type)
|
|
VALUES ($1, $2, $3, $4, $5)`,
|
|
[orderId, safeType, fileName, relativePath, mimeType || 'text/plain']
|
|
);
|
|
|
|
const { rows } = await pool.query(
|
|
`SELECT id, order_id, export_type, file_name, mime_type, created_at
|
|
FROM plan_exports
|
|
WHERE order_id = $1
|
|
ORDER BY id DESC
|
|
LIMIT 1`,
|
|
[orderId]
|
|
);
|
|
return rows[0];
|
|
}
|
|
|
|
export async function getOrders(req, res) {
|
|
try {
|
|
const { rows } = await pool.query(
|
|
'SELECT id, config, total_chf, email, name, phone, notes, created_at FROM orders ORDER BY created_at DESC'
|
|
);
|
|
res.json(rows);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function postOrder(req, res) {
|
|
try {
|
|
const { config, totalChf, email, name, phone, notes } = req.body || {};
|
|
const settingsRows = await pool.query('SELECT key, value FROM settings');
|
|
const settings = Object.fromEntries((settingsRows.rows || []).map((r) => [r.key, r.value]));
|
|
const validation = validateConfig(config || {}, settings);
|
|
if (validation.errors.length > 0) {
|
|
return res.status(400).json({ error: 'Configuration invalide', details: validation.errors });
|
|
}
|
|
await run(
|
|
`INSERT INTO orders (config, total_chf, email, name, phone, notes)
|
|
VALUES ($1, $2, $3, $4, $5, $6)`,
|
|
[JSON.stringify(config || {}), totalChf ?? null, email ?? null, name ?? null, phone ?? null, notes ?? null]
|
|
);
|
|
const { rows } = await pool.query('SELECT id, created_at FROM orders ORDER BY id DESC LIMIT 1');
|
|
const order = rows[0];
|
|
|
|
// Fire-and-forget: send supplier vouchers without blocking the HTTP response
|
|
sendSupplierVouchers(order.id, config || {}, name ?? '', order.created_at).catch(err => {
|
|
console.error('[supplier-voucher] Email error:', err.message);
|
|
});
|
|
|
|
res.status(201).json(order);
|
|
} catch (err) {
|
|
res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function getOrderSupplierVouchers(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { rows } = await pool.query(
|
|
'SELECT id, config, name, created_at FROM orders WHERE id = $1 LIMIT 1',
|
|
[id]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ error: 'Commande introuvable.' });
|
|
}
|
|
|
|
const order = rows[0];
|
|
let config = {};
|
|
try {
|
|
config = typeof order.config === 'string' ? JSON.parse(order.config) : (order.config || {});
|
|
} catch {
|
|
config = {};
|
|
}
|
|
|
|
const suppliers = await buildSupplierVoucherPayload(config);
|
|
return res.json({
|
|
orderId: order.id,
|
|
customerName: order.name || null,
|
|
createdAt: order.created_at,
|
|
suppliers
|
|
});
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function listOrderPlanExports(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { rows: orderRows } = await pool.query('SELECT id FROM orders WHERE id = $1 LIMIT 1', [id]);
|
|
if (orderRows.length === 0) {
|
|
return res.status(404).json({ error: 'Commande introuvable.' });
|
|
}
|
|
|
|
const { rows } = await pool.query(
|
|
`SELECT id, order_id, export_type, file_name, mime_type, created_at
|
|
FROM plan_exports
|
|
WHERE order_id = $1
|
|
ORDER BY created_at DESC, id DESC`,
|
|
[id]
|
|
);
|
|
return res.json(rows);
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function createOrderPlanExport(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { exportType, content, mimeType, fileExtension } = req.body || {};
|
|
|
|
if (!exportType || typeof exportType !== 'string') {
|
|
return res.status(400).json({ error: 'exportType requis.' });
|
|
}
|
|
if (typeof content !== 'string' || content.length === 0) {
|
|
return res.status(400).json({ error: 'content requis.' });
|
|
}
|
|
|
|
const { rows: orderRows } = await pool.query('SELECT id FROM orders WHERE id = $1 LIMIT 1', [id]);
|
|
if (orderRows.length === 0) {
|
|
return res.status(404).json({ error: 'Commande introuvable.' });
|
|
}
|
|
|
|
const created = await saveOrderPlanExportRecord(id, exportType, content, mimeType, fileExtension);
|
|
return res.status(201).json(created);
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function generateOrderPlanExports(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { rows } = await pool.query(
|
|
'SELECT id, config FROM orders WHERE id = $1 LIMIT 1',
|
|
[id]
|
|
);
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ error: 'Commande introuvable.' });
|
|
}
|
|
|
|
let config = {};
|
|
try {
|
|
config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : (rows[0].config || {});
|
|
} catch {
|
|
config = {};
|
|
}
|
|
if (!config || typeof config !== 'object' || !config.widthMm || !config.heightMm || !config.depthMm) {
|
|
return res.status(400).json({ error: 'Configuration commande invalide pour générer les plans.' });
|
|
}
|
|
{
|
|
const settingsRows = await pool.query('SELECT key, value FROM settings');
|
|
const settings = Object.fromEntries((settingsRows.rows || []).map((r) => [r.key, r.value]));
|
|
const validation = validateConfig(config, settings);
|
|
if (validation.errors.length > 0) {
|
|
return res.status(400).json({ error: 'Configuration invalide', details: validation.errors });
|
|
}
|
|
}
|
|
|
|
const modules = Array.isArray(config.modules) ? config.modules : [];
|
|
const colors = (config.colors && typeof config.colors === 'object') ? config.colors : {};
|
|
const cuttingList = generateCuttingList(config, colors, modules);
|
|
const packing = generatePackingList(cuttingList);
|
|
const textContent = formatCuttingListForExport(cuttingList);
|
|
const pdfBuffer = await generateOrderDossierPdf({
|
|
orderId: id,
|
|
config: {
|
|
widthMm: config.widthMm,
|
|
heightMm: config.heightMm,
|
|
depthMm: config.depthMm,
|
|
installationType: config.installationType,
|
|
doorType: config.doorType,
|
|
moduleCount: modules.length
|
|
},
|
|
cuttingList,
|
|
packing
|
|
});
|
|
|
|
const [textExport, pdfExport] = await Promise.all([
|
|
saveOrderPlanExportRecord(id, 'cutting_list_text', textContent, 'text/plain; charset=utf-8', 'txt'),
|
|
saveOrderPlanExportRecord(id, 'production_dossier_pdf', pdfBuffer, 'application/pdf', 'pdf')
|
|
]);
|
|
|
|
return res.status(201).json({ created: [textExport, pdfExport] });
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function getOrderProductionDetails(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { rows } = await pool.query(
|
|
'SELECT id, config, total_chf, name, created_at FROM orders WHERE id = $1 LIMIT 1',
|
|
[id]
|
|
);
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ error: 'Commande introuvable.' });
|
|
}
|
|
|
|
let config = {};
|
|
try {
|
|
config = typeof rows[0].config === 'string' ? JSON.parse(rows[0].config) : (rows[0].config || {});
|
|
} catch {
|
|
config = {};
|
|
}
|
|
if (!config || typeof config !== 'object' || !config.widthMm || !config.heightMm || !config.depthMm) {
|
|
return res.status(400).json({ error: 'Configuration commande invalide pour générer le dossier.' });
|
|
}
|
|
{
|
|
const settingsRows = await pool.query('SELECT key, value FROM settings');
|
|
const settings = Object.fromEntries((settingsRows.rows || []).map((r) => [r.key, r.value]));
|
|
const validation = validateConfig(config, settings);
|
|
if (validation.errors.length > 0) {
|
|
return res.status(400).json({ error: 'Configuration invalide', details: validation.errors });
|
|
}
|
|
}
|
|
|
|
const modules = Array.isArray(config.modules) ? config.modules : [];
|
|
const colors = (config.colors && typeof config.colors === 'object') ? config.colors : {};
|
|
const plans = generateAllPlans(config, modules, colors);
|
|
const cuttingList = generateCuttingList(config, colors, modules);
|
|
const packing = generatePackingList(cuttingList);
|
|
|
|
return res.json({
|
|
orderId: rows[0].id,
|
|
customerName: rows[0].name || null,
|
|
totalChf: rows[0].total_chf,
|
|
createdAt: rows[0].created_at,
|
|
config: {
|
|
widthMm: config.widthMm,
|
|
heightMm: config.heightMm,
|
|
depthMm: config.depthMm,
|
|
plinthHeightMm: config.plinthHeightMm,
|
|
installationType: config.installationType,
|
|
doorType: config.doorType,
|
|
slidingPanelsCount: config.slidingPanelsCount,
|
|
moduleCount: modules.length
|
|
},
|
|
plans,
|
|
cuttingList,
|
|
packing
|
|
});
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function downloadOrderPlanExport(req, res) {
|
|
try {
|
|
const { id, exportId } = req.params;
|
|
const disposition = String(req.query.disposition || 'attachment').toLowerCase();
|
|
const headerDisposition = disposition === 'inline' ? 'inline' : 'attachment';
|
|
const { rows } = await pool.query(
|
|
`SELECT id, order_id, file_name, file_path, mime_type
|
|
FROM plan_exports
|
|
WHERE id = $1 AND order_id = $2
|
|
LIMIT 1`,
|
|
[exportId, id]
|
|
);
|
|
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ error: 'Export introuvable.' });
|
|
}
|
|
|
|
const record = rows[0];
|
|
const absolutePath = path.join(__dirname, '..', 'data', record.file_path);
|
|
if (!fs.existsSync(absolutePath)) {
|
|
return res.status(404).json({ error: 'Fichier export introuvable.' });
|
|
}
|
|
|
|
res.setHeader('Content-Type', record.mime_type || 'application/octet-stream');
|
|
res.setHeader('Content-Disposition', `${headerDisposition}; filename="${record.file_name}"`);
|
|
return res.sendFile(absolutePath);
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|
|
|
|
export async function deleteOrderPlanExport(req, res) {
|
|
try {
|
|
const { id, exportId } = req.params;
|
|
const { rows } = await pool.query(
|
|
`SELECT id, order_id, file_path
|
|
FROM plan_exports
|
|
WHERE id = $1 AND order_id = $2
|
|
LIMIT 1`,
|
|
[exportId, id]
|
|
);
|
|
if (rows.length === 0) {
|
|
return res.status(404).json({ error: 'Export introuvable.' });
|
|
}
|
|
|
|
const record = rows[0];
|
|
const absolutePath = path.join(__dirname, '..', 'data', record.file_path);
|
|
if (fs.existsSync(absolutePath)) {
|
|
fs.unlinkSync(absolutePath);
|
|
}
|
|
|
|
await run('DELETE FROM plan_exports WHERE id = $1 AND order_id = $2', [exportId, id]);
|
|
return res.status(204).end();
|
|
} catch (err) {
|
|
return res.status(500).json({ error: err.message });
|
|
}
|
|
}
|