Files
lejayandCursor 30921aade6 v2.1
Ship configurator updates with catalog DB included for server deploy.

Co-authored-by: Cursor <[email protected]>
2026-10-07 17:13:03 +02:00

442 lines
16 KiB
JavaScript

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 { getMaterialImage } from './routes/materialImage.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,
triggerLeubaHiagSync,
streamLeubaHiagSync,
getLeubaHiagMaterials,
getLeubaHiagTargets,
getLogs,
getStatus,
getFormexMaterials,
getFormexMaterial,
createFormexMaterial,
updateFormexMaterial,
setFormexMaterialActive,
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,
generateCuttingPlanRoute,
exportExcelRoute
} from './routes/plans.js';
import {
designTipsRoute,
recommendMaterialsRoute,
renderRoute
} from './routes/ai.js';
import {
getSavedDesigns,
createSavedDesign,
updateSavedDesign,
deleteSavedDesign
} from './routes/savedDesigns.js';
import {
getLayoutPresets,
createLayoutPreset,
deleteLayoutPreset
} from './routes/layoutPresets.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/material-image', getMaterialImage);
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/saved-designs', getSavedDesigns);
app.post('/api/saved-designs', createSavedDesign);
app.put('/api/saved-designs/:id', updateSavedDesign);
app.delete('/api/saved-designs/:id', deleteSavedDesign);
app.get('/api/layout-presets', getLayoutPresets);
app.post('/api/layout-presets', createLayoutPreset);
app.delete('/api/layout-presets/:id', deleteLayoutPreset);
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/leubahiag/targets', getLeubaHiagTargets);
app.post('/api/sync/leubahiag', triggerLeubaHiagSync);
app.get('/api/sync/leubahiag/stream', streamLeubaHiagSync);
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.patch('/api/formex/materials/:id/active', setFormexMaterialActive);
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);
app.get('/api/leubahiag/materials', getLeubaHiagMaterials);
// 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/cutting-plan', generateCuttingPlanRoute);
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-excel', exportExcelRoute);
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(`
<!DOCTYPE html>
<html><head><meta charset="utf-8"><title>Configurateur</title></head>
<body style="font-family:sans-serif;padding:2rem;">
<h1>Application non construite</h1>
<p>Depuis la racine du projet, exécutez&nbsp;:</p>
<pre>npm run build</pre>
<p>Puis rechargez cette page. Ou lancez le client en mode dev&nbsp;:</p>
<pre>npm run client</pre>
<p>et ouvrez <a href="http://localhost:5173">http://localhost:5173</a>.</p>
</body></html>
`);
});
}
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', '[email protected]', 'panels'),
('blum', 'Blum Switzerland', '[email protected]', 'hardware'),
('haefele', 'Hafele', '[email protected]', 'materials'),
('leubahiag', 'Leuba Hiag', '[email protected]', '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();
// Apply migration 014: widen plinth max to 200 mm (remarque client) without clobbering custom values.
async function applyMigration014() {
try {
await run('INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2)', ['plinth_height_min_mm', '60']);
await run('INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2)', ['plinth_height_max_mm', '200']);
// Bump legacy default 160 -> 200; leave any other (customized) value untouched.
await run("UPDATE settings SET value = '200' WHERE key = 'plinth_height_max_mm' AND value = '160'");
console.log('[migration-014] Plinth max widened to 200 mm.');
} catch (err) {
console.error('[migration-014] Error:', err.message);
}
}
await applyMigration014();
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)');
}
});