diff --git a/controllers/commandeController.js b/controllers/commandeController.js index cea3747..cb79c36 100644 --- a/controllers/commandeController.js +++ b/controllers/commandeController.js @@ -1,8 +1,23 @@ // 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'); -// Get all commandes +// 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; @@ -41,6 +56,40 @@ const getAllCommandes = async (req, res) => { 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: { @@ -54,6 +103,7 @@ const getAllCommandes = async (req, res) => { } }); } catch (error) { + console.error('Error in getAllCommandes:', error); res.status(500).json({ success: false, message: 'Erreur lors de la récupération des commandes', @@ -62,7 +112,7 @@ const getAllCommandes = async (req, res) => { } }; -// Get commande by ID +// Get commande by ID with menu details const getCommandeById = async (req, res) => { try { const commande = await Commande.findByPk(req.params.id); @@ -74,11 +124,30 @@ const getCommandeById = async (req, res) => { }); } + // 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', @@ -87,9 +156,16 @@ const getCommandeById = async (req, res) => { } }; -// Create new commande +// 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, @@ -103,31 +179,181 @@ const createCommande = async (req, res) => { commentaires, serveur, date_commande, - date_service + date_service, + items // Array of items with menu_id and quantite } = req.body; - const newCommande = await Commande.create({ - client_id, - table_id, - reservation_id, - numero_commande, + // 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: total_ht || 0.00, - total_tva: total_tva || 0.00, - total_ttc: total_ttc || 0.00, - mode_paiement, - commentaires, - serveur, + 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: 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: newCommande + 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', @@ -136,11 +362,15 @@ const createCommande = async (req, res) => { } }; -// Update commande +// 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 updateData = req.body; + const { items, ...commandeData } = req.body; const commande = await Commande.findByPk(commandeId); @@ -151,14 +381,126 @@ const updateCommande = async (req, res) => { }); } - await commande.update(updateData); + // 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: commande + 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', @@ -169,7 +511,11 @@ const updateCommande = async (req, res) => { // 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); @@ -181,13 +527,32 @@ const deleteCommande = async (req, res) => { }); } - await commande.destroy(); + // 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', @@ -231,6 +596,7 @@ const updateStatut = async (req, res) => { 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', @@ -239,7 +605,7 @@ const updateStatut = async (req, res) => { } }; -// Get commandes by status +// Get commandes by status with menu details const getCommandesByStatut = async (req, res) => { try { const { statut } = req.params; @@ -249,11 +615,43 @@ const getCommandesByStatut = async (req, res) => { 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', @@ -281,7 +679,8 @@ const getDailyStats = async (req, res) => { [sequelize.fn('COUNT', sequelize.col('id')), 'count'], [sequelize.fn('SUM', sequelize.col('total_ttc')), 'total_amount'] ], - group: ['statut'] + group: ['statut'], + raw: true }); res.status(200).json({ @@ -291,7 +690,9 @@ const getDailyStats = async (req, res) => { 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', @@ -300,7 +701,6 @@ const getDailyStats = async (req, res) => { } }; -// IMPORTANT: Make sure this export is at the very end of the file module.exports = { getAllCommandes, getCommandeById, diff --git a/models/Commande.js b/models/Commande.js index 8a01fda..3ff58d5 100644 --- a/models/Commande.js +++ b/models/Commande.js @@ -22,7 +22,7 @@ const Commande = sequelize.define('Commande', { }, numero_commande: { type: DataTypes.STRING, - allowNull: false, + allowNull: true, unique: true }, statut: { diff --git a/models/CommandeItem.js b/models/CommandeItem.js index 0086fd6..cf3e985 100644 --- a/models/CommandeItem.js +++ b/models/CommandeItem.js @@ -1,61 +1,58 @@ +// models/CommandeItem.js (note the capital C and I) const { DataTypes } = require('sequelize'); const sequelize = require('../config/database'); -module.exports = (sequelize) => { const CommandeItem = sequelize.define('CommandeItem', { - id: { - type: DataTypes.INTEGER, - primaryKey: true, - autoIncrement: true - }, - commande_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: 'commandes', - key: 'id' - } - }, - menu_id: { - type: DataTypes.INTEGER, - allowNull: false, - references: { - model: 'menus', - key: 'id' - } - }, - quantite: { - type: DataTypes.INTEGER, - allowNull: false, - defaultValue: 1, - validate: { - min: { - args: [1], - msg: 'La quantité doit être au moins de 1' - } - } - }, - prix_unitaire: { - type: DataTypes.DECIMAL(10, 2), - allowNull: false - }, - total_item: { - type: DataTypes.DECIMAL(10, 2), - allowNull: false - }, - commentaires: { - type: DataTypes.TEXT, - allowNull: true - }, - statut: { - type: DataTypes.ENUM('commande', 'en_preparation', 'pret', 'servi'), - defaultValue: 'commande' + id: { + type: DataTypes.INTEGER, + primaryKey: true, + autoIncrement: true + }, + commande_id: { + type: DataTypes.INTEGER, + allowNull: false, + references: { + model: 'commandes', + key: 'id' } + }, + menu_id: { + type: DataTypes.INTEGER, + allowNull: false + }, + quantite: { + type: DataTypes.INTEGER, + allowNull: false, + defaultValue: 1 + }, + prix_unitaire: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false + }, + total_item: { + type: DataTypes.DECIMAL(10, 2), + allowNull: false + }, + commentaires: { + type: DataTypes.TEXT, + allowNull: true + }, + statut: { + type: DataTypes.ENUM('commande', 'en_preparation', 'pret', 'servi'), + allowNull: true, + defaultValue: 'commande' + } }, { - tableName: 'commande_items', - timestamps: true, - createdAt: 'created_at', - updatedAt: 'updated_at' + tableName: 'commande_items', + timestamps: true, + createdAt: 'created_at', + updatedAt: 'updated_at', + hooks: { + beforeSave: (item) => { + // Auto-calculate total_item + item.total_item = parseFloat(item.prix_unitaire) * parseInt(item.quantite); + } + } }); -return CommandeItem; -} + +module.exports = CommandeItem; diff --git a/models/associations.js b/models/associations.js index d2c09df..a031193 100644 --- a/models/associations.js +++ b/models/associations.js @@ -135,6 +135,18 @@ const defineAssociations = () => { }); } + Commande.hasMany(CommandeItem, { + foreignKey: 'commande_id', + as: 'items', + onDelete: 'CASCADE' + }); + + CommandeItem.belongsTo(Commande, { + foreignKey: 'commande_id', + as: 'commande' + }); + + if (Reservation) {