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()
|
||||
|
||||
def get_statistics(self):
|
||||
"""Récupérer les statistiques des factures"""
|
||||
with self.get_connection() as conn:
|
||||
c = conn.cursor()
|
||||
|
||||
stats = {}
|
||||
|
||||
# Total des factures
|
||||
# Nombre total de factures
|
||||
c.execute('SELECT COUNT(*) FROM invoices')
|
||||
stats['total_invoices'] = c.fetchone()[0]
|
||||
total_invoices = c.fetchone()[0] or 0
|
||||
|
||||
# Montant total des factures
|
||||
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"')
|
||||
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"')
|
||||
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
|
||||
] else if currency == "CHF" [
|
||||
// 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)
|
||||
IBAN: CH56 0900 0000 1527 2120 9
|
||||
IBAN: GB51 TRWI 2308 0140 8766 98
|
||||
#v(0.1cm)
|
||||
BIC/SWIFT: POFICHBEXXX
|
||||
BIC/SWIFT: TRWIGB2LXXX
|
||||
#v(0.1cm)
|
||||
#t.account_holder Robin Szymczak
|
||||
] else [
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
</div>
|
||||
|
||||
<!-- 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">
|
||||
<h3 class="text-gray-500 text-sm">Total Factures</h3>
|
||||
<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>
|
||||
<p class="text-2xl font-bold text-red-500" id="totalOverdue">0 €</p>
|
||||
</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>
|
||||
|
||||
<!-- Filtres -->
|
||||
@ -51,6 +55,7 @@
|
||||
<option value="issued">Émise</option>
|
||||
<option value="paid">Payée</option>
|
||||
<option value="overdue">En retard</option>
|
||||
<option value="cancelled">Annulée</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@ -58,6 +58,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById(
|
||||
"totalOverdue"
|
||||
).textContent = `${stats.total_overdue.toFixed(2)} €`;
|
||||
document.getElementById("totalCancelled").textContent = `${(
|
||||
stats.total_cancelled || 0
|
||||
).toFixed(2)} €`;
|
||||
}
|
||||
|
||||
// Fonction pour afficher les factures
|
||||
@ -66,6 +69,35 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
|
||||
invoices.forEach((invoice) => {
|
||||
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 = `
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
${invoice.invoice_number}
|
||||
@ -86,21 +118,7 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
</span>
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
|
||||
<button onclick="viewInvoice(${
|
||||
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>
|
||||
${actionButtons}
|
||||
</td>
|
||||
`;
|
||||
invoicesList.appendChild(row);
|
||||
@ -114,6 +132,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return "bg-green-100 text-green-800";
|
||||
case "overdue":
|
||||
return "bg-red-100 text-red-800";
|
||||
case "cancelled":
|
||||
return "bg-gray-100 text-gray-800";
|
||||
default:
|
||||
return "bg-yellow-100 text-yellow-800";
|
||||
}
|
||||
@ -126,6 +146,8 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
return "Payée";
|
||||
case "overdue":
|
||||
return "En retard";
|
||||
case "cancelled":
|
||||
return "Annulée";
|
||||
default:
|
||||
return "Émise";
|
||||
}
|
||||
@ -164,14 +186,14 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
};
|
||||
|
||||
// Fonction pour mettre à jour le statut d'une facture
|
||||
window.updateStatus = async (invoiceId) => {
|
||||
window.updateStatus = async (invoiceId, status) => {
|
||||
try {
|
||||
await fetch(`/api/invoices/${invoiceId}/status`, {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ status: "paid" }),
|
||||
body: JSON.stringify({ status: status }),
|
||||
});
|
||||
|
||||
loadInvoices();
|
||||
|
||||
@ -12,9 +12,9 @@
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<h1 class="text-3xl font-bold">Prévisualisation de la Facture</h1>
|
||||
<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
|
||||
</a>
|
||||
</button>
|
||||
<button id="validateInvoice" class="bg-green-500 text-white px-4 py-2 rounded-md hover:bg-green-600">
|
||||
Valider la Facture
|
||||
</button>
|
||||
|
||||
@ -89,9 +89,9 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
if (paymentInfoDiv) {
|
||||
if (formData.currency === "CHF") {
|
||||
paymentInfoDiv.innerHTML = `
|
||||
<p>Banque: PostFinance SA, Berne, Suisse</p>
|
||||
<p>IBAN: CH56 0900 0000 1527 2120 9</p>
|
||||
<p>BIC/SWIFT: POFICHBEXXX</p>
|
||||
<p>Banque: Wise Payments Limited, London, United Kingdom</p>
|
||||
<p>IBAN: GB51 TRWI 2308 0140 8766 98</p>
|
||||
<p>BIC/SWIFT: TRWIGB2LXXX</p>
|
||||
<p>Titulaire: Robin Szymczak</p>
|
||||
`;
|
||||
} 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
|
||||
document
|
||||
.getElementById("validateInvoice")
|
||||
@ -125,6 +133,13 @@ document.addEventListener("DOMContentLoaded", function () {
|
||||
const result = await response.json();
|
||||
console.log("Facture créée avec succès:", result);
|
||||
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";
|
||||
} else {
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
388
static/script.js
388
static/script.js
@ -2,13 +2,393 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
const form = document.getElementById("invoiceForm");
|
||||
const invoiceItems = document.getElementById("invoiceItems");
|
||||
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
|
||||
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
|
||||
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
|
||||
function addInvoiceItem(description = "", amount = "") {
|
||||
const itemId = Date.now(); // ID unique pour l'élément
|
||||
@ -145,6 +525,12 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
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
|
||||
const queryString = encodeURIComponent(JSON.stringify(formData));
|
||||
window.location.href = `/preview?data=${queryString}`;
|
||||
|
||||
@ -50,3 +50,26 @@ select {
|
||||
.error-message.show {
|
||||
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