facturation / index.html
Flynf's picture
Add 2 files
05781ed verified
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Facturation Horaires</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>
.fade-in {
animation: fadeIn 0.3s ease-in-out;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
.invoice-paper {
background: linear-gradient(to bottom right, #fff, #f9f9f9);
box-shadow: 0 10px 30px rgba(0,0,0,0.1);
border: 1px solid #e5e7eb;
}
.watermark {
position: absolute;
opacity: 0.05;
font-size: 120px;
font-weight: bold;
color: #3b82f6;
transform: rotate(-30deg);
pointer-events: none;
user-select: none;
z-index: 0;
}
#clientList::-webkit-scrollbar {
width: 6px;
}
#clientList::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 10px;
}
#clientList::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 10px;
}
#clientList::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
.print-only {
display: none;
}
@media print {
body * {
visibility: hidden;
}
#invoice-print, #invoice-print * {
visibility: visible;
}
#invoice-print {
position: absolute;
left: 0;
top: 0;
width: 100%;
margin: 0;
padding: 20px;
box-shadow: none;
border: none;
}
.print-only {
display: block;
}
.no-print {
display: none !important;
}
.watermark {
opacity: 0.1;
}
}
</style>
</head>
<body class="bg-gray-50 min-h-screen">
<div class="container mx-auto px-4 py-8 no-print">
<header class="mb-10 text-center">
<h1 class="text-4xl font-bold text-blue-600 mb-2">Facturation Horaires</h1>
<p class="text-gray-600">Générez facilement des factures pour vos clients</p>
</header>
<div class="grid grid-cols-1 lg:grid-cols-3 gap-8">
<!-- Section Clients -->
<div class="lg:col-span-1 bg-white rounded-xl shadow-md p-6">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold text-gray-800">Clients</h2>
<button id="addClientBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fas fa-plus mr-2"></i> Ajouter
</button>
</div>
<!-- Liste des clients -->
<div id="clientList" class="space-y-3 max-h-96 overflow-y-auto pr-2">
<!-- Les clients seront ajoutés ici dynamiquement -->
<div class="text-center py-4 text-gray-500">
<i class="fas fa-users text-2xl mb-2"></i>
<p>Aucun client enregistré</p>
</div>
</div>
</div>
<!-- Section Facturation -->
<div class="lg:col-span-2 space-y-8">
<!-- Formulaire de facturation -->
<div class="bg-white rounded-xl shadow-md p-6">
<h2 class="text-2xl font-semibold text-gray-800 mb-6">Créer une facture</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label for="selectClient" class="block text-sm font-medium text-gray-700 mb-1">Client *</label>
<select id="selectClient" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
<option value="" selected disabled>Sélectionnez un client</option>
</select>
<p id="clientError" class="text-red-500 text-xs mt-1 hidden">Veuillez sélectionner un client</p>
</div>
<div>
<label for="hoursWorked" class="block text-sm font-medium text-gray-700 mb-1">Heures travaillées *</label>
<input type="number" id="hoursWorked" min="0" step="0.5" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Ex: 5.5">
<p id="hoursError" class="text-red-500 text-xs mt-1 hidden">Veuillez entrer un nombre valide</p>
</div>
<div>
<label for="workDate" class="block text-sm font-medium text-gray-700 mb-1">Date de travail *</label>
<input type="date" id="workDate" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
</div>
<div>
<label for="invoiceDate" class="block text-sm font-medium text-gray-700 mb-1">Date de facture *</label>
<input type="date" id="invoiceDate" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500">
</div>
</div>
<div class="mt-6">
<label for="workDescription" class="block text-sm font-medium text-gray-700 mb-1">Description du travail</label>
<textarea id="workDescription" rows="3" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Détaillez les travaux effectués..."></textarea>
</div>
<div class="mt-6 flex justify-end">
<button id="generateInvoiceBtn" class="bg-green-500 hover:bg-green-600 text-white px-6 py-2 rounded-lg flex items-center transition-colors">
<i class="fas fa-file-invoice-dollar mr-2"></i> Générer la facture
</button>
</div>
</div>
<!-- Aperçu de la facture -->
<div id="invoicePreviewContainer" class="hidden bg-white rounded-xl shadow-md p-6 fade-in">
<div class="flex justify-between items-center mb-6">
<h2 class="text-2xl font-semibold text-gray-800">Aperçu de la facture</h2>
<div class="flex space-x-2">
<button id="downloadPdfBtn" class="bg-gray-200 hover:bg-gray-300 text-gray-700 px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fas fa-file-pdf mr-2"></i> PDF
</button>
<button id="printInvoiceBtn" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg flex items-center transition-colors">
<i class="fas fa-print mr-2"></i> Imprimer
</button>
</div>
</div>
<div id="invoicePreview" class="invoice-paper p-8 rounded-lg relative overflow-hidden">
<div class="watermark">FACTURE</div>
<!-- Le contenu de la facture sera généré ici -->
</div>
</div>
</div>
</div>
</div>
<!-- Modal Ajout Client -->
<div id="clientModal" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 hidden">
<div class="bg-white rounded-xl shadow-xl p-6 w-full max-w-md fade-in">
<div class="flex justify-between items-center mb-4">
<h3 class="text-xl font-semibold text-gray-800">Ajouter un client</h3>
<button id="closeModalBtn" class="text-gray-500 hover:text-gray-700 transition-colors">
<i class="fas fa-times"></i>
</button>
</div>
<form id="clientForm" class="space-y-4">
<div>
<label for="clientName" class="block text-sm font-medium text-gray-700 mb-1">Nom complet *</label>
<input type="text" id="clientName" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="Jean Dupont">
<p id="nameError" class="text-red-500 text-xs mt-1 hidden">Ce champ est obligatoire</p>
</div>
<div>
<label for="clientEmail" class="block text-sm font-medium text-gray-700 mb-1">Email</label>
<input type="email" id="clientEmail" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="client@exemple.com">
</div>
<div>
<label for="clientPhone" class="block text-sm font-medium text-gray-700 mb-1">Téléphone</label>
<input type="tel" id="clientPhone" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="06 12 34 56 78">
</div>
<div>
<label for="clientAddress" class="block text-sm font-medium text-gray-700 mb-1">Adresse</label>
<textarea id="clientAddress" rows="2" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="123 Rue des Exemples, 75000 Paris"></textarea>
</div>
<div>
<label for="hourlyRate" class="block text-sm font-medium text-gray-700 mb-1">Taux horaire (€) *</label>
<input type="number" id="hourlyRate" min="0" step="0.01" required class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:ring-blue-500 focus:border-blue-500" placeholder="50.00">
<p id="rateError" class="text-red-500 text-xs mt-1 hidden">Veuillez entrer un taux valide</p>
</div>
<div class="pt-4 flex justify-end space-x-3">
<button type="button" id="cancelClientBtn" class="px-4 py-2 border border-gray-300 rounded-lg text-gray-700 hover:bg-gray-100 transition-colors">Annuler</button>
<button type="submit" class="bg-blue-500 hover:bg-blue-600 text-white px-4 py-2 rounded-lg transition-colors">Enregistrer</button>
</div>
</form>
</div>
</div>
<!-- Contenu pour l'impression -->
<div id="invoice-print" class="hidden"></div>
<script>
// Stockage des clients
let clients = JSON.parse(localStorage.getItem('clients')) || [];
// Éléments du DOM
const clientList = document.getElementById('clientList');
const selectClient = document.getElementById('selectClient');
const addClientBtn = document.getElementById('addClientBtn');
const clientModal = document.getElementById('clientModal');
const closeModalBtn = document.getElementById('closeModalBtn');
const cancelClientBtn = document.getElementById('cancelClientBtn');
const clientForm = document.getElementById('clientForm');
const generateInvoiceBtn = document.getElementById('generateInvoiceBtn');
const invoicePreviewContainer = document.getElementById('invoicePreviewContainer');
const invoicePreview = document.getElementById('invoicePreview');
const printInvoiceBtn = document.getElementById('printInvoiceBtn');
const downloadPdfBtn = document.getElementById('downloadPdfBtn');
const invoicePrint = document.getElementById('invoice-print');
// Messages d'erreur
const clientError = document.getElementById('clientError');
const hoursError = document.getElementById('hoursError');
const nameError = document.getElementById('nameError');
const rateError = document.getElementById('rateError');
// Initialisation
document.addEventListener('DOMContentLoaded', function() {
// Définir les dates par défaut
const today = new Date().toISOString().split('T')[0];
document.getElementById('workDate').value = today;
document.getElementById('invoiceDate').value = today;
// Charger les clients
loadClients();
// Événements
addClientBtn.addEventListener('click', showClientModal);
closeModalBtn.addEventListener('click', hideClientModal);
cancelClientBtn.addEventListener('click', hideClientModal);
clientForm.addEventListener('submit', saveClient);
generateInvoiceBtn.addEventListener('click', generateInvoice);
printInvoiceBtn.addEventListener('click', printInvoice);
downloadPdfBtn.addEventListener('click', downloadPdf);
// Empêcher la fermeture du modal en cliquant à l'extérieur
clientModal.addEventListener('click', function(e) {
if (e.target === clientModal) {
hideClientModal();
}
});
});
// Fonctions
function loadClients() {
clientList.innerHTML = '';
selectClient.innerHTML = '<option value="" selected disabled>Sélectionnez un client</option>';
if (clients.length === 0) {
clientList.innerHTML = `
<div class="text-center py-4 text-gray-500">
<i class="fas fa-users text-2xl mb-2"></i>
<p>Aucun client enregistré</p>
</div>
`;
return;
}
clients.forEach((client, index) => {
// Ajouter à la liste des clients
const clientElement = document.createElement('div');
clientElement.className = 'bg-gray-50 hover:bg-gray-100 p-4 rounded-lg cursor-pointer transition-colors';
clientElement.innerHTML = `
<div class="flex justify-between items-center">
<div>
<h3 class="font-medium text-gray-800">${client.name}</h3>
<p class="text-sm text-gray-500">${client.address ? client.address.split(',')[0] : ''}</p>
</div>
<div class="text-right">
<p class="font-semibold text-blue-600">${parseFloat(client.hourlyRate).toFixed(2)} €/h</p>
<button class="delete-client text-red-500 hover:text-red-700 text-sm" data-index="${index}">
<i class="fas fa-trash"></i>
</button>
</div>
</div>
`;
clientList.appendChild(clientElement);
// Ajouter au select
const option = document.createElement('option');
option.value = index;
option.textContent = `${client.name} (${parseFloat(client.hourlyRate).toFixed(2)} €/h)`;
selectClient.appendChild(option);
});
// Gérer la suppression des clients
document.querySelectorAll('.delete-client').forEach(btn => {
btn.addEventListener('click', function(e) {
e.stopPropagation();
const index = parseInt(this.getAttribute('data-index'));
deleteClient(index);
});
});
// Gérer la sélection d'un client dans la liste
document.querySelectorAll('#clientList > div').forEach((el, index) => {
if (index < clients.length) {
el.addEventListener('click', () => {
selectClient.value = index;
clientError.classList.add('hidden');
});
}
});
}
function showClientModal() {
clientModal.classList.remove('hidden');
document.getElementById('clientName').focus();
clientForm.reset();
nameError.classList.add('hidden');
rateError.classList.add('hidden');
}
function hideClientModal() {
clientModal.classList.add('hidden');
}
function saveClient(e) {
e.preventDefault();
const name = document.getElementById('clientName').value.trim();
const hourlyRate = document.getElementById('hourlyRate').value;
// Validation
let isValid = true;
if (!name) {
nameError.classList.remove('hidden');
isValid = false;
} else {
nameError.classList.add('hidden');
}
if (!hourlyRate || isNaN(hourlyRate) || parseFloat(hourlyRate) <= 0) {
rateError.classList.remove('hidden');
isValid = false;
} else {
rateError.classList.add('hidden');
}
if (!isValid) return;
const client = {
name: name,
email: document.getElementById('clientEmail').value.trim(),
phone: document.getElementById('clientPhone').value.trim(),
address: document.getElementById('clientAddress').value.trim(),
hourlyRate: parseFloat(hourlyRate).toFixed(2)
};
clients.push(client);
localStorage.setItem('clients', JSON.stringify(clients));
hideClientModal();
loadClients();
// Sélectionner le nouveau client
selectClient.value = clients.length - 1;
clientError.classList.add('hidden');
}
function deleteClient(index) {
if (confirm('Voulez-vous vraiment supprimer ce client ?')) {
clients.splice(index, 1);
localStorage.setItem('clients', JSON.stringify(clients));
loadClients();
// Masquer l'aperçu si le client supprimé était sélectionné
if (selectClient.value === index.toString()) {
selectClient.value = '';
invoicePreviewContainer.classList.add('hidden');
}
}
}
function generateInvoice() {
const clientIndex = selectClient.value;
const hoursWorked = parseFloat(document.getElementById('hoursWorked').value);
const workDate = document.getElementById('workDate').value;
const invoiceDate = document.getElementById('invoiceDate').value;
const workDescription = document.getElementById('workDescription').value.trim();
// Validation
let isValid = true;
if (clientIndex === '' || clientIndex === null) {
clientError.classList.remove('hidden');
isValid = false;
} else {
clientError.classList.add('hidden');
}
if (isNaN(hoursWorked) || hoursWorked <= 0) {
hoursError.classList.remove('hidden');
isValid = false;
} else {
hoursError.classList.add('hidden');
}
if (!workDate) {
alert('Veuillez sélectionner une date de travail');
return;
}
if (!invoiceDate) {
alert('Veuillez sélectionner une date de facture');
return;
}
if (!isValid) return;
const client = clients[clientIndex];
const totalAmount = (hoursWorked * parseFloat(client.hourlyRate)).toFixed(2);
// Formater les dates
const formattedWorkDate = formatDate(workDate);
const formattedInvoiceDate = formatDate(invoiceDate);
// Générer la facture
const invoiceContent = `
<div class="watermark">FACTURE</div>
<div class="flex justify-between items-start mb-12">
<div>
<h1 class="text-3xl font-bold text-blue-600 mb-1">FACTURE</h1>
<p class="text-gray-500">N° ${generateInvoiceNumber()}</p>
</div>
<div class="text-right">
<p class="text-gray-700">Date: <span class="font-medium">${formattedInvoiceDate}</span></p>
<p class="text-gray-700">Échéance: <span class="font-medium">${formattedInvoiceDate}</span></p>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 mb-12">
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-2">De:</h2>
<p class="font-bold">Votre Entreprise</p>
<p>123 Rue de l'Entreprise</p>
<p>75000 Paris, France</p>
<p class="mt-2">SIRET: 123 456 789 00010</p>
<p>TVA: FR12 345678901</p>
</div>
<div>
<h2 class="text-lg font-semibold text-gray-800 mb-2">À:</h2>
<p class="font-bold">${client.name}</p>
${client.address ? `<p>${client.address.replace(/\n/g, '<br>')}</p>` : ''}
${client.email ? `<p class="mt-2">Email: ${client.email}</p>` : ''}
${client.phone ? `<p>Téléphone: ${client.phone}</p>` : ''}
</div>
</div>
<div class="mb-8">
<h2 class="text-lg font-semibold text-gray-800 mb-4">Détails de la facture</h2>
<div class="overflow-x-auto">
<table class="w-full border-collapse">
<thead>
<tr class="bg-gray-100">
<th class="text-left py-3 px-4 font-semibold text-gray-700 border-b">Description</th>
<th class="text-right py-3 px-4 font-semibold text-gray-700 border-b">Taux horaire</th>
<th class="text-right py-3 px-4 font-semibold text-gray-700 border-b">Heures</th>
<th class="text-right py-3 px-4 font-semibold text-gray-700 border-b">Montant</th>
</tr>
</thead>
<tbody>
<tr>
<td class="py-3 px-4 border-b text-gray-700">
${workDescription || 'Prestation de services'}
<p class="text-sm text-gray-500 mt-1">Date: ${formattedWorkDate}</p>
</td>
<td class="py-3 px-4 border-b text-right text-gray-700">${parseFloat(client.hourlyRate).toFixed(2)} €</td>
<td class="py-3 px-4 border-b text-right text-gray-700">${hoursWorked.toFixed(1)}</td>
<td class="py-3 px-4 border-b text-right font-medium text-gray-800">${totalAmount} €</td>
</tr>
</tbody>
<tfoot>
<tr>
<td colspan="3" class="py-3 px-4 text-right font-semibold text-gray-700">Total HT</td>
<td class="py-3 px-4 text-right font-bold text-gray-800">${totalAmount} €</td>
</tr>
<tr>
<td colspan="3" class="py-3 px-4 text-right font-semibold text-gray-700">TVA (0%)</td>
<td class="py-3 px-4 text-right font-bold text-gray-800">0,00 €</td>
</tr>
<tr>
<td colspan="3" class="py-3 px-4 text-right font-semibold text-gray-700 border-t-2 border-gray-200">Total TTC</td>
<td class="py-3 px-4 text-right font-bold text-gray-800 border-t-2 border-gray-200">${totalAmount} €</td>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="text-sm text-gray-500 pt-8 border-t border-gray-200">
<p class="font-semibold mb-2">Conditions de paiement:</p>
<p>Paiement à réception de la facture par virement bancaire.</p>
<p class="mt-4">IBAN: FR76 1234 5678 9012 3456 7890 123</p>
<p>BIC: ABCDEFGH123</p>
<p class="mt-4">En cas de retard de paiement, des pénalités de 1,5% par mois seront appliquées.</p>
</div>
<div class="print-only mt-12 pt-4 border-t border-gray-200 text-center text-sm text-gray-500">
<p>Merci pour votre confiance</p>
</div>
`;
invoicePreview.innerHTML = invoiceContent;
invoicePrint.innerHTML = invoiceContent;
invoicePreviewContainer.classList.remove('hidden');
invoicePreviewContainer.scrollIntoView({ behavior: 'smooth' });
}
function printInvoice() {
window.print();
}
function downloadPdf() {
alert("Fonctionnalité PDF à implémenter. Pour l'instant, vous pouvez utiliser l'impression et sélectionner 'Enregistrer au format PDF' comme imprimante.");
}
function formatDate(dateString) {
if (!dateString) return '';
const options = { day: '2-digit', month: '2-digit', year: 'numeric' };
return new Date(dateString).toLocaleDateString('fr-FR', options);
}
function generateInvoiceNumber() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const random = Math.floor(Math.random() * 1000);
return `FACT-${year}${month}${day}-${random}`;
}
</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=Flynf/facturation" style="color: #fff;text-decoration: underline;" target="_blank" >Remix</a></p></body>
</html>