You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 

713 lines
19 KiB

// controllers/commandesController.js
const Commande = require('../models/Commande');
const CommandeItem = require('../models/CommandeItem');
const Menu = require('../models/Menu');
const { Op } = require('sequelize');
const sequelize = require('../config/database');
// Helper function to validate and convert numbers
const validateNumber = (value, defaultValue = 0) => {
const num = parseFloat(value);
return isNaN(num) ? defaultValue : num;
};
// Helper function to validate integer
const validateInteger = (value, defaultValue = 1) => {
const num = parseInt(value);
return isNaN(num) ? defaultValue : num;
};
// Get all commandes with menu details
const getAllCommandes = async (req, res) => {
try {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 10;
const offset = (page - 1) * limit;
const where = {};
// Filters
if (req.query.statut) {
where.statut = req.query.statut;
}
if (req.query.client_id) {
where.client_id = req.query.client_id;
}
if (req.query.table_id) {
where.table_id = req.query.table_id;
}
if (req.query.serveur) {
where.serveur = { [Op.like]: `%${req.query.serveur}%` };
}
if (req.query.date_debut && req.query.date_fin) {
where.date_commande = {
[Op.between]: [req.query.date_debut, req.query.date_fin]
};
}
const { count, rows } = await Commande.findAndCountAll({
where,
limit,
offset,
order: [['date_commande', 'DESC']]
});
// Fetch items with menu details for each commande
const commandeIds = rows.map(c => c.id);
let itemsWithMenus = [];
if (commandeIds.length > 0) {
itemsWithMenus = await sequelize.query(`
SELECT
ci.*,
m.nom as menu_nom,
m.commentaire as menu_description,
m.prix as menu_prix_actuel
FROM commande_items ci
LEFT JOIN menus m ON ci.menu_id = m.id
WHERE ci.commande_id IN (${commandeIds.join(',')})
ORDER BY ci.id
`, {
type: sequelize.QueryTypes.SELECT
});
}
// Group items by commande_id
const itemsByCommandeId = itemsWithMenus.reduce((acc, item) => {
if (!acc[item.commande_id]) {
acc[item.commande_id] = [];
}
acc[item.commande_id].push(item);
return acc;
}, {});
// Add items to each commande
rows.forEach(commande => {
commande.dataValues.items = itemsByCommandeId[commande.id] || [];
});
res.status(200).json({
success: true,
data: {
commandes: rows,
pagination: {
currentPage: page,
totalPages: Math.ceil(count / limit),
totalItems: count,
itemsPerPage: limit
}
}
});
} catch (error) {
console.error('Error in getAllCommandes:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la récupération des commandes',
error: error.message
});
}
};
// Get commande by ID with menu details
const getCommandeById = async (req, res) => {
try {
const commande = await Commande.findByPk(req.params.id);
if (!commande) {
return res.status(404).json({
success: false,
message: 'Commande non trouvée'
});
}
// Fetch items with menu details
const itemsWithMenus = await sequelize.query(`
SELECT
ci.*,
m.nom as menu_nom,
m.commentaire as menu_description,
m.prix as menu_prix_actuel
FROM commande_items ci
LEFT JOIN menus m ON ci.menu_id = m.id
WHERE ci.commande_id = :commandeId
ORDER BY ci.id
`, {
replacements: { commandeId: req.params.id },
type: sequelize.QueryTypes.SELECT
});
commande.dataValues.items = itemsWithMenus;
res.status(200).json({
success: true,
data: commande
});
} catch (error) {
console.error('Error in getCommandeById:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la récupération de la commande',
error: error.message
});
}
};
// Create new commande with items (fetch prices from menu)
const createCommande = async (req, res) => {
let transaction;
try {
// Start transaction
transaction = await sequelize.transaction();
console.log('Received data:', JSON.stringify(req.body, null, 2));
const {
client_id,
table_id,
reservation_id,
numero_commande,
statut,
total_ht,
total_tva,
total_ttc,
mode_paiement,
commentaires,
serveur,
date_commande,
date_service,
items // Array of items with menu_id and quantite
} = req.body;
// Validate required fields
if (!table_id) {
return res.status(400).json({
success: false,
message: 'table_id est requis'
});
}
// Create the commande first
const commandeData = {
client_id: client_id || null,
table_id: validateInteger(table_id),
reservation_id: reservation_id || null,
numero_commande: numero_commande || null,
statut: statut || 'en_attente',
total_ht: validateNumber(total_ht, 0).toFixed(2),
total_tva: validateNumber(total_tva, 0).toFixed(2),
total_ttc: validateNumber(total_ttc, 0).toFixed(2),
mode_paiement: mode_paiement || null,
commentaires: commentaires || null,
serveur: serveur || null,
date_commande: date_commande || new Date(),
date_service: date_service || null
};
console.log('Validated commande data:', JSON.stringify(commandeData, null, 2));
const newCommande = await Commande.create(commandeData, { transaction });
// If items are provided, create them with prices from menu
if (items && Array.isArray(items) && items.length > 0) {
console.log('Processing items:', JSON.stringify(items, null, 2));
const commandeItems = [];
let calculatedTotal = 0;
// Get all menu IDs from items
const menuIds = items.map(item => validateInteger(item.menu_id)).filter(id => id > 0);
if (menuIds.length === 0) {
return res.status(400).json({
success: false,
message: 'Aucun menu_id valide fourni'
});
}
// Fetch menu prices
const menus = await Menu.findAll({
where: {
id: { [Op.in]: menuIds }
},
attributes: ['id', 'nom', 'prix', 'disponible'],
transaction
});
// Create a map of menu_id to menu data
const menuMap = menus.reduce((acc, menu) => {
acc[menu.id] = menu;
return acc;
}, {});
// Process each item
for (const item of items) {
const menu_id = validateInteger(item.menu_id);
const quantite = validateInteger(item.quantite, 1);
if (menu_id === 0) {
return res.status(400).json({
success: false,
message: 'menu_id invalide',
invalidItem: item
});
}
// Check if menu exists
const menu = menuMap[menu_id];
if (!menu) {
return res.status(400).json({
success: false,
message: `Menu avec ID ${menu_id} n'existe pas`,
invalidItem: item
});
}
// Check if menu is available
if (!menu.disponible) {
return res.status(400).json({
success: false,
message: `Menu "${menu.nom}" n'est plus disponible`,
invalidItem: item
});
}
// Use price from menu table
const prix_unitaire = validateNumber(menu.prix, 0);
const total_item = prix_unitaire * quantite;
if (prix_unitaire === 0) {
return res.status(400).json({
success: false,
message: `Prix invalide pour le menu "${menu.nom}"`,
invalidItem: item
});
}
const validatedItem = {
commande_id: newCommande.id,
menu_id: menu_id,
quantite: quantite,
prix_unitaire: prix_unitaire.toFixed(2),
total_item: total_item.toFixed(2),
commentaires: item.commentaires || null,
statut: item.statut || 'commande'
};
commandeItems.push(validatedItem);
calculatedTotal += total_item;
}
console.log('Validated items:', JSON.stringify(commandeItems, null, 2));
await CommandeItem.bulkCreate(commandeItems, { transaction });
// Auto-calculate totals if not provided
if (validateNumber(total_ttc, 0) === 0) {
await newCommande.update({
total_ht: calculatedTotal.toFixed(2),
total_ttc: calculatedTotal.toFixed(2)
}, { transaction });
}
}
// Commit the transaction
await transaction.commit();
// Now fetch the created commande with items and menu details (outside of transaction)
const createdCommande = await Commande.findByPk(newCommande.id);
const createdItemsWithMenus = await sequelize.query(`
SELECT
ci.*,
m.nom as menu_nom,
m.commentaire as menu_description,
m.prix as menu_prix_actuel
FROM commande_items ci
LEFT JOIN menus m ON ci.menu_id = m.id
WHERE ci.commande_id = :commandeId
ORDER BY ci.id
`, {
replacements: { commandeId: newCommande.id },
type: sequelize.QueryTypes.SELECT
});
createdCommande.dataValues.items = createdItemsWithMenus;
res.status(201).json({
success: true,
message: 'Commande créée avec succès',
data: createdCommande
});
} catch (error) {
// Only rollback if transaction exists and hasn't been finished
if (transaction && !transaction.finished) {
try {
await transaction.rollback();
} catch (rollbackError) {
console.error('Error rolling back transaction:', rollbackError);
}
}
console.error('Error in createCommande:', error);
res.status(400).json({
success: false,
message: 'Erreur lors de la création de la commande',
error: error.message
});
}
};
// Update commande (also fetch prices from menu)
const updateCommande = async (req, res) => {
let transaction;
try {
transaction = await sequelize.transaction();
const commandeId = req.params.id;
const { items, ...commandeData } = req.body;
const commande = await Commande.findByPk(commandeId);
if (!commande) {
return res.status(404).json({
success: false,
message: 'Commande non trouvée'
});
}
// Validate numeric fields in commandeData
if (commandeData.total_ht !== undefined) {
commandeData.total_ht = validateNumber(commandeData.total_ht, 0).toFixed(2);
}
if (commandeData.total_tva !== undefined) {
commandeData.total_tva = validateNumber(commandeData.total_tva, 0).toFixed(2);
}
if (commandeData.total_ttc !== undefined) {
commandeData.total_ttc = validateNumber(commandeData.total_ttc, 0).toFixed(2);
}
// Update commande
await commande.update(commandeData, { transaction });
// If items are provided, update them with current menu prices
if (items && Array.isArray(items) && items.length > 0) {
// Delete existing items
await CommandeItem.destroy({
where: { commande_id: commandeId },
transaction
});
// Get menu prices
const menuIds = items.map(item => validateInteger(item.menu_id)).filter(id => id > 0);
const menus = await Menu.findAll({
where: {
id: { [Op.in]: menuIds }
},
attributes: ['id', 'nom', 'prix', 'est_disponible'],
transaction
});
const menuMap = menus.reduce((acc, menu) => {
acc[menu.id] = menu;
return acc;
}, {});
// Create new items with validation
const commandeItems = [];
let calculatedTotal = 0;
for (const item of items) {
const menu_id = validateInteger(item.menu_id);
const quantite = validateInteger(item.quantite, 1);
const menu = menuMap[menu_id];
if (!menu) {
return res.status(400).json({
success: false,
message: `Menu avec ID ${menu_id} n'existe pas`
});
}
const prix_unitaire = validateNumber(menu.prix, 0);
const total_item = prix_unitaire * quantite;
const validatedItem = {
commande_id: commandeId,
menu_id: menu_id,
quantite: quantite,
prix_unitaire: prix_unitaire.toFixed(2),
total_item: total_item.toFixed(2),
commentaires: item.commentaires || null,
statut: item.statut || 'commande'
};
commandeItems.push(validatedItem);
calculatedTotal += total_item;
}
await CommandeItem.bulkCreate(commandeItems, { transaction });
// Recalculate totals if not provided
if (!commandeData.total_ttc) {
await commande.update({
total_ht: calculatedTotal.toFixed(2),
total_ttc: calculatedTotal.toFixed(2)
}, { transaction });
}
}
await transaction.commit();
// Fetch updated commande with items and menu details (outside of transaction)
const updatedCommande = await Commande.findByPk(commandeId);
const updatedItemsWithMenus = await sequelize.query(`
SELECT
ci.*,
m.nom as menu_nom,
m.commentaire as menu_description,
m.prix as menu_prix_actuel
FROM commande_items ci
LEFT JOIN menus m ON ci.menu_id = m.id
WHERE ci.commande_id = :commandeId
ORDER BY ci.id
`, {
replacements: { commandeId },
type: sequelize.QueryTypes.SELECT
});
updatedCommande.dataValues.items = updatedItemsWithMenus;
res.status(200).json({
success: true,
message: 'Commande mise à jour avec succès',
data: updatedCommande
});
} catch (error) {
// Only rollback if transaction exists and hasn't been finished
if (transaction && !transaction.finished) {
try {
await transaction.rollback();
} catch (rollbackError) {
console.error('Error rolling back transaction:', rollbackError);
}
}
console.error('Error in updateCommande:', error);
res.status(400).json({
success: false,
message: 'Erreur lors de la mise à jour de la commande',
error: error.message
});
}
};
// Delete commande
const deleteCommande = async (req, res) => {
let transaction;
try {
transaction = await sequelize.transaction();
const commandeId = req.params.id;
const commande = await Commande.findByPk(commandeId);
if (!commande) {
return res.status(404).json({
success: false,
message: 'Commande non trouvée'
});
}
// Delete items first
await CommandeItem.destroy({
where: { commande_id: commandeId },
transaction
});
// Delete commande
await commande.destroy({ transaction });
await transaction.commit();
res.status(200).json({
success: true,
message: 'Commande supprimée avec succès'
});
} catch (error) {
if (transaction && !transaction.finished) {
try {
await transaction.rollback();
} catch (rollbackError) {
console.error('Error rolling back transaction:', rollbackError);
}
}
console.error('Error in deleteCommande:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la suppression de la commande',
error: error.message
});
}
};
// Update commande status
const updateStatut = async (req, res) => {
try {
const commandeId = req.params.id;
const { statut } = req.body;
if (!statut) {
return res.status(400).json({
success: false,
message: 'Le statut est requis'
});
}
const commande = await Commande.findByPk(commandeId);
if (!commande) {
return res.status(404).json({
success: false,
message: 'Commande non trouvée'
});
}
const updateData = { statut };
if (statut === 'servie' && !commande.date_service) {
updateData.date_service = new Date();
}
await commande.update(updateData);
res.status(200).json({
success: true,
message: 'Statut de la commande mis à jour avec succès',
data: commande
});
} catch (error) {
console.error('Error in updateStatut:', error);
res.status(400).json({
success: false,
message: 'Erreur lors de la mise à jour du statut',
error: error.message
});
}
};
// Get commandes by status with menu details
const getCommandesByStatut = async (req, res) => {
try {
const { statut } = req.params;
const commandes = await Commande.findAll({
where: { statut },
order: [['date_commande', 'DESC']]
});
// Fetch items with menu details for each commande
if (commandes.length > 0) {
const commandeIds = commandes.map(c => c.id);
const itemsWithMenus = await sequelize.query(`
SELECT
ci.*,
m.nom as menu_nom,
m.commentaire as menu_description,
m.prix as menu_prix_actuel
FROM commande_items ci
LEFT JOIN menus m ON ci.menu_id = m.id
WHERE ci.commande_id IN (${commandeIds.join(',')})
ORDER BY ci.id
`, {
type: sequelize.QueryTypes.SELECT
});
const itemsByCommandeId = itemsWithMenus.reduce((acc, item) => {
if (!acc[item.commande_id]) {
acc[item.commande_id] = [];
}
acc[item.commande_id].push(item);
return acc;
}, {});
commandes.forEach(commande => {
commande.dataValues.items = itemsByCommandeId[commande.id] || [];
});
}
res.status(200).json({
success: true,
data: commandes
});
} catch (error) {
console.error('Error in getCommandesByStatut:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la récupération des commandes par statut',
error: error.message
});
}
};
// Get daily statistics
const getDailyStats = async (req, res) => {
try {
const date = req.query.date || new Date().toISOString().split('T')[0];
const startOfDay = new Date(date + 'T00:00:00.000Z');
const endOfDay = new Date(date + 'T23:59:59.999Z');
const stats = await Commande.findAll({
where: {
date_commande: {
[Op.between]: [startOfDay, endOfDay]
}
},
attributes: [
'statut',
[sequelize.fn('COUNT', sequelize.col('id')), 'count'],
[sequelize.fn('SUM', sequelize.col('total_ttc')), 'total_amount']
],
group: ['statut'],
raw: true
});
res.status(200).json({
success: true,
data: {
date,
statistics: stats
}
});
} catch (error) {
console.error('Error in getDailyStats:', error);
res.status(500).json({
success: false,
message: 'Erreur lors de la récupération des statistiques',
error: error.message
});
}
};
module.exports = {
getAllCommandes,
getCommandeById,
createCommande,
updateCommande,
deleteCommande,
updateStatut,
getCommandesByStatut,
getDailyStats
};