| <!DOCTYPE html> |
| <html lang="fr"> |
| <head> |
| <meta charset="UTF-8"> |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> |
| <title>Gestionnaire de Cartes NAFTAL</title> |
| <script src="https://cdn.tailwindcss.com"></script> |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
| <style> |
| .card-item:hover { |
| transform: translateY(-2px); |
| box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1); |
| } |
| .flip-in { |
| animation: flipIn 0.5s ease-out; |
| } |
| @keyframes flipIn { |
| from { opacity: 0; transform: rotateX(90deg); } |
| to { opacity: 1; transform: rotateX(0); } |
| } |
| .transaction-table { |
| border-collapse: collapse; |
| width: 100%; |
| } |
| .transaction-table th, .transaction-table td { |
| padding: 8px 12px; |
| border-bottom: 1px solid #e5e7eb; |
| } |
| .transaction-table tr:last-child td { |
| border-bottom: none; |
| } |
| </style> |
| </head> |
| <body class="bg-gray-50 min-h-screen"> |
| <div class="container mx-auto px-4 py-8"> |
| <div class="max-w-4xl mx-auto"> |
| |
| <header class="text-center mb-8"> |
| <h1 class="text-3xl font-bold text-indigo-700 mb-2"> |
| ⛽ Gestionnaire de Cartes NAFTAL |
| </h1> |
| <p class="text-gray-600">Suivi des cartes et des dépenses par véhicule/conducteur</p> |
| </header> |
|
|
| |
| <section class="bg-white rounded-xl shadow-sm p-6 mb-8 border border-gray-200"> |
| <h2 class="text-xl font-semibold text-gray-800 mb-4 flex items-center"> |
| <i class="fas fa-plus-circle mr-2 text-indigo-500"></i>Nouvelle carte |
| </h2> |
| <form id="cardForm" class="space-y-4"> |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label for="cardName" class="block text-sm font-medium text-gray-700 mb-1">Nom du titulaire</label> |
| <input type="text" id="cardName" required |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" |
| placeholder="Camion 12 / Jean Dupont"> |
| </div> |
| <div> |
| <label for="cardType" class="block text-sm font-medium text-gray-700 mb-1">Type de carte</label> |
| <select id="cardType" required |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"> |
| <option value="">Sélectionner...</option> |
| <option value="NAFTAL">Carte NAFTAL</option> |
| <option value="NAFTAL_Pro">Carte NAFTAL Pro</option> |
| <option value="NAFTAL_Fleet">Carte NAFTAL Flotte</option> |
| </select> |
| </div> |
| </div> |
| |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-4"> |
| <div> |
| <label for="cardNumber" class="block text-sm font-medium text-gray-700 mb-1">Numéro de carte</label> |
| <input type="text" id="cardNumber" required |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" |
| placeholder="1234567890"> |
| </div> |
| <div> |
| <label for="fuelType" class="block text-sm font-medium text-gray-700 mb-1">Catégorie</label> |
| <select id="fuelType" required |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"> |
| <option value="">Sélectionner...</option> |
| <option value="SANS PLOMB">SANS PLOMB</option> |
| <option value="DIESEL">DIESEL</option> |
| <option value="GPL">GPL</option> |
| <option value="PRODUITS NAFTAL">PRODUITS NAFTAL</option> |
| </select> |
| </div> |
| </div> |
|
|
| <div class="grid grid-cols-1 md:grid-cols-3 gap-4"> |
| <div> |
| <label for="initialAmount" class="block text-sm font-medium text-gray-700 mb-1">Solde initial (DA)</label> |
| <input type="number" step="0.01" id="initialAmount" value="0" |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"> |
| </div> |
| <div> |
| <label for="cardLimit" class="block text-sm font-medium text-gray-700 mb-1">Plafond mensuel (DA)</label> |
| <input type="number" step="1" id="cardLimit" |
| class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition" |
| placeholder="Optionnel"> |
| </div> |
| <div class="flex items-end"> |
| <button type="submit" |
| class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 px-4 rounded-lg transition duration-300 flex items-center justify-center h-[42px]"> |
| <i class="fas fa-save mr-2"></i> Enregistrer |
| </button> |
| </div> |
| </div> |
| </form> |
| </section> |
|
|
| |
| <section class="bg-white rounded-xl shadow-sm p-6 border border-gray-200"> |
| <div class="flex flex-col md:flex-row md:justify-between md:items-center mb-4 gap-4"> |
| <h2 class="text-xl font-semibold text-gray-800 flex items-center"> |
| <i class="fas fa-list-alt mr-2 text-indigo-500"></i>Cartes enregistrées |
| </h2> |
| <div class="flex flex-col sm:flex-row gap-2"> |
| <div class="relative flex-grow"> |
| <input type="text" id="searchInput" placeholder="Rechercher..." |
| class="w-full pl-10 pr-4 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition"> |
| <i class="fas fa-search absolute left-3 top-3 text-gray-400"></i> |
| </div> |
| <button onclick="exportToExcel()" class="bg-green-600 hover:bg-green-700 text-white py-2 px-4 rounded-lg transition flex items-center justify-center"> |
| <i class="fas fa-file-excel mr-2"></i> Export |
| </button> |
| </div> |
| </div> |
| |
| <div id="cardList" class="space-y-3"> |
| |
| <div class="text-center py-12 text-gray-500" id="emptyState"> |
| <i class="fas fa-credit-card text-4xl mb-3 text-gray-300"></i> |
| <p>Aucune carte enregistrée</p> |
| <p class="text-sm mt-2">Commencez par ajouter une carte ci-dessus</p> |
| </div> |
| </div> |
| </section> |
| </div> |
| </div> |
|
|
| <script> |
| |
| let cards = JSON.parse(localStorage.getItem('naftalCards')) || []; |
| |
| |
| document.addEventListener('DOMContentLoaded', () => { |
| renderCards(); |
| |
| |
| document.getElementById('cardForm').addEventListener('submit', (e) => { |
| e.preventDefault(); |
| saveCard(); |
| }); |
| }); |
| |
| |
| function saveCard() { |
| const cardName = document.getElementById('cardName').value.trim(); |
| const cardType = document.getElementById('cardType').value; |
| const cardNumber = document.getElementById('cardNumber').value.trim(); |
| const fuelType = document.getElementById('fuelType').value; |
| const initialAmount = parseFloat(document.getElementById('initialAmount').value) || 0; |
| const cardLimit = document.getElementById('cardLimit').value ? |
| parseInt(document.getElementById('cardLimit').value) : null; |
| |
| |
| if (!cardName || !cardType || !cardNumber || !fuelType) { |
| alert("Veuillez remplir tous les champs obligatoires"); |
| return; |
| } |
| |
| |
| const newCard = { |
| id: Date.now(), |
| name: cardName, |
| type: cardType, |
| number: cardNumber, |
| fuelType: fuelType, |
| limit: cardLimit, |
| transactions: initialAmount > 0 ? [{ |
| amount: initialAmount, |
| date: new Date().toISOString().split('T')[0], |
| description: "Solde initial", |
| timestamp: new Date().toLocaleString() |
| }] : [], |
| createdAt: new Date().toLocaleString(), |
| updatedAt: new Date().toLocaleString() |
| }; |
| |
| cards.push(newCard); |
| saveToLocalStorage(); |
| renderCards(); |
| resetForm(); |
| } |
| |
| |
| function renderCards(filteredCards = null) { |
| const cardList = document.getElementById('cardList'); |
| const cardsToRender = filteredCards || cards; |
| |
| if (cardsToRender.length === 0) { |
| cardList.innerHTML = document.getElementById('emptyState').outerHTML; |
| return; |
| } |
| |
| cardList.innerHTML = ''; |
| |
| cardsToRender.forEach(card => { |
| const totalSpent = calculateTotal(card.transactions); |
| const cardBg = getCardStyle(card.type); |
| |
| const cardElement = document.createElement('div'); |
| cardElement.className = 'card-item bg-white border border-gray-200 rounded-lg p-4 transition duration-300 flip-in hover:border-indigo-200'; |
| cardElement.innerHTML = ` |
| <div class="flex justify-between items-start mb-2"> |
| <div> |
| <h3 class="font-semibold text-gray-800">${card.name}</h3> |
| <p class="text-xs text-gray-500">${card.type} • ${card.fuelType}</p> |
| </div> |
| <span class="px-2 py-1 text-xs rounded-full ${cardBg.badge}"> |
| ${card.number} |
| </span> |
| </div> |
| |
| <div class="flex items-center justify-between mt-3"> |
| <div> |
| <p class="text-sm text-gray-600">Dépenses totales</p> |
| <p class="text-lg font-bold ${totalSpent > (card.limit || Infinity) ? 'text-red-600' : 'text-gray-800'}"> |
| ${totalSpent.toFixed(2)} DA |
| ${card.limit ? `<span class="text-xs font-normal text-gray-500">/ ${card.limit} DA</span>` : ''} |
| </p> |
| </div> |
| |
| <div class="flex space-x-2"> |
| <button onclick="showCardDetails('${card.id}')" |
| class="text-indigo-600 hover:text-indigo-800 transition p-2 rounded-full hover:bg-indigo-50"> |
| <i class="fas fa-eye"></i> |
| </button> |
| <button onclick="addTransaction('${card.id}')" |
| class="text-green-600 hover:text-green-800 transition p-2 rounded-full hover:bg-green-50"> |
| <i class="fas fa-plus"></i> |
| </button> |
| <button onclick="deleteCard('${card.id}')" |
| class="text-red-600 hover:text-red-800 transition p-2 rounded-full hover:bg-red-50"> |
| <i class="fas fa-trash-alt"></i> |
| </button> |
| </div> |
| </div> |
| `; |
| |
| cardList.appendChild(cardElement); |
| }); |
| } |
| |
| |
| function showCardDetails(id) { |
| const card = cards.find(c => c.id === parseInt(id)); |
| if (!card) return; |
| |
| const totalSpent = calculateTotal(card.transactions); |
| const limitPercentage = card.limit ? Math.min(100, (totalSpent / card.limit) * 100) : 0; |
| |
| const modal = document.createElement('div'); |
| modal.className = 'fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4'; |
| modal.innerHTML = ` |
| <div class="bg-white rounded-xl shadow-xl max-w-2xl w-full max-h-[90vh] overflow-hidden flex flex-col"> |
| <div class="p-6 border-b border-gray-200"> |
| <div class="flex justify-between items-start"> |
| <div> |
| <h3 class="text-xl font-bold text-gray-800">${card.name}</h3> |
| <p class="text-gray-600">${card.type} • ${card.fuelType} • ${card.number}</p> |
| </div> |
| <button onclick="this.parentElement.parentElement.parentElement.remove()" |
| class="text-gray-500 hover:text-gray-700 p-1"> |
| <i class="fas fa-times"></i> |
| </button> |
| </div> |
| |
| ${card.limit ? ` |
| <div class="mt-4"> |
| <div class="flex justify-between text-sm mb-1"> |
| <span>Utilisation du plafond</span> |
| <span>${totalSpent.toFixed(2)} DA / ${card.limit} DA</span> |
| </div> |
| <div class="w-full bg-gray-200 rounded-full h-2.5"> |
| <div class="bg-${totalSpent > card.limit ? 'red' : 'indigo'}-600 h-2.5 rounded-full" |
| style="width: ${limitPercentage}%"></div> |
| </div> |
| </div> |
| ` : ''} |
| </div> |
| |
| <div class="flex-1 overflow-y-auto p-6"> |
| <h4 class="font-semibold text-gray-800 mb-3">Historique des transactions</h4> |
| |
| <table class="transaction-table w-full text-sm"> |
| <thead> |
| <tr class="text-left text-gray-500 border-b"> |
| <th class="pb-2">Date</th> |
| <th class="pb-2">Description</th> |
| <th class="pb-2 text-right">Montant (DA)</th> |
| </tr> |
| </thead> |
| <tbody> |
| ${card.transactions.length > 0 ? |
| card.transactions.map(t => ` |
| <tr> |
| <td class="py-3">${t.date}</td> |
| <td>${t.description || 'Recharge carburant'}</td> |
| <td class="text-right font-mono">${t.amount.toFixed(2)}</td> |
| </tr> |
| `).join('') : ` |
| <tr> |
| <td colspan="3" class="text-center py-8 text-gray-400"> |
| Aucune transaction enregistrée |
| </td> |
| </tr> |
| `} |
| </tbody> |
| <tfoot class="border-t"> |
| <tr class="font-semibold"> |
| <td class="pt-3" colspan="2">Total dépensé</td> |
| <td class="text-right font-mono pt-3">${totalSpent.toFixed(2)} DA</td> |
| </tr> |
| </tfoot> |
| </table> |
| </div> |
| |
| <div class="p-4 border-t border-gray-200 bg-gray-50 flex justify-between"> |
| <button onclick="addTransaction('${card.id}')" |
| class="px-4 py-2 bg-indigo-600 text-white rounded-lg hover:bg-indigo-700 transition"> |
| <i class="fas fa-plus mr-2"></i>Nouvelle transaction |
| </button> |
| <div class="text-sm text-gray-500"> |
| Créée le ${card.createdAt} |
| </div> |
| </div> |
| </div> |
| `; |
| |
| document.body.appendChild(modal); |
| } |
| |
| |
| function addTransaction(cardId) { |
| const card = cards.find(c => c.id === parseInt(cardId)); |
| if (!card) return; |
| |
| const amount = parseFloat(prompt("Montant de la transaction (DA) :")); |
| if (!amount || isNaN(amount)) return; |
| |
| const description = prompt("Description (facultatif) :") || "Recharge carburant"; |
| |
| card.transactions.push({ |
| amount: amount, |
| date: new Date().toISOString().split('T')[0], |
| description: description, |
| timestamp: new Date().toLocaleString() |
| }); |
| |
| card.updatedAt = new Date().toLocaleString(); |
| saveToLocalStorage(); |
| renderCards(); |
| |
| |
| const openModal = document.querySelector('.fixed.inset-0'); |
| if (openModal) openModal.remove(); |
| showCardDetails(cardId); |
| } |
| |
| |
| function deleteCard(id) { |
| if (confirm('Supprimer définitivement cette carte et son historique ?')) { |
| cards = cards.filter(card => card.id !== parseInt(id)); |
| saveToLocalStorage(); |
| renderCards(); |
| } |
| } |
| |
| |
| function filterCards() { |
| const searchTerm = document.getElementById('searchInput').value.toLowerCase(); |
| const filteredCards = cards.filter(card => |
| card.name.toLowerCase().includes(searchTerm) || |
| card.number.toLowerCase().includes(searchTerm) || |
| card.type.toLowerCase().includes(searchTerm) |
| ); |
| renderCards(filteredCards); |
| } |
| |
| |
| function exportToExcel() { |
| if (cards.length === 0) { |
| alert("Aucune carte à exporter"); |
| return; |
| } |
| |
| |
| let csvContent = "Nom,Type de carte,Numéro,Catégorie,Plafond,Total dépensé,Nb transactions,Dernière transaction,Créée le\n"; |
| |
| |
| cards.forEach(card => { |
| const lastTransaction = card.transactions[card.transactions.length - 1]; |
| csvContent += `"${card.name}","${card.type}","${card.number}","${card.fuelType}",` + |
| `"${card.limit || 'Illimité'}","${calculateTotal(card.transactions).toFixed(2)}",` + |
| `"${card.transactions.length}","${lastTransaction ? lastTransaction.date : '-'}",` + |
| `"${card.createdAt}"\n`; |
| }); |
| |
| |
| const blob = new Blob(["\uFEFF" + csvContent], { type: 'text/csv;charset=utf-8;' }); |
| const link = document.createElement('a'); |
| link.href = URL.createObjectURL(blob); |
| link.download = `cartes_naftal_${new Date().toISOString().split('T')[0]}.csv`; |
| document.body.appendChild(link); |
| link.click(); |
| document.body.removeChild(link); |
| } |
| |
| |
| function getCardStyle(type) { |
| const styles = { |
| 'NAFTAL': { badge: 'bg-blue-100 text-blue-800' }, |
| 'NAFTAL_Pro': { badge: 'bg-green-100 text-green-800' }, |
| 'NAFTAL_Fleet': { badge: 'bg-purple-100 text-purple-800' }, |
| 'default': { badge: 'bg-gray-100 text-gray-800' } |
| }; |
| return styles[type] || styles.default; |
| } |
| |
| |
| function calculateTotal(transactions) { |
| return transactions.reduce((sum, t) => sum + t.amount, 0); |
| } |
| |
| |
| function saveToLocalStorage() { |
| localStorage.setItem('naftalCards', JSON.stringify(cards)); |
| } |
| |
| |
| function resetForm() { |
| document.getElementById('cardForm').reset(); |
| document.getElementById('initialAmount').value = "0"; |
| |
| |
| const submitBtn = document.querySelector('#cardForm button[type="submit"]'); |
| submitBtn.innerHTML = '<i class="fas fa-check mr-2"></i> Enregistré !'; |
| submitBtn.classList.replace('bg-indigo-600', 'bg-green-500'); |
| submitBtn.classList.replace('hover:bg-indigo-700', 'hover:bg-green-600'); |
| |
| setTimeout(() => { |
| submitBtn.innerHTML = '<i class="fas fa-save mr-2"></i> Enregistrer'; |
| submitBtn.classList.replace('bg-green-500', 'bg-indigo-600'); |
| submitBtn.classList.replace('hover:bg-green-600', 'hover:bg-indigo-700'); |
| }, 2000); |
| } |
| |
| |
| document.getElementById('searchInput').addEventListener('input', filterCards); |
| </script> |
| <p style="border-radius: 8px; text-align: center; font-size: 12px; color: #fff; margin-top: 16px;position: fixed; left: 8px; bottom: 8px; z-index: 10; background: rgba(0, 0, 0, 0.8); padding: 4px 8px;">Made with <img src="https://enzostvs-deepsite.hf.space/logo.svg" alt="DeepSite Logo" style="width: 16px; height: 16px; vertical-align: middle;display:inline-block;margin-right:3px;filter:brightness(0) invert(1);"><a href="https://enzostvs-deepsite.hf.space" style="color: #fff;text-decoration: underline;" target="_blank" >DeepSite</a> - 🧬 <a href="https://enzostvs-deepsite.hf.space?remix=Tamime/my-space" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body> |
| </html> |