import dotenv from 'dotenv'; import express from 'express'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; import cors from 'cors'; import { dbReady, run, query } from './db/index.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); dotenv.config({ path: path.join(__dirname, '.env') }); import { getProducts } from './routes/products.js'; import { getBlockTypes } from './routes/blockTypes.js'; import { getFinishes } from './routes/finishes.js'; import { getServiceOptions } from './routes/serviceOptions.js'; import { getSettings, updateSettings } from './routes/settings.js'; import { ensureEngineeringRulesSeed, getEngineeringRules, createEngineeringRule, updateEngineeringRule, deleteEngineeringRule } from './routes/engineeringRules.js'; import { postPrice } from './routes/price.js'; import { getOrders, postOrder, getOrderSupplierVouchers, listOrderPlanExports, createOrderPlanExport, downloadOrderPlanExport, generateOrderPlanExports, getOrderProductionDetails, deleteOrderPlanExport } from './routes/orders.js'; import { getSuppliers, createSupplier, updateSupplier, deleteSupplier } from './routes/suppliers.js'; import { triggerFormexSync, streamFormexSync, triggerHaefeleSync, streamHaefeleSync, getLogs, getStatus, getFormexMaterials, getFormexMaterial, createFormexMaterial, updateFormexMaterial, deleteFormexMaterial, getFormexCollections, getFormexCategories, getFormexSuppliers, getHaefeleMaterials } from './routes/sync.js'; import { uploadMiddleware, analyzeRoom, saveRoomConfig, getRoomConfig, checkApiStatus } from './routes/room.js'; import { optimizeLayoutRoute, quickOptimizeRoute, suggestLayoutRoute } from './routes/layout.js'; import { getModules, getModule, getModuleCategories, getHardware, getHardwareTypes, getHandles, getHinges } from './routes/modules.js'; import { generatePlansRoute, generateCuttingListRoute, exportCuttingListText, exportPDFHtml, exportCncRoute, exportMprRoute, exportMprPieceRoute, generateHardwareListRoute } from './routes/plans.js'; import { designTipsRoute, recommendMaterialsRoute, renderRoute } from './routes/ai.js'; import { initScheduler } from './services/scheduler.js'; const clientDist = path.join(__dirname, '..', 'client', 'dist'); const hasClientBuild = fs.existsSync(clientDist); const app = express(); const PORT = process.env.PORT || 3001; app.use(cors({ origin: true })); app.use(express.json()); app.get('/api/products', getProducts); app.get('/api/block-types', getBlockTypes); app.get('/api/finishes', getFinishes); app.get('/api/service-options', getServiceOptions); app.get('/api/settings', getSettings); app.put('/api/settings', updateSettings); app.get('/api/engineering-rules', getEngineeringRules); app.post('/api/engineering-rules', createEngineeringRule); app.put('/api/engineering-rules/:id', updateEngineeringRule); app.delete('/api/engineering-rules/:id', deleteEngineeringRule); app.post('/api/price', postPrice); app.get('/api/orders', getOrders); app.post('/api/orders', postOrder); app.get('/api/orders/:id/supplier-vouchers', getOrderSupplierVouchers); app.get('/api/orders/:id/plan-exports', listOrderPlanExports); app.post('/api/orders/:id/plan-exports', createOrderPlanExport); app.post('/api/orders/:id/plan-exports/generate', generateOrderPlanExports); app.get('/api/orders/:id/plan-exports/:exportId/download', downloadOrderPlanExport); app.delete('/api/orders/:id/plan-exports/:exportId', deleteOrderPlanExport); app.get('/api/orders/:id/production-details', getOrderProductionDetails); // Supplier routes app.get('/api/suppliers', getSuppliers); app.post('/api/suppliers', createSupplier); app.put('/api/suppliers/:id', updateSupplier); app.delete('/api/suppliers/:id', deleteSupplier); // Sync routes (Formex crawler) app.post('/api/sync/formex', triggerFormexSync); app.get('/api/sync/formex/stream', streamFormexSync); app.post('/api/sync/haefele', triggerHaefeleSync); app.get('/api/sync/haefele/stream', streamHaefeleSync); app.get('/api/sync/logs', getLogs); app.get('/api/sync/status', getStatus); // Formex materials CRUD app.get('/api/formex/materials', getFormexMaterials); app.get('/api/formex/materials/:id', getFormexMaterial); app.post('/api/formex/materials', createFormexMaterial); app.put('/api/formex/materials/:id', updateFormexMaterial); app.delete('/api/formex/materials/:id', deleteFormexMaterial); app.get('/api/formex/collections', getFormexCollections); app.get('/api/formex/categories', getFormexCategories); app.get('/api/formex/suppliers', getFormexSuppliers); app.get('/api/haefele/materials', getHaefeleMaterials); // Room analysis routes (AI-powered) app.post('/api/room/analyze', uploadMiddleware, analyzeRoom); app.post('/api/room/save', saveRoomConfig); app.get('/api/room/config/:orderId', getRoomConfig); app.get('/api/room/status', checkApiStatus); // Layout optimization routes (AI-powered) app.post('/api/layout/optimize', optimizeLayoutRoute); app.post('/api/layout/quick-optimize', quickOptimizeRoute); app.get('/api/layout/suggest', suggestLayoutRoute); // Modules catalog routes app.get('/api/modules', getModules); app.get('/api/modules/categories', getModuleCategories); app.get('/api/modules/:id', getModule); // Hardware routes app.get('/api/hardware', getHardware); app.get('/api/hardware/types', getHardwareTypes); app.get('/api/handles', getHandles); app.get('/api/hinges', getHinges); // AI design assistant routes app.post('/api/ai/design-tips', designTipsRoute); app.post('/api/ai/recommend-materials', recommendMaterialsRoute); app.post('/api/ai/render', renderRoute); // Plans and cutting list routes app.post('/api/plans/generate', generatePlansRoute); app.post('/api/plans/cutting-list', generateCuttingListRoute); app.post('/api/plans/hardware-list', generateHardwareListRoute); app.post('/api/plans/export-text', exportCuttingListText); app.post('/api/plans/export-pdf', exportPDFHtml); app.post('/api/plans/export-cnc', exportCncRoute); app.post('/api/plans/export-mpr', exportMprRoute); app.post('/api/plans/export-mpr-piece', exportMprPieceRoute); if (hasClientBuild) { app.use(express.static(clientDist)); app.get('*', (req, res) => { res.sendFile(path.join(clientDist, 'index.html')); }); } else { app.get('/', (req, res) => { res.send(`
Depuis la racine du projet, exécutez :
npm run build
Puis rechargez cette page. Ou lancez le client en mode dev :
npm run client
et ouvrez http://localhost:5173.
`); }); } await dbReady; // Apply migration 007: suppliers table + supplier_id columns async function applyMigration007() { try { await run(`CREATE TABLE IF NOT EXISTS suppliers ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, name TEXT NOT NULL, email TEXT NOT NULL, type TEXT NOT NULL, active INTEGER DEFAULT 1, created_at TEXT DEFAULT (datetime('now')) )`); // Add supplier_id to formex_materials if missing const fmCols = (await query("PRAGMA table_info(formex_materials)")).rows; if (!fmCols.find(c => c.name === 'supplier_id')) { await run('ALTER TABLE formex_materials ADD COLUMN supplier_id INTEGER REFERENCES suppliers(id)'); } // Add supplier_id to blum_hardware if missing const bhCols = (await query("PRAGMA table_info(blum_hardware)")).rows; if (!bhCols.find(c => c.name === 'supplier_id')) { await run('ALTER TABLE blum_hardware ADD COLUMN supplier_id INTEGER REFERENCES suppliers(id)'); } // Add supplier_id to handle_variants if missing const hvCols = (await query("PRAGMA table_info(handle_variants)")).rows; if (!hvCols.find(c => c.name === 'supplier_id')) { await run('ALTER TABLE handle_variants ADD COLUMN supplier_id INTEGER REFERENCES suppliers(id)'); } // Add supplier_id to hinge_variants if missing const hingeCols = (await query("PRAGMA table_info(hinge_variants)")).rows; if (!hingeCols.find(c => c.name === 'supplier_id')) { await run('ALTER TABLE hinge_variants ADD COLUMN supplier_id INTEGER REFERENCES suppliers(id)'); } // Seed default suppliers await run(`INSERT OR IGNORE INTO suppliers (code, name, email, type) VALUES ('formex', 'Formex', 'commandes@formex.ch', 'panels'), ('blum', 'Blum Switzerland', 'commandes@blum.com', 'hardware'), ('haefele', 'Hafele', 'commandes@haefele.ch', 'materials')`); // Assign existing rows to their suppliers await run(`UPDATE formex_materials SET supplier_id = (SELECT id FROM suppliers WHERE code = 'formex') WHERE supplier_id IS NULL`); await run(`UPDATE blum_hardware SET supplier_id = (SELECT id FROM suppliers WHERE code = 'blum') WHERE supplier_id IS NULL`); await run(`UPDATE handle_variants SET supplier_id = (SELECT id FROM suppliers WHERE code = 'blum') WHERE supplier_id IS NULL`); await run(`UPDATE hinge_variants SET supplier_id = (SELECT id FROM suppliers WHERE code = 'blum') WHERE supplier_id IS NULL`); console.log('[migration-007] Suppliers table ready.'); } catch (err) { console.error('[migration-007] Error:', err.message); } } await applyMigration007(); // Apply migration 008: price_unit column on formex_materials async function applyMigration008() { try { const cols = (await query("PRAGMA table_info(formex_materials)")).rows; if (!cols.find(c => c.name === 'price_unit')) { await run('ALTER TABLE formex_materials ADD COLUMN price_unit TEXT'); console.log('[migration-008] Added price_unit column to formex_materials.'); } } catch (err) { console.error('[migration-008] Error:', err.message); } } await applyMigration008(); // Apply migration 009: persist generated plan exports async function applyMigration009() { try { await run(`CREATE TABLE IF NOT EXISTS plan_exports ( id INTEGER PRIMARY KEY AUTOINCREMENT, order_id INTEGER NOT NULL REFERENCES orders(id) ON DELETE CASCADE, export_type TEXT NOT NULL, file_name TEXT NOT NULL, file_path TEXT NOT NULL, mime_type TEXT, created_at TEXT DEFAULT (datetime('now')) )`); await run('CREATE INDEX IF NOT EXISTS idx_plan_exports_order ON plan_exports(order_id)'); console.log('[migration-009] Plan exports table ready.'); } catch (err) { console.error('[migration-009] Error:', err.message); } } await applyMigration009(); async function applyMigration010() { try { const cols = (await query("PRAGMA table_info(formex_materials)")).rows; if (!cols.find(c => c.name === 'bore_diameter_mm')) { await run('ALTER TABLE formex_materials ADD COLUMN bore_diameter_mm REAL'); console.log('[migration-010] Added bore_diameter_mm to formex_materials.'); } } catch (err) { console.error('[migration-010] Error:', err.message); } } await applyMigration010(); async function applyMigration011() { try { const cols = (await query("PRAGMA table_info(formex_materials)")).rows; const colNames = cols.map(c => c.name); if (!colNames.includes('bore_depth_base_mm')) { await run('ALTER TABLE formex_materials ADD COLUMN bore_depth_base_mm REAL'); console.log('[migration-011] Added bore_depth_base_mm to formex_materials.'); } if (!colNames.includes('bore_depth_piece_mm')) { await run('ALTER TABLE formex_materials ADD COLUMN bore_depth_piece_mm REAL'); console.log('[migration-011] Added bore_depth_piece_mm to formex_materials.'); } } catch (err) { console.error('[migration-011] Error:', err.message); } } await applyMigration011(); async function applyMigration012() { try { const cols = (await query("PRAGMA table_info(formex_materials)")).rows; const colNames = cols.map(c => c.name); if (!colNames.includes('diameter_mm')) { await run('ALTER TABLE formex_materials ADD COLUMN diameter_mm REAL'); console.log('[migration-012] Added diameter_mm to formex_materials.'); } if (!colNames.includes('length_mm')) { await run('ALTER TABLE formex_materials ADD COLUMN length_mm REAL'); console.log('[migration-012] Added length_mm to formex_materials.'); } } catch (err) { console.error('[migration-012] Error:', err.message); } } await applyMigration012(); async function applyMigration013() { try { await run(`CREATE TABLE IF NOT EXISTS settings ( key TEXT PRIMARY KEY, value TEXT )`); const defaults = [ ['allow_sliding_doors', '0'], ['sanitary_gap_mm', '30'], ['drawer_depth_min_mm', '400'], ['drawer_depth_max_mm', '650'], ['drawer_depth_step_mm', '50'], ['drawer_back_gap_mm', '10'], ['drawer_hidden_handle_reduction_mm', '40'], ['standard_handle_axis_mm', '1000'], ['standard_handle_tolerance_mm', '200'], ['groove_width_mm', '9'], ['groove_depth_mm', '10.5'], ['groove_edge_offset_mm', '16'] ]; for (const [key, value] of defaults) { await run('INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2)', [key, value]); } console.log('[migration-013] Rule settings defaults ready.'); } catch (err) { console.error('[migration-013] Error:', err.message); } } await applyMigration013(); await ensureEngineeringRulesSeed(); // Initialize the scheduler for automated sync initScheduler(); app.listen(PORT, () => { console.log(`API configurateur armoires — http://localhost:${PORT}`); if (!hasClientBuild) { console.log('(Exécutez "npm run build" pour servir l\'app sur ce port)'); } });