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 }); } }