249 lines
9.0 KiB
TypeScript
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;
|