Files
facturation-santinova/backend/src/routes/purchaseOrders.ts
2026-04-23 04:49:21 -04:00

249 lines
9.0 KiB
TypeScript

import { Router, Response } from 'express';
import { getPool } from '../config/database';
import { authenticate, AuthRequest } from '../middleware/auth';
import { logAction } from '../services/auditService';
const router = Router();
// GET /api/purchase-orders
router.get('/', authenticate, async (req: AuthRequest, res: Response) => {
try {
const pool = getPool();
const { status, supplierId, search, page = '1', limit = '20' } = req.query;
let query = 'SELECT po.*, s.name as supplier_display_name FROM purchase_orders po LEFT JOIN suppliers s ON po.supplier_id = s.id WHERE 1=1';
let countQuery = 'SELECT COUNT(*) as total FROM purchase_orders po WHERE 1=1';
const params: any[] = [];
const countParams: any[] = [];
if (status) {
query += ' AND po.status = ?';
countQuery += ' AND po.status = ?';
params.push(status);
countParams.push(status);
}
if (supplierId) {
query += ' AND po.supplier_id = ?';
countQuery += ' AND po.supplier_id = ?';
params.push(supplierId);
countParams.push(supplierId);
}
if (search) {
query += ' AND (po.order_number LIKE ? OR po.supplier_name LIKE ? OR po.description LIKE ?)';
countQuery += ' AND (po.order_number LIKE ? OR po.supplier_name LIKE ? OR po.description LIKE ?)';
const s = `%${search}%`;
params.push(s, s, s);
countParams.push(s, s, s);
}
query += ' ORDER BY po.created_at DESC';
const pageNum = parseInt(page as string) || 1;
const limitNum = parseInt(limit as string) || 20;
const offset = (pageNum - 1) * limitNum;
query += ' LIMIT ? OFFSET ?';
params.push(limitNum, offset);
const [rows]: any = await pool.execute(query, params);
const [countRows]: any = await pool.execute(countQuery, countParams);
const orders = rows.map((po: any) => ({
id: po.id,
orderNumber: po.order_number,
supplierId: po.supplier_id,
supplierName: po.supplier_name || po.supplier_display_name,
orderDate: po.order_date,
expectedDeliveryDate: po.expected_delivery_date,
amountHT: po.amount_ht,
amountTVA: po.amount_tva,
amountTTC: po.amount_ttc,
status: po.status,
description: po.description,
notes: po.notes,
createdBy: po.created_by,
createdAt: po.created_at,
updatedAt: po.updated_at,
}));
res.json({
data: orders,
pagination: {
total: countRows[0].total,
page: pageNum,
limit: limitNum,
totalPages: Math.ceil(countRows[0].total / limitNum),
},
});
} catch (error: any) {
console.error('Erreur liste BC:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// GET /api/purchase-orders/:id
router.get('/:id', authenticate, async (req: AuthRequest, res: Response) => {
try {
const pool = getPool();
const [rows]: any = await pool.execute(
'SELECT po.*, s.name as supplier_display_name FROM purchase_orders po LEFT JOIN suppliers s ON po.supplier_id = s.id WHERE po.id = ?',
[req.params.id]
);
if (!rows.length) {
return res.status(404).json({ error: 'Bon de commande non trouvé' });
}
const po = rows[0];
const [lines]: any = await pool.execute(
'SELECT * FROM purchase_order_lines WHERE purchase_order_id = ? ORDER BY line_order ASC',
[req.params.id]
);
// Get linked invoices
const [linkedInvoices]: any = await pool.execute(
'SELECT id, invoice_number, amount_ttc, status, matching_status FROM invoices WHERE purchase_order_id = ?',
[req.params.id]
);
res.json({
id: po.id,
orderNumber: po.order_number,
supplierId: po.supplier_id,
supplierName: po.supplier_name || po.supplier_display_name,
orderDate: po.order_date,
expectedDeliveryDate: po.expected_delivery_date,
amountHT: po.amount_ht,
amountTVA: po.amount_tva,
amountTTC: po.amount_ttc,
status: po.status,
description: po.description,
notes: po.notes,
createdBy: po.created_by,
createdAt: po.created_at,
updatedAt: po.updated_at,
lines: lines.map((l: any) => ({
id: l.id,
description: l.description,
quantity: l.quantity,
unitPrice: l.unit_price,
amountHT: l.amount_ht,
lineOrder: l.line_order,
})),
linkedInvoices: linkedInvoices.map((i: any) => ({
id: i.id,
invoiceNumber: i.invoice_number,
amountTTC: i.amount_ttc,
status: i.status,
matchingStatus: i.matching_status,
})),
});
} catch (error: any) {
console.error('Erreur get BC:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// POST /api/purchase-orders
router.post('/', authenticate, async (req: AuthRequest, res: Response) => {
try {
const { orderNumber, supplierId, supplierName, orderDate, expectedDeliveryDate,
amountHT, amountTVA, amountTTC, description, notes, lines } = req.body;
if (!orderNumber || !orderDate) {
return res.status(400).json({ error: 'Numéro de commande et date requis' });
}
const pool = getPool();
const [result]: any = await pool.execute(
`INSERT INTO purchase_orders (order_number, supplier_id, supplier_name, order_date, expected_delivery_date, amount_ht, amount_tva, amount_ttc, description, notes, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[orderNumber, supplierId || null, supplierName || null, orderDate, expectedDeliveryDate || null,
amountHT || null, amountTVA || null, amountTTC || null, description || null, notes || null, req.user!.id]
);
const poId = result.insertId;
// Insert lines
if (lines && lines.length > 0) {
for (let idx = 0; idx < lines.length; idx++) {
const line = lines[idx];
await pool.execute(
`INSERT INTO purchase_order_lines (purchase_order_id, description, quantity, unit_price, amount_ht, line_order)
VALUES (?, ?, ?, ?, ?, ?)`,
[poId, line.description || null, line.quantity || null, line.unitPrice || null, line.amountHT || null, idx]
);
}
}
await logAction('purchase_order', poId, 'creation', req.user!.id, `${req.user!.firstName} ${req.user!.lastName}`);
res.status(201).json({ id: poId, orderNumber });
} catch (error: any) {
if (error.code === 'ER_DUP_ENTRY') {
return res.status(409).json({ error: 'Ce numéro de commande existe déjà' });
}
console.error('Erreur création BC:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// PUT /api/purchase-orders/:id
router.put('/:id', authenticate, async (req: AuthRequest, res: Response) => {
try {
const { orderNumber, supplierId, supplierName, orderDate, expectedDeliveryDate,
amountHT, amountTVA, amountTTC, status, description, notes, lines } = req.body;
const pool = getPool();
await pool.execute(
`UPDATE purchase_orders SET order_number = ?, supplier_id = ?, supplier_name = ?, order_date = ?, expected_delivery_date = ?,
amount_ht = ?, amount_tva = ?, amount_ttc = ?, status = ?, description = ?, notes = ? WHERE id = ?`,
[orderNumber, supplierId || null, supplierName || null, orderDate, expectedDeliveryDate || null,
amountHT || null, amountTVA || null, amountTTC || null, status || 'brouillon', description || null, notes || null, req.params.id]
);
// Update lines if provided
if (lines) {
await pool.execute('DELETE FROM purchase_order_lines WHERE purchase_order_id = ?', [req.params.id]);
for (let idx = 0; idx < lines.length; idx++) {
const line = lines[idx];
await pool.execute(
`INSERT INTO purchase_order_lines (purchase_order_id, description, quantity, unit_price, amount_ht, line_order)
VALUES (?, ?, ?, ?, ?, ?)`,
[req.params.id, line.description || null, line.quantity || null, line.unitPrice || null, line.amountHT || null, idx]
);
}
}
await logAction('purchase_order', parseInt(req.params.id), 'modification', req.user!.id, `${req.user!.firstName} ${req.user!.lastName}`);
res.json({ message: 'Bon de commande mis à jour' });
} catch (error: any) {
console.error('Erreur update BC:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
// DELETE /api/purchase-orders/:id
router.delete('/:id', authenticate, async (req: AuthRequest, res: Response) => {
try {
const pool = getPool();
await pool.execute('DELETE FROM purchase_order_lines WHERE purchase_order_id = ?', [req.params.id]);
await pool.execute('DELETE FROM purchase_orders WHERE id = ?', [req.params.id]);
await logAction('purchase_order', parseInt(req.params.id), 'suppression', req.user!.id, `${req.user!.firstName} ${req.user!.lastName}`);
res.json({ message: 'Bon de commande supprimé' });
} catch (error: any) {
console.error('Erreur suppression BC:', error);
res.status(500).json({ error: 'Erreur serveur' });
}
});
export default router;