feat: add database and latest updates for deployment preparation - Add invoices.db temporarily for deployment - Update README with comprehensive documentation - Latest improvements to dashboard, preview, and static files - Ready for cloud deployment
This commit is contained in:
parent
ee48d0dbe6
commit
22d6330205
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# Gestionnaire de Factures
|
||||||
|
|
||||||
|
Application interne pour la gestion et la génération de factures.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
1. Installer les dépendances :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Installer Typst :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
brew install typst # macOS
|
||||||
|
```
|
||||||
|
|
||||||
|
## Utilisation
|
||||||
|
|
||||||
|
1. Démarrer le serveur :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python3 server.py
|
||||||
|
```
|
||||||
|
|
||||||
|
2. Accéder à l'application :
|
||||||
|
|
||||||
|
- http://localhost:5000 : Page d'accueil
|
||||||
|
- http://localhost:5000/dashboard : Tableau de bord
|
||||||
|
- http://localhost:5000/generator : Création de factures
|
||||||
|
|
||||||
|
## Fonctionnalités
|
||||||
|
|
||||||
|
- Création de factures (FR/DE/EN)
|
||||||
|
- Génération de PDF avec Typst
|
||||||
|
- Tableau de bord des factures
|
||||||
|
- Support EUR/CHF
|
||||||
|
- Numérotation automatique
|
||||||
|
|
||||||
|
## Structure
|
||||||
|
|
||||||
|
```
|
||||||
|
accounting/
|
||||||
|
├── server.py # Application Flask
|
||||||
|
├── database.py # Base de données SQLite
|
||||||
|
├── requirements.txt # Dépendances
|
||||||
|
├── static/ # Fichiers statiques
|
||||||
|
└── generated/ # PDF générés
|
||||||
|
```
|
||||||
|
|
||||||
|
## Fonctionnalités Techniques
|
||||||
|
|
||||||
|
- **Gestion de la Base de Données** :
|
||||||
|
|
||||||
|
- Système de migration pour les mises à jour du schéma
|
||||||
|
- Gestion des connexions avec timeout et retry
|
||||||
|
- Mode WAL pour de meilleures performances
|
||||||
|
|
||||||
|
- **Génération de PDF** :
|
||||||
|
|
||||||
|
- Templates Typst personnalisables
|
||||||
|
- Support multilingue
|
||||||
|
- Mise en page professionnelle
|
||||||
|
|
||||||
|
- **Interface Utilisateur** :
|
||||||
|
- Design responsive avec Tailwind CSS
|
||||||
|
- Prévisualisation en temps réel
|
||||||
|
- Validation des formulaires
|
||||||
|
|
||||||
|
## Contribution
|
||||||
|
|
||||||
|
Les contributions sont les bienvenues ! N'hésitez pas à :
|
||||||
|
|
||||||
|
1. Fork le projet
|
||||||
|
2. Créer une branche pour votre fonctionnalité
|
||||||
|
3. Commiter vos changements
|
||||||
|
4. Pousser vers la branche
|
||||||
|
5. Ouvrir une Pull Request
|
||||||
|
|
||||||
|
## Licence
|
||||||
|
|
||||||
|
Ce projet est sous licence MIT. Voir le fichier `LICENSE` pour plus de détails.
|
||||||
29
database.py
29
database.py
@ -182,25 +182,34 @@ class Database:
|
|||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
def get_statistics(self):
|
def get_statistics(self):
|
||||||
|
"""Récupérer les statistiques des factures"""
|
||||||
with self.get_connection() as conn:
|
with self.get_connection() as conn:
|
||||||
c = conn.cursor()
|
c = conn.cursor()
|
||||||
|
|
||||||
stats = {}
|
# Nombre total de factures
|
||||||
|
|
||||||
# Total des factures
|
|
||||||
c.execute('SELECT COUNT(*) FROM invoices')
|
c.execute('SELECT COUNT(*) FROM invoices')
|
||||||
stats['total_invoices'] = c.fetchone()[0]
|
total_invoices = c.fetchone()[0] or 0
|
||||||
|
|
||||||
# Montant total des factures
|
# Montant total des factures
|
||||||
c.execute('SELECT SUM(amount) FROM invoices')
|
c.execute('SELECT SUM(amount) FROM invoices')
|
||||||
stats['total_amount'] = c.fetchone()[0] or 0
|
total_amount = c.fetchone()[0] or 0
|
||||||
|
|
||||||
# Montant total des factures payées
|
# Montant des factures payées
|
||||||
c.execute('SELECT SUM(amount) FROM invoices WHERE status = "paid"')
|
c.execute('SELECT SUM(amount) FROM invoices WHERE status = "paid"')
|
||||||
stats['total_paid'] = c.fetchone()[0] or 0
|
total_paid = c.fetchone()[0] or 0
|
||||||
|
|
||||||
# Montant total des factures en retard
|
# Montant des factures en retard
|
||||||
c.execute('SELECT SUM(amount) FROM invoices WHERE status = "overdue"')
|
c.execute('SELECT SUM(amount) FROM invoices WHERE status = "overdue"')
|
||||||
stats['total_overdue'] = c.fetchone()[0] or 0
|
total_overdue = c.fetchone()[0] or 0
|
||||||
|
|
||||||
return stats
|
# Montant des factures annulées
|
||||||
|
c.execute('SELECT SUM(amount) FROM invoices WHERE status = "cancelled"')
|
||||||
|
total_cancelled = c.fetchone()[0] or 0
|
||||||
|
|
||||||
|
return {
|
||||||
|
'total_invoices': total_invoices,
|
||||||
|
'total_amount': total_amount,
|
||||||
|
'total_paid': total_paid,
|
||||||
|
'total_overdue': total_overdue,
|
||||||
|
'total_cancelled': total_cancelled
|
||||||
|
}
|
||||||
BIN
invoices.db
Normal file
BIN
invoices.db
Normal file
Binary file not shown.
@ -307,11 +307,11 @@ def create_invoice():
|
|||||||
#t.account_holder Robin Szymczak
|
#t.account_holder Robin Szymczak
|
||||||
] else if currency == "CHF" [
|
] else if currency == "CHF" [
|
||||||
// Compte pour les transactions en francs suisses
|
// Compte pour les transactions en francs suisses
|
||||||
#t.bank PostFinance SA, Mingerstrasse 20, 3030 Bern, Switzerland
|
#t.bank Wise Payments Limited, 1st Floor, Worship Square, 65 Clifton Street, London, EC2A 4JE, United Kingdom
|
||||||
#v(0.1cm)
|
#v(0.1cm)
|
||||||
IBAN: CH56 0900 0000 1527 2120 9
|
IBAN: GB51 TRWI 2308 0140 8766 98
|
||||||
#v(0.1cm)
|
#v(0.1cm)
|
||||||
BIC/SWIFT: POFICHBEXXX
|
BIC/SWIFT: TRWIGB2LXXX
|
||||||
#v(0.1cm)
|
#v(0.1cm)
|
||||||
#t.account_holder Robin Szymczak
|
#t.account_holder Robin Szymczak
|
||||||
] else [
|
] else [
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Statistiques -->
|
<!-- Statistiques -->
|
||||||
<div class="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
|
<div class="grid grid-cols-1 md:grid-cols-5 gap-4 mb-8">
|
||||||
<div class="bg-white rounded-lg shadow p-4">
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
<h3 class="text-gray-500 text-sm">Total Factures</h3>
|
<h3 class="text-gray-500 text-sm">Total Factures</h3>
|
||||||
<p class="text-2xl font-bold" id="totalInvoices">0</p>
|
<p class="text-2xl font-bold" id="totalInvoices">0</p>
|
||||||
@ -39,6 +39,10 @@
|
|||||||
<h3 class="text-gray-500 text-sm">Factures en Retard</h3>
|
<h3 class="text-gray-500 text-sm">Factures en Retard</h3>
|
||||||
<p class="text-2xl font-bold text-red-500" id="totalOverdue">0 €</p>
|
<p class="text-2xl font-bold text-red-500" id="totalOverdue">0 €</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="bg-white rounded-lg shadow p-4">
|
||||||
|
<h3 class="text-gray-500 text-sm">Factures Annulées</h3>
|
||||||
|
<p class="text-2xl font-bold text-gray-500" id="totalCancelled">0 €</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Filtres -->
|
<!-- Filtres -->
|
||||||
@ -51,6 +55,7 @@
|
|||||||
<option value="issued">Émise</option>
|
<option value="issued">Émise</option>
|
||||||
<option value="paid">Payée</option>
|
<option value="paid">Payée</option>
|
||||||
<option value="overdue">En retard</option>
|
<option value="overdue">En retard</option>
|
||||||
|
<option value="cancelled">Annulée</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@ -58,6 +58,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
document.getElementById(
|
document.getElementById(
|
||||||
"totalOverdue"
|
"totalOverdue"
|
||||||
).textContent = `${stats.total_overdue.toFixed(2)} €`;
|
).textContent = `${stats.total_overdue.toFixed(2)} €`;
|
||||||
|
document.getElementById("totalCancelled").textContent = `${(
|
||||||
|
stats.total_cancelled || 0
|
||||||
|
).toFixed(2)} €`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fonction pour afficher les factures
|
// Fonction pour afficher les factures
|
||||||
@ -66,6 +69,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
|
|
||||||
invoices.forEach((invoice) => {
|
invoices.forEach((invoice) => {
|
||||||
const row = document.createElement("tr");
|
const row = document.createElement("tr");
|
||||||
|
|
||||||
|
// Préparer les boutons d'action en fonction du statut
|
||||||
|
let actionButtons = `
|
||||||
|
<button onclick="viewInvoice(${invoice.id})" class="text-blue-600 hover:text-blue-900 mr-3">
|
||||||
|
Voir
|
||||||
|
</button>`;
|
||||||
|
|
||||||
|
// N'afficher le bouton "Marquer comme payée" que si la facture n'est pas déjà payée ou annulée
|
||||||
|
if (invoice.status !== "paid" && invoice.status !== "cancelled") {
|
||||||
|
actionButtons += `
|
||||||
|
<button onclick="updateStatus(${invoice.id}, 'paid')" class="text-green-600 hover:text-green-900 mr-3">
|
||||||
|
Marquer comme payée
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// N'afficher le bouton "Annuler" que si la facture n'est pas déjà annulée
|
||||||
|
if (invoice.status !== "cancelled") {
|
||||||
|
actionButtons += `
|
||||||
|
<button onclick="updateStatus(${invoice.id}, 'cancelled')" class="text-red-600 hover:text-red-900 mr-3">
|
||||||
|
Annuler
|
||||||
|
</button>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toujours afficher le bouton de téléchargement
|
||||||
|
actionButtons += `
|
||||||
|
<button onclick="downloadInvoice(${invoice.id})" class="text-gray-600 hover:text-gray-900">
|
||||||
|
Télécharger
|
||||||
|
</button>`;
|
||||||
|
|
||||||
row.innerHTML = `
|
row.innerHTML = `
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||||
${invoice.invoice_number}
|
${invoice.invoice_number}
|
||||||
@ -86,21 +118,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
</span>
|
</span>
|
||||||
</td>
|
</td>
|
||||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||||
<button onclick="viewInvoice(${
|
${actionButtons}
|
||||||
invoice.id
|
|
||||||
})" class="text-blue-600 hover:text-blue-900 mr-3">
|
|
||||||
Voir
|
|
||||||
</button>
|
|
||||||
<button onclick="updateStatus(${
|
|
||||||
invoice.id
|
|
||||||
})" class="text-green-600 hover:text-green-900 mr-3">
|
|
||||||
Marquer comme payée
|
|
||||||
</button>
|
|
||||||
<button onclick="downloadInvoice(${
|
|
||||||
invoice.id
|
|
||||||
})" class="text-gray-600 hover:text-gray-900">
|
|
||||||
Télécharger
|
|
||||||
</button>
|
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
invoicesList.appendChild(row);
|
invoicesList.appendChild(row);
|
||||||
@ -114,6 +132,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return "bg-green-100 text-green-800";
|
return "bg-green-100 text-green-800";
|
||||||
case "overdue":
|
case "overdue":
|
||||||
return "bg-red-100 text-red-800";
|
return "bg-red-100 text-red-800";
|
||||||
|
case "cancelled":
|
||||||
|
return "bg-gray-100 text-gray-800";
|
||||||
default:
|
default:
|
||||||
return "bg-yellow-100 text-yellow-800";
|
return "bg-yellow-100 text-yellow-800";
|
||||||
}
|
}
|
||||||
@ -126,6 +146,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
return "Payée";
|
return "Payée";
|
||||||
case "overdue":
|
case "overdue":
|
||||||
return "En retard";
|
return "En retard";
|
||||||
|
case "cancelled":
|
||||||
|
return "Annulée";
|
||||||
default:
|
default:
|
||||||
return "Émise";
|
return "Émise";
|
||||||
}
|
}
|
||||||
@ -164,14 +186,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Fonction pour mettre à jour le statut d'une facture
|
// Fonction pour mettre à jour le statut d'une facture
|
||||||
window.updateStatus = async (invoiceId) => {
|
window.updateStatus = async (invoiceId, status) => {
|
||||||
try {
|
try {
|
||||||
await fetch(`/api/invoices/${invoiceId}/status`, {
|
await fetch(`/api/invoices/${invoiceId}/status`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
headers: {
|
headers: {
|
||||||
"Content-Type": "application/json",
|
"Content-Type": "application/json",
|
||||||
},
|
},
|
||||||
body: JSON.stringify({ status: "paid" }),
|
body: JSON.stringify({ status: status }),
|
||||||
});
|
});
|
||||||
|
|
||||||
loadInvoices();
|
loadInvoices();
|
||||||
|
|||||||
@ -12,9 +12,9 @@
|
|||||||
<div class="flex justify-between items-center mb-8">
|
<div class="flex justify-between items-center mb-8">
|
||||||
<h1 class="text-3xl font-bold">Prévisualisation de la Facture</h1>
|
<h1 class="text-3xl font-bold">Prévisualisation de la Facture</h1>
|
||||||
<div class="space-x-4">
|
<div class="space-x-4">
|
||||||
<a href="/generator" class="bg-gray-500 text-white px-4 py-2 rounded-md hover:bg-gray-600">
|
<button id="backToForm" class="bg-gray-500 text-white px-4 py-2 rounded-md hover:bg-gray-600">
|
||||||
Retour
|
Retour
|
||||||
</a>
|
</button>
|
||||||
<button id="validateInvoice" class="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600">
|
<button id="validateInvoice" class="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600">
|
||||||
Valider la Facture
|
Valider la Facture
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@ -89,9 +89,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
if (paymentInfoDiv) {
|
if (paymentInfoDiv) {
|
||||||
if (formData.currency === "CHF") {
|
if (formData.currency === "CHF") {
|
||||||
paymentInfoDiv.innerHTML = `
|
paymentInfoDiv.innerHTML = `
|
||||||
<p>Banque: PostFinance SA, Berne, Suisse</p>
|
<p>Banque: Wise Payments Limited, London, United Kingdom</p>
|
||||||
<p>IBAN: CH56 0900 0000 1527 2120 9</p>
|
<p>IBAN: GB51 TRWI 2308 0140 8766 98</p>
|
||||||
<p>BIC/SWIFT: POFICHBEXXX</p>
|
<p>BIC/SWIFT: TRWIGB2LXXX</p>
|
||||||
<p>Titulaire: Robin Szymczak</p>
|
<p>Titulaire: Robin Szymczak</p>
|
||||||
`;
|
`;
|
||||||
} else {
|
} else {
|
||||||
@ -104,6 +104,14 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gérer le bouton "Retour"
|
||||||
|
document.getElementById("backToForm").addEventListener("click", function () {
|
||||||
|
// Sauvegarder les données du formulaire dans localStorage
|
||||||
|
localStorage.setItem("invoiceData", JSON.stringify(formData));
|
||||||
|
// Rediriger vers la page du générateur
|
||||||
|
window.location.href = "/generator";
|
||||||
|
});
|
||||||
|
|
||||||
// Gérer la validation de la facture
|
// Gérer la validation de la facture
|
||||||
document
|
document
|
||||||
.getElementById("validateInvoice")
|
.getElementById("validateInvoice")
|
||||||
@ -125,6 +133,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
console.log("Facture créée avec succès:", result);
|
console.log("Facture créée avec succès:", result);
|
||||||
alert("Facture créée avec succès !");
|
alert("Facture créée avec succès !");
|
||||||
|
|
||||||
|
// Effacer les données du formulaire du localStorage après validation réussie
|
||||||
|
localStorage.removeItem("invoiceData");
|
||||||
|
|
||||||
|
// Supprimer également de l'historique des factures récentes
|
||||||
|
removeFromRecentInvoices(formData.recipient_name);
|
||||||
|
|
||||||
window.location.href = "/dashboard";
|
window.location.href = "/dashboard";
|
||||||
} else {
|
} else {
|
||||||
const error = await response.json();
|
const error = await response.json();
|
||||||
@ -139,4 +154,24 @@ document.addEventListener("DOMContentLoaded", function () {
|
|||||||
alert("Erreur lors de la création de la facture : " + error.message);
|
alert("Erreur lors de la création de la facture : " + error.message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Fonction pour supprimer une facture de l'historique récent
|
||||||
|
function removeFromRecentInvoices(recipientName) {
|
||||||
|
if (!recipientName) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
let recentInvoices = JSON.parse(
|
||||||
|
localStorage.getItem("recentInvoices") || "[]"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Filtrer pour supprimer la facture avec le même destinataire
|
||||||
|
recentInvoices = recentInvoices.filter(
|
||||||
|
(invoice) => invoice.recipient_name !== recipientName
|
||||||
|
);
|
||||||
|
|
||||||
|
localStorage.setItem("recentInvoices", JSON.stringify(recentInvoices));
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la suppression de l'historique:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
390
static/script.js
390
static/script.js
@ -2,13 +2,393 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
const form = document.getElementById("invoiceForm");
|
const form = document.getElementById("invoiceForm");
|
||||||
const invoiceItems = document.getElementById("invoiceItems");
|
const invoiceItems = document.getElementById("invoiceItems");
|
||||||
const addItemButton = document.getElementById("addItemButton");
|
const addItemButton = document.getElementById("addItemButton");
|
||||||
|
const clearFormButton = document.createElement("button");
|
||||||
|
const recipientNameInput = form.recipient_name;
|
||||||
|
const clientSuggestions = document.createElement("div");
|
||||||
|
|
||||||
// Initialiser avec une ligne de facturation par défaut
|
// Configuration de la boîte de suggestions
|
||||||
addInvoiceItem();
|
clientSuggestions.className =
|
||||||
|
"client-suggestions absolute z-10 bg-white shadow-lg rounded-md w-full max-h-60 overflow-y-auto hidden";
|
||||||
|
recipientNameInput.parentNode.style.position = "relative";
|
||||||
|
recipientNameInput.parentNode.appendChild(clientSuggestions);
|
||||||
|
|
||||||
|
// Charger les clients précédents
|
||||||
|
let clientsData = JSON.parse(localStorage.getItem("clientsData") || "[]");
|
||||||
|
|
||||||
|
// Ajouter un bouton pour effacer le formulaire
|
||||||
|
clearFormButton.type = "button";
|
||||||
|
clearFormButton.className =
|
||||||
|
"bg-red-500 text-white px-4 py-2 rounded-md hover:bg-red-600 ml-4";
|
||||||
|
clearFormButton.textContent = "Effacer le formulaire";
|
||||||
|
clearFormButton.id = "clearFormButton";
|
||||||
|
|
||||||
|
// Ajouter le bouton après le bouton de soumission
|
||||||
|
const submitButton = form.querySelector('button[type="submit"]');
|
||||||
|
submitButton.parentNode.insertBefore(
|
||||||
|
clearFormButton,
|
||||||
|
submitButton.nextSibling
|
||||||
|
);
|
||||||
|
|
||||||
|
// Gestionnaire d'événement pour effacer le formulaire
|
||||||
|
clearFormButton.addEventListener("click", () => {
|
||||||
|
if (confirm("Êtes-vous sûr de vouloir effacer le formulaire ?")) {
|
||||||
|
localStorage.removeItem("invoiceData");
|
||||||
|
form.reset();
|
||||||
|
// Réinitialiser les lignes de facturation
|
||||||
|
invoiceItems.innerHTML = "";
|
||||||
|
addInvoiceItem();
|
||||||
|
showMessage("Formulaire effacé", "success");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Restaurer les données du formulaire depuis localStorage si elles existent
|
||||||
|
restoreFormData();
|
||||||
|
|
||||||
|
// Initialiser avec une ligne de facturation par défaut ou restaurer les lignes sauvegardées
|
||||||
|
if (invoiceItems.children.length === 0) {
|
||||||
|
addInvoiceItem();
|
||||||
|
}
|
||||||
|
|
||||||
// Ajouter une ligne de facturation lorsqu'on clique sur le bouton
|
// Ajouter une ligne de facturation lorsqu'on clique sur le bouton
|
||||||
addItemButton.addEventListener("click", addInvoiceItem);
|
addItemButton.addEventListener("click", addInvoiceItem);
|
||||||
|
|
||||||
|
// Sauvegarder les données pendant la saisie (debounced)
|
||||||
|
let saveTimeout;
|
||||||
|
form.addEventListener("input", () => {
|
||||||
|
clearTimeout(saveTimeout);
|
||||||
|
saveTimeout = setTimeout(() => {
|
||||||
|
saveFormData();
|
||||||
|
}, 500); // Attendre 500ms après la dernière saisie
|
||||||
|
});
|
||||||
|
|
||||||
|
// Gérer l'auto-complétion des clients
|
||||||
|
recipientNameInput.addEventListener("input", function () {
|
||||||
|
const query = this.value.toLowerCase();
|
||||||
|
if (query.length < 2) {
|
||||||
|
clientSuggestions.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filtrer les clients qui correspondent à la recherche
|
||||||
|
const matches = clientsData.filter((client) =>
|
||||||
|
client.name.toLowerCase().includes(query)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (matches.length === 0) {
|
||||||
|
clientSuggestions.classList.add("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Afficher les suggestions
|
||||||
|
clientSuggestions.innerHTML = matches
|
||||||
|
.map(
|
||||||
|
(client) => `
|
||||||
|
<div class="client-suggestion p-2 hover:bg-gray-100 cursor-pointer" data-client-id="${client.id}">
|
||||||
|
<div class="font-medium">${client.name}</div>
|
||||||
|
<div class="text-sm text-gray-600">${client.address}, ${client.postal_code} ${client.town}</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
clientSuggestions.classList.remove("hidden");
|
||||||
|
|
||||||
|
// Ajouter des gestionnaires d'événements pour les suggestions
|
||||||
|
document.querySelectorAll(".client-suggestion").forEach((suggestion) => {
|
||||||
|
suggestion.addEventListener("click", function () {
|
||||||
|
const clientId = this.getAttribute("data-client-id");
|
||||||
|
const client = clientsData.find((c) => c.id === clientId);
|
||||||
|
if (client) {
|
||||||
|
fillClientData(client);
|
||||||
|
clientSuggestions.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Masquer les suggestions lorsqu'on clique ailleurs
|
||||||
|
document.addEventListener("click", function (e) {
|
||||||
|
if (
|
||||||
|
!recipientNameInput.contains(e.target) &&
|
||||||
|
!clientSuggestions.contains(e.target)
|
||||||
|
) {
|
||||||
|
clientSuggestions.classList.add("hidden");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fonction pour remplir les données du client
|
||||||
|
function fillClientData(client) {
|
||||||
|
form.recipient_name.value = client.name;
|
||||||
|
form.recipient_address.value = client.address;
|
||||||
|
form.recipient_postal_code.value = client.postal_code;
|
||||||
|
form.recipient_town.value = client.town;
|
||||||
|
form.recipient_country.value = client.country;
|
||||||
|
if (client.vat_number) {
|
||||||
|
form.recipient_vat_number.value = client.vat_number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour sauvegarder un nouveau client
|
||||||
|
function saveClient() {
|
||||||
|
// Ne sauvegarder que si les champs essentiels sont remplis
|
||||||
|
if (
|
||||||
|
!form.recipient_name.value ||
|
||||||
|
!form.recipient_address.value ||
|
||||||
|
!form.recipient_postal_code.value ||
|
||||||
|
!form.recipient_town.value
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const clientData = {
|
||||||
|
id: Date.now().toString(), // ID unique
|
||||||
|
name: form.recipient_name.value,
|
||||||
|
address: form.recipient_address.value,
|
||||||
|
postal_code: form.recipient_postal_code.value,
|
||||||
|
town: form.recipient_town.value,
|
||||||
|
country: form.recipient_country.value,
|
||||||
|
vat_number: form.recipient_vat_number.value || null,
|
||||||
|
lastUsed: new Date().toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vérifier si le client existe déjà (par nom)
|
||||||
|
const existingClientIndex = clientsData.findIndex(
|
||||||
|
(client) => client.name.toLowerCase() === clientData.name.toLowerCase()
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingClientIndex !== -1) {
|
||||||
|
// Mettre à jour le client existant
|
||||||
|
clientsData[existingClientIndex] = {
|
||||||
|
...clientsData[existingClientIndex],
|
||||||
|
...clientData,
|
||||||
|
id: clientsData[existingClientIndex].id, // Conserver l'ID d'origine
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Ajouter le nouveau client
|
||||||
|
clientsData.push(clientData);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Trier par date d'utilisation (plus récent en premier)
|
||||||
|
clientsData.sort((a, b) => new Date(b.lastUsed) - new Date(a.lastUsed));
|
||||||
|
|
||||||
|
// Limiter à 50 clients
|
||||||
|
clientsData = clientsData.slice(0, 50);
|
||||||
|
|
||||||
|
// Sauvegarder dans localStorage
|
||||||
|
localStorage.setItem("clientsData", JSON.stringify(clientsData));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour sauvegarder les données du formulaire
|
||||||
|
function saveFormData() {
|
||||||
|
const formData = {
|
||||||
|
language: form.language.value,
|
||||||
|
invoice_number: form.invoice_number.value,
|
||||||
|
amount: form.amount.value,
|
||||||
|
currency: form.currency.value,
|
||||||
|
recipient_name: form.recipient_name.value,
|
||||||
|
recipient_address: form.recipient_address.value,
|
||||||
|
recipient_postal_code: form.recipient_postal_code.value,
|
||||||
|
recipient_town: form.recipient_town.value,
|
||||||
|
recipient_country: form.recipient_country.value,
|
||||||
|
recipient_vat_number: form.recipient_vat_number.value || null,
|
||||||
|
items: collectInvoiceItems(),
|
||||||
|
};
|
||||||
|
|
||||||
|
localStorage.setItem("invoiceData", JSON.stringify(formData));
|
||||||
|
|
||||||
|
// Sauvegarder aussi dans l'historique des factures récentes
|
||||||
|
saveToRecentInvoices(formData);
|
||||||
|
|
||||||
|
// Sauvegarder les informations du client si tous les champs sont remplis
|
||||||
|
saveClient();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour sauvegarder dans l'historique des factures récentes
|
||||||
|
function saveToRecentInvoices(formData) {
|
||||||
|
if (!formData.recipient_name) return; // Ne pas sauvegarder si pas de nom de destinataire
|
||||||
|
|
||||||
|
let recentInvoices = JSON.parse(
|
||||||
|
localStorage.getItem("recentInvoices") || "[]"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Créer un identifiant unique basé sur le nom et la date
|
||||||
|
const now = new Date();
|
||||||
|
const id = `${formData.recipient_name}_${now.toISOString()}`;
|
||||||
|
|
||||||
|
// Ajouter la facture actuelle à l'historique avec un timestamp
|
||||||
|
const invoiceWithMeta = {
|
||||||
|
...formData,
|
||||||
|
id,
|
||||||
|
lastEdited: now.toISOString(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Vérifier si une facture similaire existe déjà (même destinataire)
|
||||||
|
const existingIndex = recentInvoices.findIndex(
|
||||||
|
(invoice) => invoice.recipient_name === formData.recipient_name
|
||||||
|
);
|
||||||
|
|
||||||
|
if (existingIndex !== -1) {
|
||||||
|
// Remplacer la facture existante
|
||||||
|
recentInvoices[existingIndex] = invoiceWithMeta;
|
||||||
|
} else {
|
||||||
|
// Ajouter la nouvelle facture
|
||||||
|
recentInvoices.unshift(invoiceWithMeta);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limiter à 10 factures récentes
|
||||||
|
recentInvoices = recentInvoices.slice(0, 10);
|
||||||
|
|
||||||
|
localStorage.setItem("recentInvoices", JSON.stringify(recentInvoices));
|
||||||
|
|
||||||
|
// Mettre à jour la liste des factures récentes dans l'interface
|
||||||
|
updateRecentInvoicesList();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour mettre à jour la liste des factures récentes dans l'interface
|
||||||
|
function updateRecentInvoicesList() {
|
||||||
|
const recentInvoicesContainer = document.getElementById("recentInvoices");
|
||||||
|
if (!recentInvoicesContainer) {
|
||||||
|
// Créer le conteneur s'il n'existe pas
|
||||||
|
const container = document.createElement("div");
|
||||||
|
container.id = "recentInvoices";
|
||||||
|
container.className = "mt-8 bg-white rounded-lg shadow p-4";
|
||||||
|
container.innerHTML = `
|
||||||
|
<h3 class="text-lg font-semibold mb-4">Factures récentes</h3>
|
||||||
|
<div id="recentInvoicesList" class="space-y-2"></div>
|
||||||
|
`;
|
||||||
|
form.parentNode.insertBefore(container, form.nextSibling);
|
||||||
|
}
|
||||||
|
|
||||||
|
const recentInvoicesList = document.getElementById("recentInvoicesList");
|
||||||
|
const recentInvoices = JSON.parse(
|
||||||
|
localStorage.getItem("recentInvoices") || "[]"
|
||||||
|
);
|
||||||
|
|
||||||
|
if (recentInvoices.length === 0) {
|
||||||
|
recentInvoicesList.innerHTML = `<p class="text-gray-500">Aucune facture récente</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
recentInvoicesList.innerHTML = recentInvoices
|
||||||
|
.map(
|
||||||
|
(invoice) => `
|
||||||
|
<div class="flex justify-between items-center p-2 border-b">
|
||||||
|
<div>
|
||||||
|
<span class="font-medium">${invoice.recipient_name}</span>
|
||||||
|
<span class="text-sm text-gray-500 ml-2">${invoice.amount} ${
|
||||||
|
invoice.currency
|
||||||
|
}</span>
|
||||||
|
<span class="text-xs text-gray-400 ml-2">${new Date(
|
||||||
|
invoice.lastEdited
|
||||||
|
).toLocaleDateString()}</span>
|
||||||
|
</div>
|
||||||
|
<button class="load-invoice bg-blue-500 text-white px-2 py-1 rounded text-sm" data-id="${
|
||||||
|
invoice.id
|
||||||
|
}">
|
||||||
|
Charger
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
// Ajouter les gestionnaires d'événements pour charger les factures
|
||||||
|
document.querySelectorAll(".load-invoice").forEach((button) => {
|
||||||
|
button.addEventListener("click", (e) => {
|
||||||
|
const id = e.target.getAttribute("data-id");
|
||||||
|
const invoice = recentInvoices.find((inv) => inv.id === id);
|
||||||
|
if (invoice) {
|
||||||
|
if (
|
||||||
|
confirm(
|
||||||
|
"Charger cette facture ? Les données actuelles seront remplacées."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
loadInvoice(invoice);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour charger une facture depuis l'historique
|
||||||
|
function loadInvoice(invoice) {
|
||||||
|
// Remplir le formulaire avec les données de la facture
|
||||||
|
form.language.value = invoice.language || "";
|
||||||
|
form.invoice_number.value = invoice.invoice_number || "";
|
||||||
|
form.amount.value = invoice.amount || "";
|
||||||
|
form.currency.value = invoice.currency || "EUR";
|
||||||
|
form.recipient_name.value = invoice.recipient_name || "";
|
||||||
|
form.recipient_address.value = invoice.recipient_address || "";
|
||||||
|
form.recipient_postal_code.value = invoice.recipient_postal_code || "";
|
||||||
|
form.recipient_town.value = invoice.recipient_town || "";
|
||||||
|
form.recipient_country.value = invoice.recipient_country || "";
|
||||||
|
form.recipient_vat_number.value = invoice.recipient_vat_number || "";
|
||||||
|
|
||||||
|
// Restaurer les lignes de facturation
|
||||||
|
invoiceItems.innerHTML = "";
|
||||||
|
if (invoice.items && invoice.items.length > 0) {
|
||||||
|
invoice.items.forEach((item) => {
|
||||||
|
addInvoiceItem(item.description, item.amount);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
addInvoiceItem();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sauvegarder dans le localStorage
|
||||||
|
localStorage.setItem("invoiceData", JSON.stringify(invoice));
|
||||||
|
|
||||||
|
showMessage("Facture chargée avec succès", "success");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fonction pour restaurer les données du formulaire depuis localStorage
|
||||||
|
function restoreFormData() {
|
||||||
|
const savedData = localStorage.getItem("invoiceData");
|
||||||
|
if (!savedData) {
|
||||||
|
// Mettre à jour la liste des factures récentes même s'il n'y a pas de données sauvegardées
|
||||||
|
updateRecentInvoicesList();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const formData = JSON.parse(savedData);
|
||||||
|
|
||||||
|
// Remplir les champs du formulaire
|
||||||
|
if (formData.language) form.language.value = formData.language;
|
||||||
|
if (formData.invoice_number)
|
||||||
|
form.invoice_number.value = formData.invoice_number;
|
||||||
|
if (formData.amount) form.amount.value = formData.amount;
|
||||||
|
if (formData.currency) form.currency.value = formData.currency;
|
||||||
|
if (formData.recipient_name)
|
||||||
|
form.recipient_name.value = formData.recipient_name;
|
||||||
|
if (formData.recipient_address)
|
||||||
|
form.recipient_address.value = formData.recipient_address;
|
||||||
|
if (formData.recipient_postal_code)
|
||||||
|
form.recipient_postal_code.value = formData.recipient_postal_code;
|
||||||
|
if (formData.recipient_town)
|
||||||
|
form.recipient_town.value = formData.recipient_town;
|
||||||
|
if (formData.recipient_country)
|
||||||
|
form.recipient_country.value = formData.recipient_country;
|
||||||
|
if (formData.recipient_vat_number)
|
||||||
|
form.recipient_vat_number.value = formData.recipient_vat_number;
|
||||||
|
|
||||||
|
// Restaurer les lignes de facturation
|
||||||
|
if (formData.items && formData.items.length > 0) {
|
||||||
|
// Vider les lignes existantes
|
||||||
|
invoiceItems.innerHTML = "";
|
||||||
|
|
||||||
|
// Ajouter chaque ligne sauvegardée
|
||||||
|
formData.items.forEach((item) => {
|
||||||
|
addInvoiceItem(item.description, item.amount);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mettre à jour la liste des factures récentes
|
||||||
|
updateRecentInvoicesList();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Erreur lors de la restauration des données:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fonction pour créer une nouvelle ligne de facturation
|
// Fonction pour créer une nouvelle ligne de facturation
|
||||||
function addInvoiceItem(description = "", amount = "") {
|
function addInvoiceItem(description = "", amount = "") {
|
||||||
const itemId = Date.now(); // ID unique pour l'élément
|
const itemId = Date.now(); // ID unique pour l'élément
|
||||||
@ -145,6 +525,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
|||||||
items: collectInvoiceItems(), // Ajouter les lignes de facturation
|
items: collectInvoiceItems(), // Ajouter les lignes de facturation
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Sauvegarder les données du formulaire dans localStorage
|
||||||
|
localStorage.setItem("invoiceData", JSON.stringify(formData));
|
||||||
|
|
||||||
|
// Sauvegarder les informations du client
|
||||||
|
saveClient();
|
||||||
|
|
||||||
// Rediriger vers la page de prévisualisation avec les données
|
// Rediriger vers la page de prévisualisation avec les données
|
||||||
const queryString = encodeURIComponent(JSON.stringify(formData));
|
const queryString = encodeURIComponent(JSON.stringify(formData));
|
||||||
window.location.href = `/preview?data=${queryString}`;
|
window.location.href = `/preview?data=${queryString}`;
|
||||||
|
|||||||
@ -49,4 +49,27 @@ select {
|
|||||||
|
|
||||||
.error-message.show {
|
.error-message.show {
|
||||||
transform: translateX(0);
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Styles pour l'auto-complétion des clients */
|
||||||
|
.client-suggestions {
|
||||||
|
border: 1px solid #e2e8f0;
|
||||||
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-suggestion {
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-suggestion:hover {
|
||||||
|
background-color: #f3f4f6;
|
||||||
|
}
|
||||||
|
|
||||||
|
.client-suggestion:not(:last-child) {
|
||||||
|
border-bottom: 1px solid #e2e8f0;
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user