const { Ticket, TicketItem, Commande, CommandeItem, Client, Utilisateur, Menu, sequelize } = require('../models/associations'); const { Op } = require('sequelize'); const PDFDocument = require('pdfkit'); const fs = require('fs').promises; const path = require('path'); class TicketController { // Générer un numéro de ticket unique async generateTicketNumber() { const date = new Date(); const year = date.getFullYear().toString().substr(-2); const month = (date.getMonth() + 1).toString().padStart(2, '0'); const day = date.getDate().toString().padStart(2, '0'); const prefix = `T${year}${month}${day}`; // Trouver le dernier ticket du jour const lastTicket = await Ticket.findOne({ where: { numero_ticket: { [Op.like]: `${prefix}%` } }, order: [['numero_ticket', 'DESC']] }); let sequence = 1; if (lastTicket) { const lastSequence = parseInt(lastTicket.numero_ticket.substr(-4)); sequence = lastSequence + 1; } return `${prefix}${sequence.toString().padStart(4, '0')}`; } // Calculer les montants avec TVA calculateAmounts(items, taux_tva = 20, remise = 0) { let montant_ht = 0; items.forEach(item => { const prix_unitaire_ht = item.prix_unitaire_ttc / (1 + taux_tva / 100); const montant_item_ht = prix_unitaire_ht * item.quantite - (item.remise_unitaire || 0); montant_ht += montant_item_ht; }); montant_ht -= remise; const montant_tva = montant_ht * (taux_tva / 100); const montant_ttc = montant_ht + montant_tva; return { montant_ht: Math.max(0, montant_ht), montant_tva: Math.max(0, montant_tva), montant_ttc: Math.max(0, montant_ttc) }; } // Obtenir tous les tickets avec pagination et filtres async getAllTickets(req, res) { try { const { page = 1, limit = 10, search = '', statut, mode_paiement, date_debut, date_fin, client_id, utilisateur_id, sort_by = 'date_emission', sort_order = 'DESC' } = req.query; const offset = (parseInt(page) - 1) * parseInt(limit); const whereConditions = {}; if (search) { whereConditions[Op.or] = [ { numero_ticket: { [Op.like]: `%${search}%` } }, { '$Client.nom$': { [Op.like]: `%${search}%` } }, { '$Client.prenom$': { [Op.like]: `%${search}%` } } ]; } if (statut) whereConditions.statut = statut; if (mode_paiement) whereConditions.mode_paiement = mode_paiement; if (client_id) whereConditions.client_id = client_id; if (utilisateur_id) whereConditions.utilisateur_id = utilisateur_id; if (date_debut || date_fin) { whereConditions.date_emission = {}; if (date_debut) whereConditions.date_emission[Op.gte] = new Date(date_debut); if (date_fin) whereConditions.date_emission[Op.lte] = new Date(date_fin); } const validSortFields = ['numero_ticket', 'date_emission', 'montant_ttc', 'statut']; const sortField = validSortFields.includes(sort_by) ? sort_by : 'date_emission'; const sortOrder = ['ASC', 'DESC'].includes(sort_order.toUpperCase()) ? sort_order.toUpperCase() : 'DESC'; const { count, rows } = await Ticket.findAndCountAll({ where: whereConditions, include: [ { model: Client, attributes: ['id', 'nom', 'prenom', 'email', 'telephone'], required: false }, { model: Utilisateur, attributes: ['id', 'nom', 'prenom'], required: true }, { model: Commande, attributes: ['id', 'numero_commande'], required: true }, { model: TicketItem, attributes: ['id', 'nom_item', 'quantite', 'montant_ttc'], required: false } ], order: [[sortField, sortOrder]], limit: parseInt(limit), offset: offset, distinct: true }); res.json({ success: true, data: { tickets: rows, pagination: { currentPage: parseInt(page), totalPages: Math.ceil(count / parseInt(limit)), totalItems: count, itemsPerPage: parseInt(limit) } } }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la récupération des tickets', error: error.message }); } } // Obtenir un ticket par ID async getTicketById(req, res) { try { const { id } = req.params; const ticket = await Ticket.findByPk(id, { include: [ { model: Client, required: false }, { model: Utilisateur, attributes: ['id', 'nom', 'prenom', 'email'] }, { model: Commande, include: [{ model: CommandeItem, include: [{ model: Menu, attributes: ['nom', 'description'] }] }] }, { model: TicketItem, required: false } ] }); if (!ticket) { return res.status(404).json({ success: false, message: 'Ticket non trouvé' }); } res.json({ success: true, data: ticket }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la récupération du ticket', error: error.message }); } } // Créer un ticket depuis une commande async createTicketFromOrder(req, res) { const transaction = await sequelize.transaction(); try { const { commande_id, client_id, utilisateur_id, mode_paiement = 'especes', taux_tva = 20, remise = 0, notes } = req.body; // Vérifier que la commande existe const commande = await Commande.findByPk(commande_id, { include: [{ model: CommandeItem, include: [{ model: Menu }] }], transaction }); if (!commande) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Commande non trouvée' }); } if (commande.CommandeItems.length === 0) { await transaction.rollback(); return res.status(400).json({ success: false, message: 'La commande ne contient aucun item' }); } // Générer le numéro de ticket const numero_ticket = await this.generateTicketNumber(); // Calculer les montants const amounts = this.calculateAmounts( commande.CommandeItems.map(item => ({ prix_unitaire_ttc: parseFloat(item.prix_unitaire), quantite: item.quantite, remise_unitaire: 0 })), taux_tva, remise ); // Récupérer les données client si fourni let donnees_client = null; if (client_id) { const client = await Client.findByPk(client_id, { transaction }); if (client) { donnees_client = { nom: client.nom, prenom: client.prenom, email: client.email, telephone: client.telephone, adresse: client.adresse }; } } // Créer le ticket const ticket = await Ticket.create({ numero_ticket, commande_id, client_id, utilisateur_id, montant_ht: amounts.montant_ht, montant_tva: amounts.montant_tva, montant_ttc: amounts.montant_ttc, remise, taux_tva, mode_paiement, statut: 'emis', date_emission: new Date(), notes, donnees_client }, { transaction }); // Créer les items du ticket const ticketItems = await Promise.all( commande.CommandeItems.map(async (item) => { const prix_unitaire_ht = parseFloat(item.prix_unitaire) / (1 + taux_tva / 100); const montant_ht = prix_unitaire_ht * item.quantite; const montant_tva = montant_ht * (taux_tva / 100); const montant_ttc = montant_ht + montant_tva; return TicketItem.create({ ticket_id: ticket.id, commande_item_id: item.id, nom_item: item.Menu ? item.Menu.nom : `Item ${item.id}`, description: item.notes, quantite: item.quantite, prix_unitaire_ht, prix_unitaire_ttc: parseFloat(item.prix_unitaire), montant_ht, montant_tva, montant_ttc, taux_tva, remise_unitaire: 0 }, { transaction }); }) ); await transaction.commit(); // Récupérer le ticket complet const ticketComplet = await Ticket.findByPk(ticket.id, { include: [ { model: Client }, { model: Utilisateur, attributes: ['nom', 'prenom'] }, { model: Commande }, { model: TicketItem } ] }); res.status(201).json({ success: true, message: 'Ticket créé avec succès', data: ticketComplet }); } catch (error) { await transaction.rollback(); res.status(500).json({ success: false, message: 'Erreur lors de la création du ticket', error: error.message }); } } // Mettre à jour le statut d'un ticket async updateTicketStatus(req, res) { try { const { id } = req.params; const { statut, date_paiement, notes } = req.body; const ticket = await Ticket.findByPk(id); if (!ticket) { return res.status(404).json({ success: false, message: 'Ticket non trouvé' }); } const updateData = { statut }; if (statut === 'paye' && date_paiement) { updateData.date_paiement = new Date(date_paiement); } if (notes !== undefined) { updateData.notes = notes; } await ticket.update(updateData); res.json({ success: true, message: 'Statut du ticket mis à jour avec succès', data: ticket }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la mise à jour du statut', error: error.message }); } } // Obtenir les statistiques des tickets async getTicketStats(req, res) { try { const { date_debut, date_fin } = req.query; const whereConditions = {}; if (date_debut || date_fin) { whereConditions.date_emission = {}; if (date_debut) whereConditions.date_emission[Op.gte] = new Date(date_debut); if (date_fin) whereConditions.date_emission[Op.lte] = new Date(date_fin); } const [ total, emis, payes, annules, totalRevenue, payedRevenue ] = await Promise.all([ Ticket.count({ where: whereConditions }), Ticket.count({ where: { ...whereConditions, statut: 'emis' } }), Ticket.count({ where: { ...whereConditions, statut: 'paye' } }), Ticket.count({ where: { ...whereConditions, statut: 'annule' } }), Ticket.sum('montant_ttc', { where: whereConditions }), Ticket.sum('montant_ttc', { where: { ...whereConditions, statut: 'paye' } }) ]); // Statistiques par mode de paiement const paymentStats = await Ticket.findAll({ attributes: [ 'mode_paiement', [sequelize.fn('COUNT', sequelize.col('id')), 'count'], [sequelize.fn('SUM', sequelize.col('montant_ttc')), 'total'] ], where: { ...whereConditions, statut: 'paye' }, group: ['mode_paiement'] }); res.json({ success: true, data: { total, emis, payes, annules, totalRevenue: parseFloat(totalRevenue || 0), payedRevenue: parseFloat(payedRevenue || 0), paymentMethods: paymentStats.map(stat => ({ mode: stat.mode_paiement, count: parseInt(stat.dataValues.count), total: parseFloat(stat.dataValues.total || 0) })) } }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la récupération des statistiques', error: error.message }); } } // Générer un PDF pour un ticket async generatePDF(req, res) { try { const { id } = req.params; const ticket = await Ticket.findByPk(id, { include: [ { model: Client }, { model: Utilisateur, attributes: ['nom', 'prenom'] }, { model: TicketItem } ] }); if (!ticket) { return res.status(404).json({ success: false, message: 'Ticket non trouvé' }); } // Créer le dossier s'il n'existe pas const pdfDir = path.join(__dirname, '../uploads/tickets'); await fs.mkdir(pdfDir, { recursive: true }); const pdfPath = path.join(pdfDir, `ticket_${ticket.numero_ticket}.pdf`); // Créer le document PDF const doc = new PDFDocument(); doc.pipe(fs.createWriteStream(pdfPath)); // En-tête doc.fontSize(20).text('TICKET DE CAISSE', { align: 'center' }); doc.moveDown(); doc.fontSize(12).text(`Numéro: ${ticket.numero_ticket}`, { align: 'left' }); doc.text(`Date: ${ticket.date_emission.toLocaleDateString('fr-FR')}`, { align: 'left' }); doc.text(`Serveur: ${ticket.Utilisateur.nom} ${ticket.Utilisateur.prenom}`, { align: 'left' }); if (ticket.Client) { doc.text(`Client: ${ticket.Client.nom} ${ticket.Client.prenom}`, { align: 'left' }); } doc.moveDown(); // Détail des items doc.text('DÉTAIL:', { underline: true }); doc.moveDown(0.5); ticket.TicketItems.forEach(item => { doc.text(`${item.nom_item} x${item.quantite}`, { continued: true }); doc.text(`${parseFloat(item.montant_ttc).toFixed(2)}€`, { align: 'right' }); }); doc.moveDown(); // Totaux doc.text(`Montant HT: ${parseFloat(ticket.montant_ht).toFixed(2)}€`, { align: 'right' }); doc.text(`TVA (${ticket.taux_tva}%): ${parseFloat(ticket.montant_tva).toFixed(2)}€`, { align: 'right' }); if (ticket.remise > 0) { doc.text(`Remise: ${parseFloat(ticket.remise).toFixed(2)}€`, { align: 'right' }); } doc.fontSize(14).text(`TOTAL TTC: ${parseFloat(ticket.montant_ttc).toFixed(2)}€`, { align: 'right' }); doc.moveDown(); doc.fontSize(12).text(`Mode de paiement: ${ticket.mode_paiement.toUpperCase()}`, { align: 'left' }); doc.text(`Statut: ${ticket.statut.toUpperCase()}`, { align: 'left' }); if (ticket.notes) { doc.moveDown(); doc.text(`Notes: ${ticket.notes}`, { align: 'left' }); } // Pied de page doc.moveDown(2); doc.fontSize(10).text('Merci de votre visite !', { align: 'center' }); doc.text('À bientôt dans notre restaurant', { align: 'center' }); doc.end(); // Mettre à jour le chemin du PDF dans la base await ticket.update({ facture_pdf: `tickets/ticket_${ticket.numero_ticket}.pdf` }); res.json({ success: true, message: 'PDF généré avec succès', data: { pdf_path: `/uploads/tickets/ticket_${ticket.numero_ticket}.pdf`, ticket_id: ticket.id } }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la génération du PDF', error: error.message }); } } // Supprimer un ticket async deleteTicket(req, res) { try { const { id } = req.params; const ticket = await Ticket.findByPk(id); if (!ticket) { return res.status(404).json({ success: false, message: 'Ticket non trouvé' }); } // Vérifier si le ticket peut être supprimé if (ticket.statut === 'paye') { return res.status(400).json({ success: false, message: 'Impossible de supprimer un ticket payé. Vous pouvez l\'annuler.' }); } // Supprimer le fichier PDF s'il existe if (ticket.facture_pdf) { const pdfPath = path.join(__dirname, '../uploads/', ticket.facture_pdf); try { await fs.unlink(pdfPath); } catch (err) { console.log('PDF file not found or already deleted'); } } await ticket.destroy(); res.json({ success: true, message: 'Ticket supprimé avec succès' }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la suppression du ticket', error: error.message }); } } // Dupliquer un ticket async duplicateTicket(req, res) { const transaction = await sequelize.transaction(); try { const { id } = req.params; const { utilisateur_id, notes } = req.body; const originalTicket = await Ticket.findByPk(id, { include: [{ model: TicketItem }], transaction }); if (!originalTicket) { await transaction.rollback(); return res.status(404).json({ success: false, message: 'Ticket original non trouvé' }); } // Générer un nouveau numéro de ticket const numero_ticket = await this.generateTicketNumber(); // Créer le nouveau ticket const newTicket = await Ticket.create({ numero_ticket, commande_id: originalTicket.commande_id, client_id: originalTicket.client_id, utilisateur_id: utilisateur_id || originalTicket.utilisateur_id, montant_ht: originalTicket.montant_ht, montant_tva: originalTicket.montant_tva, montant_ttc: originalTicket.montant_ttc, remise: originalTicket.remise, taux_tva: originalTicket.taux_tva, mode_paiement: originalTicket.mode_paiement, statut: 'brouillon', date_emission: new Date(), notes: notes || `Copie du ticket ${originalTicket.numero_ticket}`, donnees_client: originalTicket.donnees_client }, { transaction }); // Dupliquer les items const newItems = await Promise.all( originalTicket.TicketItems.map(item => TicketItem.create({ ticket_id: newTicket.id, commande_item_id: item.commande_item_id, nom_item: item.nom_item, description: item.description, quantite: item.quantite, prix_unitaire_ht: item.prix_unitaire_ht, prix_unitaire_ttc: item.prix_unitaire_ttc, montant_ht: item.montant_ht, montant_tva: item.montant_tva, montant_ttc: item.montant_ttc, taux_tva: item.taux_tva, remise_unitaire: item.remise_unitaire }, { transaction }) ) ); await transaction.commit(); // Récupérer le ticket complet const ticketComplet = await Ticket.findByPk(newTicket.id, { include: [ { model: Client }, { model: Utilisateur, attributes: ['nom', 'prenom'] }, { model: TicketItem } ] }); res.status(201).json({ success: true, message: 'Ticket dupliqué avec succès', data: ticketComplet }); } catch (error) { await transaction.rollback(); res.status(500).json({ success: false, message: 'Erreur lors de la duplication du ticket', error: error.message }); } } // Recherche avancée de tickets async searchTickets(req, res) { try { const { numero_ticket, client_nom, montant_min, montant_max, date_debut, date_fin, statut, mode_paiement, limit = 50 } = req.query; const whereConditions = {}; const clientWhereConditions = {}; if (numero_ticket) { whereConditions.numero_ticket = { [Op.like]: `%${numero_ticket}%` }; } if (client_nom) { clientWhereConditions[Op.or] = [ { nom: { [Op.like]: `%${client_nom}%` } }, { prenom: { [Op.like]: `%${client_nom}%` } } ]; } if (montant_min || montant_max) { whereConditions.montant_ttc = {}; if (montant_min) whereConditions.montant_ttc[Op.gte] = parseFloat(montant_min); if (montant_max) whereConditions.montant_ttc[Op.lte] = parseFloat(montant_max); } if (date_debut || date_fin) { whereConditions.date_emission = {}; if (date_debut) whereConditions.date_emission[Op.gte] = new Date(date_debut); if (date_fin) whereConditions.date_emission[Op.lte] = new Date(date_fin); } if (statut) whereConditions.statut = statut; if (mode_paiement) whereConditions.mode_paiement = mode_paiement; const tickets = await Ticket.findAll({ where: whereConditions, include: [ { model: Client, where: Object.keys(clientWhereConditions).length > 0 ? clientWhereConditions : undefined, required: false, attributes: ['id', 'nom', 'prenom', 'email'] }, { model: Utilisateur, attributes: ['id', 'nom', 'prenom'] } ], order: [['date_emission', 'DESC']], limit: parseInt(limit) }); res.json({ success: true, data: { tickets, count: tickets.length } }); } catch (error) { res.status(500).json({ success: false, message: 'Erreur lors de la recherche', error: error.message }); } } } module.exports = new TicketController();