my-space / index.html
Tamime's picture
undefined - Initial Deployment
db26ad5 verified
<!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">
<!-- En-tête -->
<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>
<!-- Formulaire d'ajout -->
<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>
<!-- Liste des cartes -->
<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">
<!-- État vide -->
<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>
// Stockage des cartes
let cards = JSON.parse(localStorage.getItem('naftalCards')) || [];
// Initialisation
document.addEventListener('DOMContentLoaded', () => {
renderCards();
// Gestion du formulaire
document.getElementById('cardForm').addEventListener('submit', (e) => {
e.preventDefault();
saveCard();
});
});
// Sauvegarde d'une nouvelle carte
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;
// Validation
if (!cardName || !cardType || !cardNumber || !fuelType) {
alert("Veuillez remplir tous les champs obligatoires");
return;
}
// Création de la carte
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();
}
// Affichage des cartes
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);
});
}
// Détails d'une carte
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);
}
// Ajout d'une transaction
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();
// Ferme et rouvre les modals ouverts
const openModal = document.querySelector('.fixed.inset-0');
if (openModal) openModal.remove();
showCardDetails(cardId);
}
// Suppression d'une carte
function deleteCard(id) {
if (confirm('Supprimer définitivement cette carte et son historique ?')) {
cards = cards.filter(card => card.id !== parseInt(id));
saveToLocalStorage();
renderCards();
}
}
// Filtrage des cartes
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);
}
// Export Excel
function exportToExcel() {
if (cards.length === 0) {
alert("Aucune carte à exporter");
return;
}
// En-têtes
let csvContent = "Nom,Type de carte,Numéro,Catégorie,Plafond,Total dépensé,Nb transactions,Dernière transaction,Créée le\n";
// Données
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`;
});
// Téléchargement
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);
}
// Styles visuels par type de carte
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;
}
// Calcul du total des transactions
function calculateTotal(transactions) {
return transactions.reduce((sum, t) => sum + t.amount, 0);
}
// Sauvegarde locale
function saveToLocalStorage() {
localStorage.setItem('naftalCards', JSON.stringify(cards));
}
// Réinitialisation du formulaire
function resetForm() {
document.getElementById('cardForm').reset();
document.getElementById('initialAmount').value = "0";
// Animation de confirmation
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);
}
// Recherche en temps réel
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>