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.
 
 
 
 
 
 

1702 lines
63 KiB

<!-- Content Wrapper. Contains page content -->
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<?php
$session = session();
$users = $session->get('user');
$isAdmin = isset($users['group_name']) && in_array($users['group_name'], ['SuperAdmin', 'Direction', 'DAF']);
$isCommerciale = isset($users['group_name']) && in_array($users['group_name'], ['COMMERCIALE']);
$isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Caissière']);
?>
<div class="content-wrapper">
<section class="content-header">
<h1>
Gérer les
<small>Avances</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Accueil</a></li>
<li class="active">Avances</li>
</ol>
</section>
<section class="content">
<div id="messages"></div>
<?php if (in_array('createAvance', $user_permission)): ?>
<button class="btn btn-primary" data-toggle="modal" data-target="#createModal">AJOUTER UNE AVANCE</button>
<br /><br />
<?php endif; ?>
<!-- ✅ CORRECTION : Onglets avec badge pour caissière -->
<div class="row">
<?php if ($isCaissier): ?>
<div class="col-md-3">
<button id="avance_pending" class="btn btn-warning w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-clock-o me-2"></i>
<span class="badge badge-light" id="pending-count">0</span>
En attente validation
</button>
</div>
<?php endif; ?>
<div class="col-md-3">
<button id="avance_no_order" class="btn btn-info w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-hourglass-half me-2"></i> Avances Incomplètes
</button>
</div>
<div class="col-md-3">
<button id="avance_order" class="btn btn-success w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-check-circle me-2"></i> Avances Complètes
</button>
</div>
<div class="col-md-3">
<button id="avance_expired" class="btn btn-outline-danger w-100 rounded-pill shadow-sm py-2 fw-bold border-2">
<i class="fa fa-exclamation-circle me-2"></i> Avances Expirées
</button>
</div>
</div>
<br>
<!-- Bouton de vérification des avances expirées -->
<?php if ($isAdmin): ?>
<div class="row mb-3">
<div class="col-md-12 text-right">
<button type="button" class="btn btn-warning" onclick="processExpiredAvances()" id="btnProcessExpired">
<i class="fa fa-exclamation-triangle"></i> Vérification des avances expirées
</button>
</div>
</div>
<?php endif; ?>
<div class="box">
<div class="box-header">
<h3 class="box-title" id="table-title">Liste des avances</h3>
</div>
<div class="box-body">
<table id="avanceTable" class="table table-bordered table-striped">
<thead>
<?php if ($isAdmin): ?>
<tr>
<th>Client</th>
<th>Téléphone</th>
<th>Adresse</th>
<th>Produit</th>
<th>Prix</th>
<th>Avance</th>
<th>Reste à payer</th>
<th>Date</th>
<?php if (in_array('updateAvance', $user_permission) || in_array('deleteAvance', $user_permission)): ?>
<th>Action</th>
<?php endif;?>
</tr>
<?php endif;?>
<?php if ($isCommerciale || $isCaissier): ?>
<tr>
<th>#</th>
<th>Produit</th>
<th>Avance</th>
<th>Reste à payer</th>
<th>Date</th>
<?php if (in_array('updateAvance', $user_permission) || in_array('deleteAvance', $user_permission) || in_array('viewAvance', $user_permission)): ?>
<th>Action</th>
<?php endif;?>
</tr>
<?php endif;?>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</section>
</div>
<!-- Modal Création -->
<?php if (in_array('createAvance', $user_permission)): ?>
<div class="modal fade" id="createModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<form id="create_avance_form">
<div class="modal-header">
<h4 class="modal-title">Ajouter une avance</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="row">
<!-- Type d'avance -->
<div class="form-group col-md-6">
<label for="type_avance" class="form-label">Type d'avance</label>
<select name="type_avance" id="type_avance" class="form-control" required>
<option value="" disabled selected>Sélectionnez un type d'avance</option>
<option value="terre">Avance sur terre</option>
<option value="mere">Avance sur mer</option>
</select>
</div>
<!-- Moyen de paiement -->
<div class="form-group col-md-6">
<label for="type_payment" class="form-label">Moyen de paiement</label>
<select class="form-control" id="type_payment" name="type_payment">
<option value="" disabled selected>Sélectionnez un moyen de paiement</option>
<option value="MVOLA">MVOLA</option>
<option value="Virement Bancaire">Virement Bancaire</option>
<option value="En espèce">En espèce</option>
</select>
</div>
<!-- Nom client -->
<div class="form-group col-md-6">
<label>Nom du client</label>
<input type="text" name="customer_name_avance" id="customer_name_avance" class="form-control" required>
</div>
<!-- Téléphone client -->
<div class="form-group col-md-6">
<label>Téléphone du client</label>
<input type="text" name="customer_phone_avance" id="customer_phone_avance" class="form-control" required>
</div>
<!-- Adresse client -->
<div class="form-group col-md-6">
<label>Adresse du client</label>
<input type="text" name="customer_address_avance" id="customer_address_avance" class="form-control" required>
</div>
<!-- CIN client -->
<div class="form-group col-md-6">
<label>CIN du client</label>
<input type="text" name="customer_cin_avance" id="customer_cin_avance" class="form-control" required>
</div>
</div>
<div class="row">
<!-- Produit avec sélection (pour "terre") -->
<div class="form-group col-md-6" id="product_select_container">
<label for="id_product" class="form-label">Produit</label>
<select name="id_product" id="id_product" class="form-control" onchange="getProductDataCreate()">
<option value="">Sélectionnez un produit</option>
<?php foreach($products as $p): ?>
<option value="<?= $p['id'] ?>" <?= $p['product_sold'] ? 'disabled' : '' ?>>
<?= esc($p['name']) ?>|<?= esc($p['sku']) ?> <?= $p['product_sold'] ? '(Rupture)' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Produit avec texte libre (pour "mère") -->
<div class="form-group col-md-6" id="product_text_container" style="display:none;">
<label>Produit (à compléter)</label>
<input type="text" name="product_name_text" id="product_name_text" class="form-control" placeholder="Entrez le nom du produit">
</div>
<!-- Prix du produit -->
<div class="form-group col-md-6">
<label>Prix du produit</label>
<input type="text" name="gross_amount" id="gross_amount" class="form-control" placeholder="Entrez le prix">
</div>
</div>
<div class="row">
<!-- Avance -->
<div class="form-group col-md-6">
<label>Avance</label>
<input type="text" name="avance_amount" id="avance_amount" class="form-control" placeholder="Entrez l'avance">
</div>
<!-- Reste à payer -->
<div class="form-group col-md-6">
<label>Reste à payer</label>
<input type="text" name="amount_due" id="amount_due" class="form-control" readonly>
</div>
</div>
<!-- Commentaire (affiché uniquement pour "mère") -->
<div class="row">
<div class="form-group col-md-12" id="commentaire_container" style="display:none;">
<label>Commentaire</label>
<textarea name="commentaire" id="commentaire" class="form-control" rows="3"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-primary">Enregistrer</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Modal Modification -->
<?php if (in_array('updateAvance', $user_permission)): ?>
<div class="modal fade" id="updateModal" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<form id="update_avance_form" method="post">
<input type="hidden" name="id" id="avance_id_edit">
<div class="modal-header">
<h4 class="modal-title">Modifier une avance</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body">
<div class="row">
<!-- Type d'avance -->
<div class="form-group col-md-6">
<label for="type_avance_edit" class="form-label">Type d'avance</label>
<select name="type_avance_edit" id="type_avance_edit" class="form-control" required>
<option value="" disabled>Sélectionnez un type d'avance</option>
<option value="terre">Avance sur terre</option>
<option value="mere">Avance sur mer</option>
</select>
</div>
<!-- Moyen de paiement -->
<div class="form-group col-md-6">
<label for="type_payment_edit" class="form-label">Moyen de paiement</label>
<select class="form-control" id="type_payment_edit" name="type_payment_edit">
<option value="" disabled>Sélectionnez un moyen de paiement</option>
<option value="MVOLA">MVOLA</option>
<option value="Virement Bancaire">Virement Bancaire</option>
<option value="En espèce">En espèce</option>
</select>
</div>
<!-- Nom client -->
<div class="form-group col-md-6">
<label>Nom du client</label>
<input type="text" name="customer_name_avance_edit" id="customer_name_avance_edit" class="form-control" required>
</div>
<!-- Téléphone client -->
<div class="form-group col-md-6">
<label>Téléphone du client</label>
<input type="text" name="customer_phone_avance_edit" id="customer_phone_avance_edit" class="form-control" required>
</div>
<!-- Adresse client -->
<div class="form-group col-md-6">
<label>Adresse du client</label>
<input type="text" name="customer_address_avance_edit" id="customer_address_avance_edit" class="form-control" required>
</div>
<!-- CIN client -->
<div class="form-group col-md-6">
<label>CIN du client</label>
<input type="text" name="customer_cin_avance_edit" id="customer_cin_avance_edit" class="form-control" required>
</div>
</div>
<div class="row">
<!-- Produit avec sélection (pour "terre") -->
<div class="form-group col-md-6" id="product_select_container_edit">
<label class="form_label">Produit</label>
<select name="id_product_edit" id="id_product_edit" class="form-control" onchange="getProductDataUpdate()">
<option value="">Sélectionnez un produit</option>
<?php foreach($products as $p): ?>
<option value="<?= $p['id'] ?>" <?= $p['product_sold'] ? 'disabled' : '' ?>>
<?= esc($p['sku']) ?> | <?= esc($p['name']) ?> | <?= esc($p['numero_de_moteur']) ?> | <?= esc($p['puissance']) ?>
<?= $p['product_sold'] ? ' (Rupture)' : '' ?>
</option>
<?php endforeach; ?>
</select>
</div>
<!-- Produit avec texte libre (pour "mère") -->
<div class="form-group col-md-6" id="product_text_container_edit" style="display:none;">
<label>Produit (à compléter)</label>
<input type="text" name="product_name_text_edit" id="product_name_text_edit" class="form-control" placeholder="Entrez le nom du produit">
</div>
<!-- Prix du produit -->
<div class="form-group col-md-6">
<label>Prix du produit</label>
<input type="text" name="gross_amount_edit" id="gross_amount_edit" class="form-control" placeholder="Entrez le prix">
</div>
</div>
<div class="row">
<!-- Avance -->
<div class="form-group col-md-6">
<label>Avance</label>
<input type="text" name="avance_amount_edit" id="avance_amount_edit" class="form-control" placeholder="Entrez l'avance">
</div>
<!-- Reste à payer -->
<div class="form-group col-md-6">
<label>Reste à payer</label>
<input type="text" name="amount_due_edit" id="amount_due_edit" class="form-control" readonly>
</div>
</div>
<!-- Commentaire (affiché uniquement pour "mère") -->
<div class="row">
<div class="form-group col-md-12" id="commentaire_container_edit" style="display:none;">
<label>Commentaire</label>
<textarea name="commentaire_edit" id="commentaire_edit" class="form-control" rows="3"></textarea>
</div>
</div>
</div>
<div class="modal-footer">
<button type="submit" class="btn btn-success">Modifier</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button>
</div>
</form>
</div>
</div>
</div>
<?php endif;?>
<!-- Modal de Prévisualisation pour Caissière et Direction -->
<?php if (in_array('viewAvance', $user_permission)): ?>
<div class="modal fade" id="viewModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title">Prévisualisation de la Facture</h4>
<button type="button" class="close" data-dismiss="modal">&times;</button>
</div>
<div class="modal-body" id="invoice-preview-container">
<!-- Le contenu de la facture sera injecté ici -->
</div>
<div class="modal-footer">
<!-- ✅ Le bouton est MASQUÉ par défaut et sera affiché par JavaScript si l'utilisateur est caissière -->
<button type="button"
class="btn btn-primary"
onclick="printFromModal()"
id="btnPrintInvoice"
style="display:none;">
<i class="fa fa-print"></i> Imprimer
</button>
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>
<?php endif; ?>
<?php if (in_array('deleteAvance', $user_permission)): ?>
<!-- remove brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="removeModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<h4 class="modal-title">Supprimer cette avance</h4>
</div>
<form role="form" action="<?php echo base_url('avances/deleteAvance') ?>" method="post" id="removeForm">
<input type="hidden" name="avance_id" value="">
<input type="hidden" name="product_id" value="">
<div class="modal-body">
<p>Voulez-vous vraiment supprimer ?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Oui</button>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
var base_url = "<?= base_url() ?>", brutCreate = 0, brutEdit = 0;
// =====================================================
// 🔥 FONCTIONS DE FORMATAGE DES NOMBRES
// =====================================================
function formatNumber(number) {
if (!number && number !== 0) return '';
var numStr = number.toString().replace(/\s/g, '');
if (!/^\d+$/.test(numStr)) return number;
return numStr.replace(/\B(?=(\d{3})+(?!\d))/g, " ");
}
function unformatNumber(formattedNumber) {
if (!formattedNumber) return 0;
return parseFloat(formattedNumber.toString().replace(/\s/g, '')) || 0;
}
function handleNumberFormat(input) {
var cursorPosition = input.selectionStart;
var originalLength = input.value.length;
var rawValue = input.value.replace(/\s/g, '');
if (rawValue === '' || /^\d+$/.test(rawValue)) {
var formattedValue = formatNumber(rawValue);
input.value = formattedValue;
var newLength = formattedValue.length;
var lengthDiff = newLength - originalLength;
var newCursorPosition = cursorPosition + lengthDiff;
input.setSelectionRange(newCursorPosition, newCursorPosition);
}
}
function rebuildTableHeaders(headers) {
var thead = '<thead><tr>';
headers.forEach(function(header) {
thead += '<th>' + header + '</th>';
});
thead += '</tr></thead>';
$('#avanceTable').html(thead + '<tbody></tbody>');
}
// =====================================================
// 🔥 DOCUMENT READY
// =====================================================
$(document).ready(function() {
$('#avance_menu').addClass("active");
$('.select2').select2();
// Configuration langue française DataTable
var datatableLangFr = {
lengthMenu: "Afficher _MENU_ enregistrements par page",
zeroRecords: "Aucun résultat trouvé",
info: "Affichage de _START_ à _END_ sur _TOTAL_ enregistrements",
infoEmpty: "Aucun enregistrement disponible",
infoFiltered: "(filtré depuis _MAX_ enregistrements au total)",
search: "Rechercher :",
paginate: {
first: "Premier",
last: "Dernier",
next: "Suivant",
previous: "Précédent"
}
};
// Fonction pour initialiser la DataTable
function initAvanceTable(url, columns) {
if ($.fn.DataTable.isDataTable('#avanceTable')) {
$('#avanceTable').DataTable().destroy();
}
return $('#avanceTable').DataTable({
ajax: url,
columns: columns,
language: datatableLangFr
});
}
// Fonction de rafraîchissement
function refreshDataTable() {
if (typeof manageTable !== 'undefined' && manageTable) {
manageTable.ajax.reload(null, false);
}
}
// Variables de rôles
var isCaissier = <?php echo json_encode($isCaissier ?? false); ?>;
var isCommerciale = <?php echo json_encode($isCommerciale ?? false); ?>;
var isAdmin = <?php echo json_encode($isAdmin ?? false); ?>;
// =====================================================
// 🔥 INITIALISATION DES COLONNES SELON LE RÔLE
// =====================================================
<?php if ($isAdmin): ?>
// Colonnes pour ADMIN
var adminColumns = [
{ title: "Client" },
{ title: "Téléphone" },
{ title: "Adresse" },
{ title: "Produit" },
{
title: "Prix",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{
title: "Avance",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{
title: "Reste à payer",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{ title: "Date" }
<?php if (in_array('updateAvance', $user_permission) || in_array('deleteAvance', $user_permission)): ?>
,{ title: "Action", orderable: false, searchable: false }
<?php endif; ?>
];
var manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns);
$('#avance_no_order').on('click', function() {
$('#table-title').text('Avances Incomplètes');
manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns);
});
$('#avance_order').on('click', function() {
$('#table-title').text('Avances Complètes');
manageTable = initAvanceTable(base_url + 'avances/fetchAvanceBecameOrder', adminColumns);
});
$('#avance_expired').on('click', function() {
$('#table-title').text('Avances Expirées');
manageTable = initAvanceTable(base_url + 'avances/fetchExpiredAvance', adminColumns);
});
<?php endif; ?>
<?php if ($isCommerciale || $isCaissier): ?>
// Colonnes pour COMMERCIAL/CAISSIÈRE
var userColumns = [
{ title: "#" },
{ title: "Produit" },
{
title: "Avance",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{
title: "Reste à payer",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{ title: "Date" }
<?php if (in_array('updateAvance', $user_permission) || in_array('deleteAvance', $user_permission) || in_array('viewAvance', $user_permission)): ?>
,{ title: "Action", orderable: false, searchable: false }
<?php endif; ?>
];
var manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', userColumns);
$('#avance_no_order').on('click', function() {
console.log('🔍 Clic sur Avances Incomplètes');
$('#table-title').text('Avances Incomplètes');
// Détruire la table
if ($.fn.DataTable.isDataTable('#avanceTable')) {
$('#avanceTable').DataTable().destroy();
}
// ✅ Reconstruire les headers (format caissière/commercial)
rebuildTableHeaders([
'#',
'Produit',
'Avance',
'Reste à payer',
'Date',
'Action'
]);
// Initialiser
manageTable = $('#avanceTable').DataTable({
ajax: {
url: base_url + 'avances/fetchAvanceData',
type: 'GET',
dataSrc: 'data'
},
columns: userColumns,
language: datatableLangFr
});
console.log('✅ DataTable Incomplètes initialisée');
});
$('#avance_order').on('click', function() {
console.log('🔍 Clic sur Avances Complètes');
$('#table-title').text('Avances Complètes');
if ($.fn.DataTable.isDataTable('#avanceTable')) {
$('#avanceTable').DataTable().destroy();
}
rebuildTableHeaders([
'#',
'Produit',
'Avance',
'Reste à payer',
'Date',
'Action'
]);
manageTable = $('#avanceTable').DataTable({
ajax: {
url: base_url + 'avances/fetchAvanceBecameOrder',
type: 'GET',
dataSrc: 'data'
},
columns: userColumns,
language: datatableLangFr
});
console.log('✅ DataTable Complètes initialisée');
});
$('#avance_expired').on('click', function() {
console.log('🔍 Clic sur Avances Expirées');
$('#table-title').text('Avances Expirées');
if ($.fn.DataTable.isDataTable('#avanceTable')) {
$('#avanceTable').DataTable().destroy();
}
rebuildTableHeaders([
'#',
'Produit',
'Avance',
'Reste à payer',
'Date',
'Action'
]);
manageTable = $('#avanceTable').DataTable({
ajax: {
url: base_url + 'avances/fetchExpiredAvance',
type: 'GET',
dataSrc: 'data'
},
columns: userColumns,
language: datatableLangFr
});
console.log('✅ DataTable Expirées initialisée');
});
// Charger le compteur d'avances en attente
loadPendingCount();
setInterval(loadPendingCount, 30000);
console.log('✅ Module caissière initialisé');
<?php endif; ?>
// =====================================================
// 🔥 ONGLET "EN ATTENTE VALIDATION" (CAISSIÈRE UNIQUEMENT)
// =====================================================
<?php if ($isCaissier): ?>
var pendingColumns = [
{ title: "#" },
{ title: "Client" },
{ title: "Téléphone" },
{ title: "Produit" },
{
title: "Prix",
render: function(data) { return formatNumber(data); }
},
{
title: "Avance",
render: function(data) { return formatNumber(data); }
},
{ title: "Date" },
{ title: "Action", orderable: false, searchable: false }
];
// ✅ CORRECTION COMPLÈTE : Reconstruire le tableau avant d'initialiser DataTables
$('#avance_pending').on('click', function() {
console.log('🔍 Clic sur bouton En attente validation');
$('#table-title').text('Avances en attente de validation');
// ✅ 1. Détruire complètement la DataTable existante
if ($.fn.DataTable.isDataTable('#avanceTable')) {
$('#avanceTable').DataTable().destroy();
}
// ✅ 2. Reconstruire les headers du tableau HTML
rebuildTableHeaders([
'#',
'Client',
'Téléphone',
'Produit',
'Prix',
'Avance',
'Date',
'Action'
]);
// ✅ 3. Initialiser la nouvelle DataTable
manageTable = $('#avanceTable').DataTable({
ajax: {
url: base_url + 'avances/fetchPendingValidation',
type: 'POST',
dataSrc: 'data',
error: function(xhr, error, code) {
console.error('❌ Erreur AJAX:', error);
console.error('❌ Statut HTTP:', xhr.status);
console.error('❌ Réponse:', xhr.responseText);
Swal.fire({
icon: 'error',
title: 'Erreur de chargement',
html: '<p>Impossible de charger les avances en attente</p>' +
'<p><strong>Erreur :</strong> ' + error + '</p>' +
'<p><strong>Code HTTP :</strong> ' + xhr.status + '</p>',
confirmButtonText: 'OK'
});
}
},
columns: pendingColumns,
language: datatableLangFr,
order: [[0, 'desc']]
});
console.log('✅ DataTable "En attente validation" initialisée');
});
// Charger le compteur d'avances en attente
loadPendingCount();
setInterval(loadPendingCount, 30000); // Actualiser toutes les 30 secondes
<?php endif; ?>
// =====================================================
// 🔥 MODAL CRÉATION - INITIALISATION
// =====================================================
$('#createModal').on('show.bs.modal', function() {
$('#create_avance_form')[0].reset();
// Par défaut : mode "terre"
$('#product_select_container').show();
$('#id_product').prop('required', true);
$('#product_text_container').hide();
$('#product_name_text').prop('required', false);
$('#gross_amount').prop('readonly', true).prop('type', 'text');
$('#commentaire_container').hide();
// Réattacher les événements
$('#avance_amount').off('keyup').on('keyup', updateDueCreate);
});
// =====================================================
// 🔥 ÉVÉNEMENTS DE FORMATAGE EN TEMPS RÉEL
// =====================================================
$('#avance_amount').on('input', function() {
handleNumberFormat(this);
updateDueCreate();
});
$('#gross_amount').on('input', function() {
if ($('#type_avance').val() === 'mere') {
handleNumberFormat(this);
updateDueCreate();
}
});
$('#avance_amount_edit').on('input', function() {
handleNumberFormat(this);
updateDueEdit();
});
$('#gross_amount_edit').on('input', function() {
if ($('#type_avance_edit').val() === 'mere') {
handleNumberFormat(this);
updateDueEdit();
}
});
// =====================================================
// 🔥 GESTION DU TYPE D'AVANCE - CRÉATION
// =====================================================
$('#type_avance').on('change', function() {
var typeAvance = $(this).val();
if (typeAvance === 'mere') {
$('#product_select_container').hide();
$('#id_product').prop('required', false).val('');
$('#product_text_container').show();
$('#product_name_text').prop('required', true);
$('#gross_amount').prop('readonly', false).val('').prop('required', true);
$('#commentaire_container').show();
$('#avance_amount, #gross_amount').off('keyup').on('keyup', function() {
var prix = unformatNumber($('#gross_amount').val());
var avance = unformatNumber($('#avance_amount').val());
var reste = prix - avance;
$('#amount_due').val(formatNumber(reste >= 0 ? reste.toFixed(0) : 0));
});
} else if (typeAvance === 'terre') {
$('#product_select_container').show();
$('#id_product').prop('required', true);
$('#product_text_container').hide();
$('#product_name_text').prop('required', false).val('');
$('#gross_amount').prop('readonly', true).prop('required', false);
$('#commentaire_container').hide();
$('#commentaire').val('');
$('#avance_amount').off('keyup').on('keyup', updateDueCreate);
$('#gross_amount').off('keyup');
}
$('#gross_amount').val('');
$('#avance_amount').val('');
$('#amount_due').val('');
});
// =====================================================
// 🔥 GESTION DU TYPE D'AVANCE - ÉDITION
// =====================================================
$('#type_avance_edit').on('change', function() {
var typeAvance = $(this).val();
if (typeAvance === 'mere') {
$('#product_select_container_edit').hide();
$('#id_product_edit').prop('required', false).val('');
$('#product_text_container_edit').show();
$('#product_name_text_edit').prop('required', true);
$('#gross_amount_edit').prop('readonly', false).prop('required', true);
$('#commentaire_container_edit').show();
$('#avance_amount_edit, #gross_amount_edit').off('keyup').on('keyup', function() {
var prix = unformatNumber($('#gross_amount_edit').val());
var avance = unformatNumber($('#avance_amount_edit').val());
var reste = prix - avance;
$('#amount_due_edit').val(formatNumber(reste >= 0 ? reste.toFixed(0) : 0));
});
} else if (typeAvance === 'terre') {
$('#product_select_container_edit').show();
$('#id_product_edit').prop('required', true);
$('#product_text_container_edit').hide();
$('#product_name_text_edit').prop('required', false).val('');
$('#gross_amount_edit').prop('readonly', true).prop('required', false);
$('#commentaire_container_edit').hide();
$('#commentaire_edit').val('');
$('#avance_amount_edit').off('keyup').on('keyup', updateDueEdit);
$('#gross_amount_edit').off('keyup');
}
});
// =====================================================
// 🔥 SOUMISSION FORMULAIRE CRÉATION
// =====================================================
$('#create_avance_form').on('submit', function(e) {
e.preventDefault();
const $form = $(this);
var brut = unformatNumber($('#gross_amount').val());
var avance = unformatNumber($('#avance_amount').val());
var typeAvance = $('#type_avance').val();
if (typeAvance === 'terre') {
var minAvance = brut * 0.5;
if (avance < minAvance) {
Swal.fire({
icon: 'error',
title: 'Avance insuffisante',
html: `L'avance doit être au minimum de <b>50%</b> du prix du produit (<b>${formatNumber(minAvance.toFixed(0))} Ar</b>).`,
confirmButtonText: 'OK',
confirmButtonColor: '#d33'
});
return;
}
}
if (typeAvance === 'mere') {
if (!$('#product_name_text').val() || brut === 0 || avance === 0) {
Swal.fire({
icon: 'warning',
title: 'Champs manquants',
text: 'Veuillez remplir tous les champs : produit, prix et avance.'
});
return;
}
}
$form.find('button[type="submit"]').prop('disabled', true).text('Enregistrement...');
var formData = new FormData(this);
formData.set('gross_amount', brut);
formData.set('avance_amount', avance);
formData.set('amount_due', unformatNumber($('#amount_due').val()));
$.ajax({
url: base_url + 'avances/createAvance',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(res) {
if (res.success === true) {
Swal.fire({
icon: 'success',
title: 'Succès',
text: res.messages,
timer: 1500,
showConfirmButton: false
}).then(() => {
$("#createModal").modal('hide');
$form[0].reset();
location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: res.messages
});
}
},
error: function() {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Erreur de connexion'
});
},
complete: function() {
$form.find('button[type="submit"]').prop('disabled', false).text('Enregistrer');
}
});
});
// =====================================================
// 🔥 SUPPRESSION
// =====================================================
window.removeFunc = function(id, product_id) {
$('#removeModal').modal('show');
$('#removeForm input[name="avance_id"]').val(id);
$('#removeForm input[name="product_id"]').val(product_id || 0);
};
$('#removeForm').on('submit', function(e) {
e.preventDefault();
var form = $(this);
var submitButton = form.find('button[type="submit"]');
submitButton.prop('disabled', true).text('Suppression...');
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
dataType: 'json',
success: function(response) {
if (response.success === true) {
$('#removeModal').modal('hide');
$("#messages").html(`
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<strong><span class="glyphicon glyphicon-ok-sign"></span></strong> ${response.messages}
</div>
`);
refreshDataTable();
setTimeout(function() {
$("#messages .alert").fadeOut();
location.reload();
}, 3000);
} else {
$('#removeModal').modal('hide');
$("#messages").html(`
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<strong><span class="glyphicon glyphicon-exclamation-sign"></span></strong> ${response.messages}
</div>
`);
}
},
complete: function() {
submitButton.prop('disabled', false).text('Oui');
}
});
return false;
});
}); // FIN DOCUMENT READY
// =====================================================
// 🔥 FONCTIONS UTILITAIRES
// =====================================================
function getProductDataCreate() {
var id = $('#id_product').val();
if (!id) {
brutCreate = 0;
$('#gross_amount').val('');
$('#amount_due').val('');
$('#avance_amount').val('');
return;
}
$.post(base_url + 'orders/getProductValueById', { product_id: id }, function(r) {
brutCreate = parseFloat(r.prix_vente) || 0;
var prixFormate = formatNumber(brutCreate.toFixed(0));
var avance50 = brutCreate * 0.5;
var avanceFormatee = formatNumber(avance50.toFixed(0));
var resteFormate = formatNumber((brutCreate - avance50).toFixed(0));
$('#gross_amount').val(prixFormate);
$('#avance_amount').val(avanceFormatee);
$('#amount_due').val(resteFormate);
}, 'json').fail(function(xhr, status, error) {
console.error('❌ Erreur AJAX:', error);
});
}
function updateDueCreate() {
var av = unformatNumber($('#avance_amount').val());
var brutAmount = unformatNumber($('#gross_amount').val());
var reste = Math.max(brutAmount - av, 0);
$('#amount_due').val(formatNumber(reste.toFixed(0)));
}
function getProductDataUpdate() {
var id = $('#id_product_edit').val();
if (!id) {
brutEdit = 0;
$('#gross_amount_edit').val('');
$('#amount_due_edit').val('');
$('#avance_amount_edit').val('');
return;
}
$.ajax({
url: base_url + 'orders/getProductValueById',
type: 'POST',
data: { product_id: id },
dataType: 'json',
success: function(r) {
brutEdit = parseFloat(r.prix_vente) || 0;
var prixFormate = formatNumber(brutEdit.toFixed(0));
var avance50 = brutEdit * 0.5;
var avanceFormatee = formatNumber(avance50.toFixed(0));
var resteFormate = formatNumber((brutEdit - avance50).toFixed(0));
$('#gross_amount_edit').val(prixFormate);
$('#avance_amount_edit').val(avanceFormatee);
$('#amount_due_edit').val(resteFormate);
},
error: function(xhr, status, error) {
console.error('❌ Erreur AJAX (update):', error);
}
});
}
function updateDueEdit() {
var av = unformatNumber($('#avance_amount_edit').val());
var reste = Math.max(brutEdit - av, 0);
$('#amount_due_edit').val(formatNumber(reste.toFixed(0)));
}
// =====================================================
// 🔥 FONCTION MODIFICATION
// =====================================================
function editFunc(id) {
$('#update_avance_form')[0].reset();
$.ajax({
url: base_url + 'avances/fetchSingleAvance/' + id,
type: 'GET',
dataType: 'json',
success: function(r) {
populateEditModal(r, id);
},
error: function() {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'Impossible de récupérer les données de l\'avance'
});
}
});
}
function populateEditModal(r, id) {
$('#avance_id_edit').val(r.avance_id || r.id || id);
$('#customer_name_avance_edit').val(r.customer_name || '');
$('#customer_phone_avance_edit').val(r.customer_phone || '');
$('#customer_address_avance_edit').val(r.customer_address || r.customer_adress || '');
$('#customer_cin_avance_edit').val(r.customer_cin || '');
$('#type_avance_edit').val(r.type_avance || '');
$('#type_payment_edit').val(r.type_payment || '');
var grossAmount = r.gross_amount || r.product_price || 0;
$('#gross_amount_edit').val(formatNumber(grossAmount));
brutEdit = parseFloat(grossAmount);
$('#avance_amount_edit').val(formatNumber(r.avance_amount || ''));
$('#amount_due_edit').val(formatNumber(r.amount_due || ''));
$('#commentaire_edit').val(r.commentaire || '');
if (r.type_avance === 'mere') {
var productNameMer = r.product_name || '';
$('#product_name_text_edit').val(productNameMer);
} else if (r.type_avance === 'terre') {
var productId = r.product_id || r.id_product;
if (productId) {
setTimeout(function() {
$('#id_product_edit').val(productId);
if ($('#id_product_edit option:selected').val() != productId) {
var productNameFromDB = r.product_name_db || 'Produit #' + productId;
$('#id_product_edit').append(
$('<option></option>')
.attr('value', productId)
.attr('selected', 'selected')
.text(productNameFromDB + ' (Actuellement sélectionné)')
);
}
}, 100);
}
}
$('#updateModal').modal('show');
$('#updateModal').on('shown.bs.modal', function(e) {
$('#type_avance_edit').trigger('change');
$(this).off('shown.bs.modal');
});
$('#update_avance_form').off('submit').on('submit', function(e) {
e.preventDefault();
var $form = $(this);
var $submitBtn = $form.find('button[type="submit"]');
var typeAvance = $('#type_avance_edit').val();
var avance = unformatNumber($('#avance_amount_edit').val());
var brut = unformatNumber($('#gross_amount_edit').val());
if (typeAvance === 'terre') {
var minAvance = brutEdit * 0.5;
if (avance < minAvance) {
Swal.fire({
icon: 'error',
title: 'Avance insuffisante',
html: `L'avance doit être au minimum de <b>50%</b> du prix du produit (<b>${formatNumber(minAvance.toFixed(0))} Ar</b>).`,
confirmButtonText: 'OK',
confirmButtonColor: '#d33'
});
return;
}
}
if (typeAvance === 'mere') {
if (!$('#product_name_text_edit').val() || brut === 0 || avance === 0) {
Swal.fire({
icon: 'warning',
title: 'Champs manquants',
text: 'Veuillez remplir tous les champs : produit, prix et avance.'
});
return;
}
}
$submitBtn.prop('disabled', true).text('Modification...');
var formData = new FormData(this);
formData.set('gross_amount_edit', brut);
formData.set('avance_amount_edit', avance);
formData.set('amount_due_edit', unformatNumber($('#amount_due_edit').val()));
$.ajax({
url: base_url + 'avances/updateAvance',
type: 'POST',
data: formData,
processData: false,
contentType: false,
success: function(res) {
if (res.success === true) {
$('#updateModal').modal('hide');
if (res.converted === true && res.redirect_url) {
Swal.fire({
icon: 'success',
title: '🎉 Conversion automatique !',
html: '<div style="text-align: left; padding: 10px;">' +
'<p style="margin: 10px 0;"><strong>✅ Avance modifiée avec succès !</strong></p>' +
'<p style="margin: 10px 0;">🔄 L\'avance sur terre étant complète, elle a été <strong>automatiquement convertie en commande</strong>.</p>' +
'<hr style="margin: 15px 0; border: none; border-top: 1px solid #ddd;">' +
'<p style="margin: 10px 0;"><strong>N° Commande :</strong> ' + (res.bill_no || 'N/A') + '</p>' +
'<p style="margin: 15px 0 5px 0; color: #666; font-style: italic;">Vous allez être redirigé vers la commande...</p>' +
'</div>',
timer: 4000,
timerProgressBar: true,
showConfirmButton: false,
allowOutsideClick: false
}).then(() => {
window.location.href = res.redirect_url;
});
} else {
Swal.fire({
icon: 'success',
title: 'Succès',
text: res.messages,
timer: 2000,
showConfirmButton: false
}).then(() => {
location.reload();
});
}
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: res.messages
});
}
},
error: function(xhr, status, error) {
console.error('Erreur AJAX:', error);
Swal.fire({
icon: 'error',
title: 'Erreur de connexion',
text: 'Impossible de modifier l\'avance'
});
},
complete: function() {
$submitBtn.prop('disabled', false).text('Modifier');
}
});
});
}
// =====================================================
// 🔥 FONCTION VISUALISATION FACTURE
// =====================================================
window.viewFunc = function(avance_id) {
if (!avance_id) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'ID avance manquant'
});
return;
}
$.ajax({
url: base_url + 'avances/getInvoicePreview/' + avance_id,
type: 'GET',
dataType: 'json',
beforeSend: function() {
$('#invoice-preview-container').html('<div class="text-center"><i class="fa fa-spinner fa-spin fa-3x"></i><p>Chargement...</p></div>');
$('#viewModal').modal('show');
},
success: function(response) {
if (response.success) {
$('#invoice-preview-container').html(response.html);
$('#viewModal').data('avance-id', response.avance_id);
$('#viewModal').data('can-print', response.can_print);
if (response.can_print === true) {
$('#btnPrintInvoice').show();
} else {
$('#btnPrintInvoice').hide();
}
} else {
$('#invoice-preview-container').html('<div class="alert alert-danger">' + (response.messages || 'Erreur lors du chargement') + '</div>');
}
},
error: function(xhr, status, error) {
console.error('Erreur viewFunc:', error);
$('#invoice-preview-container').html('<div class="alert alert-danger">Erreur de connexion au serveur</div>');
}
});
};
// =====================================================
// 🔥 IMPRESSION DEPUIS MODAL
// =====================================================
window.printFromModal = function() {
var avanceId = $('#viewModal').data('avance-id');
var canPrint = $('#viewModal').data('can-print');
if (!avanceId) {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: 'ID de facture non trouvé'
});
return;
}
if (canPrint !== true) {
Swal.fire({
icon: 'warning',
title: 'Accès refusé',
text: 'Seule la caissière peut imprimer les factures'
});
return;
}
$.ajax({
url: base_url + 'avances/notifyPrintInvoice',
type: 'POST',
data: { avance_id: avanceId },
dataType: 'json',
success: function(response) {
if (response.success) {
printInvoiceContent(avanceId);
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: response.messages || 'Erreur lors de l\'enregistrement de l\'impression'
});
}
},
error: function() {
Swal.fire({
icon: 'error',
title: 'Erreur de connexion',
text: 'Impossible d\'enregistrer l\'impression'
});
}
});
};
function printInvoiceContent(avanceId) {
Swal.fire({
title: 'Préparation de l\'impression...',
allowOutsideClick: false,
allowEscapeKey: false,
showConfirmButton: false,
didOpen: () => {
Swal.showLoading();
}
});
$.ajax({
url: base_url + 'avances/getFullInvoiceForPrint/' + avanceId,
type: 'GET',
dataType: 'json',
success: function(response) {
if (response.success) {
Swal.close();
var printFrame = $('<iframe></iframe>')
.attr('id', 'print-frame')
.css({
position: 'absolute',
width: '0',
height: '0',
border: 'none',
visibility: 'hidden'
})
.appendTo('body');
var frameDoc = printFrame[0].contentWindow || printFrame[0].contentDocument;
if (frameDoc.document) frameDoc = frameDoc.document;
frameDoc.open();
frameDoc.write(response.html);
frameDoc.close();
setTimeout(function() {
try {
printFrame[0].contentWindow.focus();
printFrame[0].contentWindow.print();
$('#viewModal').modal('hide');
setTimeout(function() {
Swal.fire({
icon: 'success',
title: 'Impression réussie !',
html: '<p>La facture a été imprimée avec succès.</p>' +
'<p><strong>La Direction a été notifiée.</strong></p>',
confirmButtonText: 'OK',
confirmButtonColor: '#28a745',
allowOutsideClick: false
}).then(() => {
location.reload();
});
}, 500);
setTimeout(function() {
printFrame.remove();
}, 1000);
} catch (e) {
console.error('Erreur impression:', e);
Swal.fire({
icon: 'error',
title: 'Erreur d\'impression',
text: 'Une erreur est survenue lors de l\'impression'
});
printFrame.remove();
}
}, 500);
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: response.messages || 'Impossible de récupérer la facture'
});
}
},
error: function(xhr) {
Swal.fire({
icon: 'error',
title: 'Erreur de connexion',
text: 'Impossible de récupérer la facture pour impression'
});
}
});
}
$('#viewModal').on('hidden.bs.modal', function() {
$('#print-frame').remove();
});
// =====================================================
// 🔥 VALIDATION AVANCE (CAISSIÈRE)
// =====================================================
window.validateAvanceFunc = function(avance_id) {
Swal.fire({
title: 'Valider cette avance ?',
html: '<p>En validant cette avance :</p>' +
'<ul style="text-align: left; margin-left: 20px;">' +
'<li>✅ Le montant sera ajouté à la caisse</li>' +
'<li>✅ Le commercial sera notifié</li>' +
'<li>✅ L\'avance ne pourra plus être modifiée par le commercial</li>' +
'</ul>',
icon: 'question',
showCancelButton: true,
confirmButtonText: '<i class="fa fa-check"></i> Valider',
cancelButtonText: 'Annuler',
confirmButtonColor: '#28a745',
cancelButtonColor: '#6c757d'
}).then((result) => {
if (result.isConfirmed) {
$.ajax({
url: base_url + 'avances/validateAvance',
type: 'POST',
data: { avance_id: avance_id },
dataType: 'json',
success: function(response) {
if (response.success) {
Swal.fire({
icon: 'success',
title: 'Validée !',
text: response.messages,
timer: 2000,
showConfirmButton: false
}).then(() => {
location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: response.messages
});
}
},
error: function() {
Swal.fire({
icon: 'error',
title: 'Erreur de connexion',
text: 'Impossible de valider l\'avance'
});
}
});
}
});
};
// =====================================================
// 🔥 CHARGER COMPTEUR AVANCES EN ATTENTE
// =====================================================
function loadPendingCount() {
$.ajax({
url: base_url + 'avances/fetchPendingValidation',
type: 'POST', // ✅ CORRECTION
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#pending-count').text(count);
if (count > 0) {
$('#pending-count').removeClass('badge-light').addClass('badge-danger');
$('#avance_pending').addClass('btn-pulse');
} else {
$('#pending-count').removeClass('badge-danger').addClass('badge-light');
$('#avance_pending').removeClass('btn-pulse');
}
},
error: function() {
console.error('Erreur chargement compteur avances en attente');
}
});
}
// =====================================================
// 🔥 TRAITER AVANCES EXPIRÉES (ADMIN)
// =====================================================
function processExpiredAvances() {
var btn = $('#btnProcessExpired');
var originalText = btn.html();
Swal.fire({
title: 'Traiter les avances expirées ?',
html: '<div style="text-align: left;">' +
'<p>Cette action va automatiquement :</p>' +
'<ul style="margin-left: 20px;">' +
'<li>✅ Désactiver les avances dont la deadline est dépassée</li>' +
'<li>✅ Libérer les produits (remettre en stock pour "avances sur terre")</li>' +
'<li>✅ Les avances expirées apparaîtront dans l\'onglet "Avances Expirées"</li>' +
'</ul>' +
'<p style="margin-top: 15px;"><strong>Voulez-vous continuer ?</strong></p>' +
'</div>',
icon: 'question',
showCancelButton: true,
confirmButtonText: '<i class="fa fa-check"></i> Oui, traiter',
cancelButtonText: '<i class="fa fa-times"></i> Annuler',
confirmButtonColor: '#d33',
cancelButtonColor: '#6c757d',
width: '600px'
}).then((result) => {
if (result.isConfirmed) {
executeProcessExpired(btn, originalText);
}
});
}
function executeProcessExpired(btn, originalText) {
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Traitement en cours...');
$.ajax({
url: base_url + 'avances/processExpiredAvances',
type: 'POST',
dataType: 'json',
success: function(response) {
if (response.success) {
var message = '<strong>' + response.processed + ' avance(s) expirée(s) traitée(s)</strong>';
if (response.errors > 0) {
message += '<br><span class="text-warning">⚠️ ' + response.errors + ' erreur(s) rencontrée(s)</span>';
}
var detailsHtml = '';
if (response.details && response.details.length > 0) {
detailsHtml = '<div style="margin-top: 20px; max-height: 300px; overflow-y: auto;">' +
'<table class="table table-sm table-bordered" style="font-size: 12px;">' +
'<thead style="background-color: #f8f9fa;">' +
'<tr>' +
'<th>ID</th>' +
'<th>Client</th>' +
'<th>Deadline</th>' +
'<th>Produit libéré</th>' +
'</tr>' +
'</thead>' +
'<tbody>';
response.details.forEach(function(detail) {
detailsHtml += '<tr>' +
'<td><strong>#' + detail.avance_id + '</strong></td>' +
'<td>' + detail.customer + '</td>' +
'<td><span class="text-danger">' + detail.deadline + '</span></td>' +
'<td>' + (detail.product_freed ?
'<span class="text-success"><i class="fa fa-check-circle"></i> Oui (ID: ' + detail.product_id + ')</span>' :
'<span class="text-muted">-</span>') + '</td>' +
'</tr>';
});
detailsHtml += '</tbody></table></div>';
}
Swal.fire({
icon: 'success',
title: '✅ Traitement terminé !',
html: message + detailsHtml,
confirmButtonText: 'OK',
confirmButtonColor: '#28a745',
width: '700px'
}).then(() => {
location.reload();
});
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: response.messages || 'Une erreur est survenue lors du traitement'
});
}
},
error: function(xhr, status, error) {
console.error('Erreur AJAX:', error);
Swal.fire({
icon: 'error',
title: 'Erreur de connexion',
html: '<p>Impossible de traiter les avances expirées.</p>' +
'<p class="text-muted" style="font-size: 12px;">Vérifiez les logs du serveur pour plus de détails.</p>'
});
},
complete: function() {
btn.prop('disabled', false).html(originalText);
}
});
}
</script>
<style>
@keyframes pulse {
0% { box-shadow: 0 0 0 0 rgba(255, 193, 7, 0.7); }
70% { box-shadow: 0 0 0 10px rgba(255, 193, 7, 0); }
100% { box-shadow: 0 0 0 0 rgba(255, 193, 7, 0); }
}
.btn-pulse {
animation: pulse 2s infinite;
}
.badge-danger {
background-color: #dc3545 !important;
color: white;
font-weight: bold;
}
</style>