v2.1
Ship configurator updates with catalog DB included for server deploy. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { sync, getSyncLogs, getMaterials } from '../services/formexCrawler.js';
|
||||
import { syncHaefeleByUrls } from '../services/haefeleCrawler.js';
|
||||
import { syncLeubaHiag, LEUBA_HIAG_TARGETS } from '../services/leubaHiagCrawler.js';
|
||||
import { getSchedulerStatus } from '../services/scheduler.js';
|
||||
import pool, { run } from '../db/index.js';
|
||||
|
||||
@@ -206,6 +207,150 @@ export async function getHaefeleMaterials(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sync/leubahiag/targets
|
||||
* List the curated Leuba Hiag catalog lines available for import.
|
||||
*/
|
||||
export function getLeubaHiagTargets(req, res) {
|
||||
res.json(LEUBA_HIAG_TARGETS.map(({ key, label, tier }) => ({ key, label, tier })));
|
||||
}
|
||||
|
||||
function parseTargetsParam(value) {
|
||||
if (Array.isArray(value)) return value.filter(Boolean);
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
return value.split(/[\n,;]+/g).map((v) => v.trim()).filter(Boolean);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /api/sync/leubahiag
|
||||
* Trigger a Leuba Hiag curated catalog sync (non-streaming).
|
||||
* Body: { targets?: string[], activate?: boolean, credentials?: {...} }
|
||||
*/
|
||||
export async function triggerLeubaHiagSync(req, res) {
|
||||
try {
|
||||
const targets = parseTargetsParam(req.body?.targets);
|
||||
const activate = req.body?.activate === true || req.body?.activate === '1';
|
||||
const fetchPrices = req.body?.fetchPrices !== false && req.body?.fetchPrices !== '0';
|
||||
const credentials = {
|
||||
username: req.body?.username || req.body?.email,
|
||||
password: req.body?.password,
|
||||
};
|
||||
|
||||
const result = await syncLeubaHiag({ targets, activate, fetchPrices, credentials }, () => {});
|
||||
if (!result.success) {
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
error: result.error || 'Sync Leuba Hiag échouée',
|
||||
itemsCount: result.itemsCount || 0,
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: `Synchronisation Leuba Hiag terminée: ${result.itemsCount} produits`,
|
||||
itemsCount: result.itemsCount,
|
||||
createdCount: result.createdCount || 0,
|
||||
updatedCount: result.updatedCount || 0,
|
||||
pricedCount: result.pricedCount || 0,
|
||||
duration: result.duration,
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ success: false, error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sync/leubahiag/stream
|
||||
* SSE endpoint for Leuba Hiag sync progress.
|
||||
* Query: targets=key1,key2 (optional), activate=1 (optional)
|
||||
*/
|
||||
export async function streamLeubaHiagSync(req, res) {
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('X-Accel-Buffering', 'no');
|
||||
res.flushHeaders();
|
||||
|
||||
const emit = (data) => {
|
||||
res.write(`data: ${JSON.stringify(data)}\n\n`);
|
||||
};
|
||||
|
||||
req.on('close', () => {
|
||||
// client disconnected
|
||||
});
|
||||
|
||||
const targets = parseTargetsParam(req.query.targets);
|
||||
const activate = req.query.activate === '1' || req.query.activate === 'true';
|
||||
const fetchPrices = req.query.prices !== '0' && req.query.prices !== 'false';
|
||||
|
||||
try {
|
||||
await syncLeubaHiag({ targets, activate, fetchPrices }, emit);
|
||||
} catch (err) {
|
||||
emit({ type: 'error', error: err.message });
|
||||
}
|
||||
|
||||
res.end();
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/leubahiag/materials
|
||||
* List Leuba Hiag materials that were imported.
|
||||
*/
|
||||
export async function getLeubaHiagMaterials(req, res) {
|
||||
try {
|
||||
const { search = '', active, page = '1', limit = '25' } = req.query;
|
||||
const pageNum = Math.max(1, parseInt(page, 10));
|
||||
const limitNum = Math.min(200, Math.max(1, parseInt(limit, 10)));
|
||||
const offset = (pageNum - 1) * limitNum;
|
||||
|
||||
const whereClauses = [
|
||||
`(supplier_id = (SELECT id FROM suppliers WHERE code = 'leubahiag') OR formex_code LIKE 'LH-%')`
|
||||
];
|
||||
const params = [];
|
||||
|
||||
if (search) {
|
||||
whereClauses.push('(name LIKE ? OR formex_code LIKE ? OR collection LIKE ?)');
|
||||
params.push(`%${search}%`, `%${search}%`, `%${search}%`);
|
||||
}
|
||||
if (active === '1' || active === 'true') {
|
||||
whereClauses.push('active = 1');
|
||||
} else if (active === '0' || active === 'false') {
|
||||
whereClauses.push('active = 0');
|
||||
}
|
||||
|
||||
const whereSQL = `WHERE ${whereClauses.join(' AND ')}`;
|
||||
const countResult = await pool.query(
|
||||
`SELECT COUNT(*) as total FROM formex_materials ${whereSQL}`,
|
||||
params
|
||||
);
|
||||
const total = countResult.rows[0]?.total || 0;
|
||||
|
||||
const { rows } = await pool.query(
|
||||
`SELECT id, formex_code, name, collection, category, thickness_mm, width_mm, height_mm,
|
||||
price_chf, price_unit, image_url, formex_url, active, last_synced_at
|
||||
FROM formex_materials
|
||||
${whereSQL}
|
||||
ORDER BY collection, name
|
||||
LIMIT ? OFFSET ?`,
|
||||
[...params, limitNum, offset]
|
||||
);
|
||||
|
||||
res.json({
|
||||
materials: rows,
|
||||
pagination: {
|
||||
page: pageNum,
|
||||
limit: limitNum,
|
||||
total,
|
||||
totalPages: Math.ceil(total / limitNum)
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /api/sync/logs
|
||||
* Get sync history
|
||||
@@ -439,6 +584,33 @@ export async function updateFormexMaterial(req, res) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* PATCH /api/formex/materials/:id/active
|
||||
* Toggle only the `active` flag of a material (lossless: never touches other columns).
|
||||
* Body: { active: boolean }
|
||||
*/
|
||||
export async function setFormexMaterialActive(req, res) {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const active = req.body?.active === true || req.body?.active === 1 || req.body?.active === '1';
|
||||
|
||||
const existing = await pool.query('SELECT id FROM formex_materials WHERE id = ?', [id]);
|
||||
if (existing.rows.length === 0) {
|
||||
return res.status(404).json({ error: 'Matériau non trouvé' });
|
||||
}
|
||||
|
||||
await run('UPDATE formex_materials SET active = ? WHERE id = ?', [active ? 1 : 0, id]);
|
||||
|
||||
const { rows } = await pool.query(
|
||||
'SELECT id, formex_code, name, active FROM formex_materials WHERE id = ?',
|
||||
[id]
|
||||
);
|
||||
res.json(rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /api/formex/materials/:id
|
||||
* Delete a material
|
||||
|
||||
Reference in New Issue
Block a user