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.
 

760 lines
27 KiB

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();