392 lines
14 KiB
JavaScript
392 lines
14 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 { 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(`
|
|
<!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 :</p>
|
|
<pre>npm run build</pre>
|
|
<p>Puis rechargez cette page. Ou lancez le client en mode dev :</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')`);
|
|
|
|
// 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)');
|
|
}
|
|
});
|