diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f5aefd --- /dev/null +++ b/.gitignore @@ -0,0 +1,45 @@ +# Base de données +*.db +*.db-journal + +# Fichiers générés +generated/ +*.pdf +*.typ + +# Fichiers Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Environnement virtuel +venv/ +ENV/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Système +.DS_Store +Thumbs.db \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..1b4bdcf --- /dev/null +++ b/database.py @@ -0,0 +1,206 @@ +import sqlite3 +from datetime import datetime +import json +import os +from contextlib import contextmanager +import time + +class Database: + def __init__(self, db_file="invoices.db"): + self.db_file = db_file + self.version = 1 + self.init_db() + + @contextmanager + def get_connection(self, timeout=30, max_retries=3): + retries = 0 + while retries < max_retries: + try: + conn = sqlite3.connect(self.db_file, timeout=timeout) + conn.execute("PRAGMA journal_mode=WAL") # Utiliser le mode WAL pour de meilleures performances + conn.execute("PRAGMA busy_timeout=5000") # Timeout de 5 secondes + try: + yield conn + conn.commit() + finally: + conn.close() + break + except sqlite3.OperationalError as e: + if "database is locked" in str(e) and retries < max_retries - 1: + retries += 1 + time.sleep(0.1 * (2 ** retries)) # Backoff exponentiel + continue + raise + + def init_db(self): + with self.get_connection() as conn: + c = conn.cursor() + + # Table des versions + c.execute(''' + CREATE TABLE IF NOT EXISTS db_version ( + version INTEGER PRIMARY KEY + ) + ''') + + # Vérifier la version actuelle + c.execute('SELECT version FROM db_version') + result = c.fetchone() + current_version = result[0] if result else 0 + + # Appliquer les migrations si nécessaire + if current_version < self.version: + self._apply_migrations(conn, current_version) + + conn.commit() + + def _apply_migrations(self, conn, current_version): + c = conn.cursor() + + # Migration 1: Structure initiale + if current_version < 1: + # Table des clients + c.execute(''' + CREATE TABLE IF NOT EXISTS clients ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + address TEXT NOT NULL, + postal_code TEXT NOT NULL, + town TEXT NOT NULL, + country TEXT NOT NULL, + vat_number TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # Table des factures + c.execute(''' + CREATE TABLE IF NOT EXISTS invoices ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + invoice_number TEXT UNIQUE NOT NULL, + client_id INTEGER NOT NULL, + amount DECIMAL(10,2) NOT NULL, + currency TEXT NOT NULL, + language TEXT NOT NULL, + status TEXT DEFAULT 'issued', + issue_date TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + due_date TIMESTAMP, + typst_content TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (client_id) REFERENCES clients (id) + ) + ''') + + # Mettre à jour la version + c.execute('INSERT INTO db_version (version) VALUES (1)') + current_version = 1 + + def get_next_invoice_number(self): + with self.get_connection() as conn: + c = conn.cursor() + c.execute('SELECT MAX(CAST(SUBSTR(invoice_number, 1) AS INTEGER)) FROM invoices') + result = c.fetchone() + last_number = result[0] if result[0] is not None else 0 + return str(last_number + 1).zfill(3) + + def add_client(self, name, address, postal_code, town, country, vat_number=None): + with self.get_connection() as conn: + c = conn.cursor() + c.execute(''' + INSERT INTO clients (name, address, postal_code, town, country, vat_number) + VALUES (?, ?, ?, ?, ?, ?) + ''', (name, address, postal_code, town, country, vat_number)) + client_id = c.lastrowid + conn.commit() + return client_id + + def add_invoice(self, invoice_number, client_id, amount, currency, language, typst_content, due_date=None): + with self.get_connection() as conn: + c = conn.cursor() + c.execute(''' + INSERT INTO invoices (invoice_number, client_id, amount, currency, language, typst_content, due_date) + VALUES (?, ?, ?, ?, ?, ?, ?) + ''', (invoice_number, client_id, amount, currency, language, typst_content, due_date)) + invoice_id = c.lastrowid + conn.commit() + return invoice_id + + def get_invoices(self, filters=None): + with self.get_connection() as conn: + c = conn.cursor() + + query = ''' + SELECT i.*, c.name as client_name, c.address, c.postal_code, c.town, c.country, c.vat_number + FROM invoices i + JOIN clients c ON i.client_id = c.id + ''' + + params = [] + if filters: + conditions = [] + if 'status' in filters: + conditions.append('i.status = ?') + params.append(filters['status']) + if 'client_id' in filters: + conditions.append('i.client_id = ?') + params.append(filters['client_id']) + if 'date_from' in filters: + conditions.append('i.issue_date >= ?') + params.append(filters['date_from']) + if 'date_to' in filters: + conditions.append('i.issue_date <= ?') + params.append(filters['date_to']) + + if conditions: + query += ' WHERE ' + ' AND '.join(conditions) + + query += ' ORDER BY i.issue_date DESC' + + c.execute(query, params) + columns = [description[0] for description in c.description] + invoices = [dict(zip(columns, row)) for row in c.fetchall()] + + return invoices + + def get_invoice(self, invoice_id): + with self.get_connection() as conn: + c = conn.cursor() + c.execute(''' + SELECT i.*, c.name as client_name, c.address, c.postal_code, c.town, c.country, c.vat_number + FROM invoices i + JOIN clients c ON i.client_id = c.id + WHERE i.id = ? + ''', (invoice_id,)) + columns = [description[0] for description in c.description] + invoice = dict(zip(columns, c.fetchone())) + return invoice + + def update_invoice_status(self, invoice_id, status): + with self.get_connection() as conn: + c = conn.cursor() + c.execute('UPDATE invoices SET status = ? WHERE id = ?', (status, invoice_id)) + conn.commit() + + def get_statistics(self): + with self.get_connection() as conn: + c = conn.cursor() + + stats = {} + + # Total des factures + c.execute('SELECT COUNT(*) FROM invoices') + stats['total_invoices'] = c.fetchone()[0] + + # Montant total des factures + c.execute('SELECT SUM(amount) FROM invoices') + stats['total_amount'] = c.fetchone()[0] or 0 + + # Montant total des factures payées + c.execute('SELECT SUM(amount) FROM invoices WHERE status = "paid"') + stats['total_paid'] = c.fetchone()[0] or 0 + + # Montant total des factures en retard + c.execute('SELECT SUM(amount) FROM invoices WHERE status = "overdue"') + stats['total_overdue'] = c.fetchone()[0] or 0 + + return stats \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..0a94e57 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +flask==3.0.2 +python-dotenv==1.0.1 +typst==0.1.0 \ No newline at end of file diff --git a/server.py b/server.py new file mode 100644 index 0000000..e823277 --- /dev/null +++ b/server.py @@ -0,0 +1,319 @@ +from flask import Flask, request, jsonify, send_file +from database import Database +import os +from datetime import datetime +import subprocess + +app = Flask(__name__) +db = Database() + +@app.route('/') +def index(): + return app.send_static_file('generator.html') + +@app.route('/generator') +def generator(): + return app.send_static_file('generator.html') + +@app.route('/dashboard') +def dashboard(): + return app.send_static_file('dashboard.html') + +@app.route('/preview') +def preview(): + return app.send_static_file('preview.html') + +@app.route('/api/invoices', methods=['GET']) +def get_invoices(): + filters = {} + if request.args.get('status'): + filters['status'] = request.args.get('status') + if request.args.get('date_from'): + filters['date_from'] = request.args.get('date_from') + if request.args.get('date_to'): + filters['date_to'] = request.args.get('date_to') + + invoices = db.get_invoices(filters) + statistics = db.get_statistics() + + return jsonify({ + 'invoices': invoices, + 'statistics': statistics + }) + +@app.route('/api/invoices/', methods=['GET']) +def get_invoice(invoice_id): + invoice = db.get_invoice(invoice_id) + if invoice: + return jsonify(invoice) + return jsonify({'error': 'Facture non trouvée'}), 404 + +@app.route('/api/invoices//status', methods=['PUT']) +def update_invoice_status(invoice_id): + data = request.get_json() + if 'status' not in data: + return jsonify({'error': 'Statut manquant'}), 400 + + db.update_invoice_status(invoice_id, data['status']) + return jsonify({'message': 'Statut mis à jour avec succès'}) + +@app.route('/api/invoices//download', methods=['GET']) +def download_invoice(invoice_id): + invoice = db.get_invoice(invoice_id) + if not invoice: + return jsonify({'error': 'Facture non trouvée'}), 404 + + # Créer le dossier pour les fichiers générés s'il n'existe pas + os.makedirs('generated', exist_ok=True) + + # Créer un fichier temporaire pour la facture + timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") + temp_file = os.path.join('generated', f'invoice_{invoice_id}_{timestamp}.typ') + output_pdf = temp_file.replace('.typ', '.pdf') + + try: + # Écrire le contenu Typst + with open(temp_file, 'w') as f: + f.write(invoice['typst_content']) + + # Générer le PDF avec Typst + result = subprocess.run(['typst', 'compile', temp_file, output_pdf], + capture_output=True, text=True) + + if result.returncode != 0: + return jsonify({'error': f'Erreur lors de la génération du PDF: {result.stderr}'}), 500 + + # Vérifier que le PDF a été créé + if not os.path.exists(output_pdf): + return jsonify({'error': 'Le PDF n\'a pas été généré'}), 500 + + # Envoyer le PDF + return send_file( + output_pdf, + as_attachment=True, + download_name=f'invoice_{invoice["invoice_number"]}.pdf' + ) + finally: + # Nettoyer les fichiers temporaires + if os.path.exists(temp_file): + os.remove(temp_file) + if os.path.exists(output_pdf): + os.remove(output_pdf) + +@app.route('/api/invoices', methods=['POST']) +def create_invoice(): + data = request.get_json() + + # Générer automatiquement le numéro de facture + invoice_number = db.get_next_invoice_number() + + # Ajouter le client + client_id = db.add_client( + name=data['recipient_name'], + address=data['recipient_address'], + postal_code=data['recipient_postal_code'], + town=data['recipient_town'], + country=data['recipient_country'], + vat_number=data.get('recipient_vat_number') + ) + + # Créer le contenu du template Typst + typst_content = f'''#let language = "{data['language']}" +#let invoice_number = "{invoice_number}" +#let amount = "{data['amount']}" +#let currency = "{data['currency']}" +#let current_date = datetime.today() +#let recipient_name = [{data['recipient_name']}] +#let recipient_adress = [{data['recipient_address']}] +#let recipient_postal_code = [{data['recipient_postal_code']}] +#let recipient_town = [{data['recipient_town']}] +#let recipient_country = [{data['recipient_country']}] +#let recipient_vat_number = {"none" if data.get('recipient_vat_number') is None else f'"{data["recipient_vat_number"]}"'} + +// Dictionnaire des traductions +#let translations = ( + français: ( + sender: "ÉMETTEUR", + recipient: "DESTINATAIRE", + invoice: "FACTURE", + invoice_number: "Facture N° :", + date: "Date :", + due_date: "Échéance :", + description: "Description", + amount: "Montant", + total: "Total :", + vat_notice: "Association non soumise à la TVA selon l'Art. 10 LTVA", + payment_terms: "Cette facture est payable dans les 30 jours suivant sa réception.", + banking_info: "COORDONNÉES BANCAIRES", + bank: "Banque :", + account_holder: "Titulaire :", + account_notice: "Compte au nom d'un membre de l'association", + association_notice: "Association à but non lucratif non inscrite au registre du commerce", + ), + deutsch: ( + sender: "ABSENDER", + recipient: "EMPFÄNGER", + invoice: "RECHNUNG", + invoice_number: "Rechnungs-Nr.:", + date: "Datum:", + due_date: "Fälligkeitsdatum:", + description: "Beschreibung", + amount: "Betrag", + total: "Gesamtbetrag:", + vat_notice: "Verein nicht mehrwertsteuerpflichtig gemäß Art. 10 MWSTG", + payment_terms: "Diese Rechnung ist innerhalb von 30 Tagen nach Erhalt zu bezahlen.", + banking_info: "BANKVERBINDUNG", + bank: "Bank:", + account_holder: "Kontoinhaber:", + account_notice: "Konto auf den Namen eines Vereinsmitglieds", + association_notice: "Gemeinnütziger Verein, nicht im Handelsregister eingetragen", + ), + english: ( + sender: "FROM", + recipient: "TO", + invoice: "INVOICE", + invoice_number: "Invoice No.:", + date: "Date:", + due_date: "Due date:", + description: "Description", + amount: "Amount", + total: "Total:", + vat_notice: "Association not subject to VAT according to Art. 10 VAT Act", + payment_terms: "This invoice is payable within 30 days of receipt.", + banking_info: "BANKING DETAILS", + bank: "Bank:", + account_holder: "Account holder:", + account_notice: "Account in the name of an association member", + association_notice: "Non-profit association not registered in the commercial register", + ), +) + +// Sélection du dictionnaire en fonction de la langue +#let t = translations.at(language, default: translations.français) + +#set page( + margin: 2cm, + numbering: none, +) + +#set text(font: "Arial", size: 11pt) + +// En-tête de l'association +#align(center)[ + #text(weight: "bold", size: 14pt)[Cheap Motion Pictures ] + + #text(style: "italic")[#t.association_notice] +] + +#v(0.5cm) + +#grid( + columns: (1fr, 1fr), + row-gutter: 0.5cm, + + // Informations de l'émetteur + align(left)[ + #text(weight: "bold", size: 14pt)[#t.sender] + #v(0.2cm) + Cheap Motion Pictures + #v(0.1cm) + rue de l'ale 1 + #v(0.1cm) + 1003 Lausanne, Suisse + #v(0.1cm) + Tél: +41 77 471 11 34 + #v(0.1cm) + Email: contact\@cheapmo.ch + ], + + // Informations du destinataire + align(right)[ + #text(weight: "bold", size: 14pt)[#t.recipient] + #v(0.2cm) + #recipient_name + #v(0.1cm) + #recipient_adress + #v(0.1cm) + #recipient_postal_code #recipient_town, #recipient_country + + #if recipient_vat_number != none [VAT: #recipient_vat_number] else [ ]; + ], +) + +#v(0.5cm) + +// Titre et informations de la facture +#align(center)[ + #text(weight: "bold", size: 18pt)[#t.invoice] + #v(0.3cm) + #t.invoice_number 2025-#invoice_number - #t.date #current_date.display("[day]/[month]/[year]") + #v(0.2cm) + #t.due_date 30/03/2025 +] + +#v(0.5cm) + +// Tableau des prestations +#table( + columns: (auto, 1fr), + inset: 10pt, + align: (left, right), + table.header( + [*#t.description*], + [*#t.amount (#currency)* ], + ), + [Poong rental], [#amount], + [*#t.total*], [*#amount #currency*], +) + +#v(.5cm) + +// Informations complémentaires +#align(left)[ + #t.vat_notice + #v(0.3cm) + #t.payment_terms +] + +#v(0.5cm) + +// Coordonnées bancaires avec notification spéciale + #text(weight: "bold", size: 13pt)[#t.banking_info] + #v(0.2cm) + #t.bank Wise, Rue du Trône 100, 3rd floor, Brussels, 1050, Belgium + #v(0.1cm) + IBAN: BE22905094540247 + #v(0.1cm) + BIC/SWIFT: TRWIBEB1XXX + #v(0.1cm) + #t.account_holder Robin Szymczak + #v(0.2cm) + #t.account_notice + + +// Pied de page avec contact +#align(center)[ + #v(1cm) + #line(length: 100%, stroke: 0.5pt + gray) + #v(0.3cm) + Cheap Motion Pictures - 1 rue de l'ale - 1003 Lausanne - contact\@cheapmo.ch +] +''' + + # Ajouter la facture + invoice_id = db.add_invoice( + invoice_number=invoice_number, + client_id=client_id, + amount=data['amount'], + currency=data['currency'], + language=data['language'], + typst_content=typst_content + ) + + return jsonify({ + 'message': 'Facture créée avec succès', + 'invoice_id': invoice_id + }) + +if __name__ == '__main__': + app.run(debug=True) \ No newline at end of file diff --git a/static/dashboard.html b/static/dashboard.html new file mode 100644 index 0000000..9562edd --- /dev/null +++ b/static/dashboard.html @@ -0,0 +1,106 @@ + + + + + + Gestion des Factures + + + + +
+
+

Gestion des Factures

+ + Nouvelle Facture + +
+ + +
+
+

Total Factures

+

0

+
+
+

Montant Total

+

0 €

+
+
+

Factures Payées

+

0 €

+
+
+

Factures en Retard

+

0 €

+
+
+ + +
+
+
+ + +
+
+ + +
+
+ + +
+
+ +
+
+
+ + +
+ + + + + + + + + + + + + + +
N° FactureClientDateMontantStatutActions
+
+
+ + + + + + + \ No newline at end of file diff --git a/static/dashboard.js b/static/dashboard.js new file mode 100644 index 0000000..67ec2a3 --- /dev/null +++ b/static/dashboard.js @@ -0,0 +1,204 @@ +document.addEventListener("DOMContentLoaded", () => { + // Éléments du DOM + const invoicesList = document.getElementById("invoicesList"); + const statusFilter = document.getElementById("statusFilter"); + const dateFrom = document.getElementById("dateFrom"); + const dateTo = document.getElementById("dateTo"); + const applyFilters = document.getElementById("applyFilters"); + const modal = document.getElementById("invoiceModal"); + const modalTitle = document.getElementById("modalTitle"); + const modalContent = document.getElementById("modalContent"); + const closeModal = document.getElementById("closeModal"); + + // Charger les factures au démarrage + loadInvoices(); + + // Gestionnaire d'événements pour les filtres + applyFilters.addEventListener("click", () => { + loadInvoices({ + status: statusFilter.value, + date_from: dateFrom.value, + date_to: dateTo.value, + }); + }); + + // Gestionnaire d'événements pour fermer le modal + closeModal.addEventListener("click", () => { + modal.classList.add("hidden"); + }); + + // Fonction pour charger les factures + async function loadInvoices(filters = {}) { + try { + const response = await fetch( + "/api/invoices?" + new URLSearchParams(filters) + ); + const data = await response.json(); + + // Mettre à jour les statistiques + updateStatistics(data.statistics); + + // Mettre à jour la liste des factures + displayInvoices(data.invoices); + } catch (error) { + console.error("Erreur lors du chargement des factures:", error); + showMessage("Erreur lors du chargement des factures", "error"); + } + } + + // Fonction pour mettre à jour les statistiques + function updateStatistics(stats) { + document.getElementById("totalInvoices").textContent = stats.total_invoices; + document.getElementById( + "totalAmount" + ).textContent = `${stats.total_amount.toFixed(2)} €`; + document.getElementById( + "totalPaid" + ).textContent = `${stats.total_paid.toFixed(2)} €`; + document.getElementById( + "totalOverdue" + ).textContent = `${stats.total_overdue.toFixed(2)} €`; + } + + // Fonction pour afficher les factures + function displayInvoices(invoices) { + invoicesList.innerHTML = ""; + + invoices.forEach((invoice) => { + const row = document.createElement("tr"); + row.innerHTML = ` + + ${invoice.invoice_number} + + + ${invoice.client_name} + + + ${new Date(invoice.issue_date).toLocaleDateString()} + + + ${invoice.amount} ${invoice.currency} + + + + ${getStatusText(invoice.status)} + + + + + + + + `; + invoicesList.appendChild(row); + }); + } + + // Fonction pour obtenir la classe CSS du statut + function getStatusClass(status) { + switch (status) { + case "paid": + return "bg-green-100 text-green-800"; + case "overdue": + return "bg-red-100 text-red-800"; + default: + return "bg-yellow-100 text-yellow-800"; + } + } + + // Fonction pour obtenir le texte du statut + function getStatusText(status) { + switch (status) { + case "paid": + return "Payée"; + case "overdue": + return "En retard"; + default: + return "Émise"; + } + } + + // Fonction pour afficher une facture + window.viewInvoice = async (invoiceId) => { + try { + const response = await fetch(`/api/invoices/${invoiceId}`); + const invoice = await response.json(); + + modalTitle.textContent = `Facture ${invoice.invoice_number}`; + modalContent.innerHTML = ` +
+

Client: ${invoice.client_name}

+

Date: ${new Date( + invoice.issue_date + ).toLocaleDateString()}

+

Montant: ${invoice.amount} ${ + invoice.currency + }

+

Statut: ${getStatusText( + invoice.status + )}

+

Adresse: ${invoice.address}

+

${invoice.postal_code} ${invoice.town}

+

${invoice.country}

+
+ `; + + modal.classList.remove("hidden"); + } catch (error) { + console.error("Erreur lors du chargement de la facture:", error); + showMessage("Erreur lors du chargement de la facture", "error"); + } + }; + + // Fonction pour mettre à jour le statut d'une facture + window.updateStatus = async (invoiceId) => { + try { + await fetch(`/api/invoices/${invoiceId}/status`, { + method: "PUT", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ status: "paid" }), + }); + + loadInvoices(); + showMessage("Statut de la facture mis à jour"); + } catch (error) { + console.error("Erreur lors de la mise à jour du statut:", error); + showMessage("Erreur lors de la mise à jour du statut", "error"); + } + }; + + // Fonction pour télécharger une facture + window.downloadInvoice = async (invoiceId) => { + try { + const response = await fetch(`/api/invoices/${invoiceId}/download`); + const blob = await response.blob(); + + const url = window.URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = `invoice_${invoiceId}.pdf`; + document.body.appendChild(a); + a.click(); + window.URL.revokeObjectURL(url); + document.body.removeChild(a); + } catch (error) { + console.error("Erreur lors du téléchargement de la facture:", error); + showMessage("Erreur lors du téléchargement de la facture", "error"); + } + }; +}); diff --git a/static/generator.html b/static/generator.html new file mode 100644 index 0000000..dcbb169 --- /dev/null +++ b/static/generator.html @@ -0,0 +1,99 @@ + + + + + + Générateur de Factures + + + + +
+
+

Générateur de Factures

+ + Dashboard + +
+ +
+
+ +
+

Informations générales

+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+
+ + +
+

Destinataire

+ +
+ + +
+ +
+ + +
+ +
+
+ + +
+
+ + +
+
+ +
+ + +
+ +
+ + +
+
+
+ +
+ +
+
+
+ + + \ No newline at end of file diff --git a/static/preview.html b/static/preview.html new file mode 100644 index 0000000..c12dad4 --- /dev/null +++ b/static/preview.html @@ -0,0 +1,68 @@ + + + + + + Prévisualisation de la Facture + + + + +
+
+

Prévisualisation de la Facture

+
+ + Retour + + +
+
+ +
+
+ +
+

Émetteur

+
+

Cheap Motion Pictures

+

rue de l'ale 1

+

1003 Lausanne, Suisse

+

Tél: +41 77 471 11 34

+

Email: contact@cheapmo.ch

+
+
+ + +
+

Destinataire

+
+ +
+
+
+ +
+

Détails de la Facture

+
+ +
+
+ +
+

Informations de Paiement

+
+

Banque: Wise

+

IBAN: BE22905094540247

+

BIC/SWIFT: TRWIBEB1XXX

+

Titulaire: Robin Szymczak

+
+
+
+
+ + + + \ No newline at end of file diff --git a/static/preview.js b/static/preview.js new file mode 100644 index 0000000..fca18a3 --- /dev/null +++ b/static/preview.js @@ -0,0 +1,80 @@ +document.addEventListener("DOMContentLoaded", function () { + // Récupérer les données du formulaire depuis l'URL + const urlParams = new URLSearchParams(window.location.search); + const formData = JSON.parse(decodeURIComponent(urlParams.get("data"))); + + // Afficher les informations du destinataire + const recipientInfo = document.getElementById("recipientInfo"); + recipientInfo.innerHTML = ` +

${formData.recipient_name}

+

${formData.recipient_address}

+

${formData.recipient_postal_code} ${formData.recipient_town}

+

${formData.recipient_country}

+ ${ + formData.recipient_vat_number + ? `

TVA: ${formData.recipient_vat_number}

` + : "" + } + `; + + // Afficher les détails de la facture + const invoiceDetails = document.getElementById("invoiceDetails"); + invoiceDetails.innerHTML = ` +
+
+

Numéro de facture

+

${formData.invoice_number}

+
+
+

Date

+

${new Date().toLocaleDateString()}

+
+
+

Langue

+

${formData.language}

+
+
+

Montant

+

${formData.amount} ${ + formData.currency + }

+
+
+ `; + + // Gérer la validation de la facture + document + .getElementById("validateInvoice") + .addEventListener("click", async function () { + try { + console.log("Envoi des données à l'API:", formData); + + const response = await fetch("/api/invoices", { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(formData), + }); + + console.log("Réponse reçue:", response.status); + + if (response.ok) { + const result = await response.json(); + console.log("Facture créée avec succès:", result); + alert("Facture créée avec succès !"); + window.location.href = "/dashboard"; + } else { + const error = await response.json(); + console.error("Erreur API:", error); + alert( + "Erreur lors de la création de la facture : " + + (error.error || "Erreur inconnue") + ); + } + } catch (error) { + console.error("Erreur lors de la requête:", error); + alert("Erreur lors de la création de la facture : " + error.message); + } + }); +}); diff --git a/static/script.js b/static/script.js new file mode 100644 index 0000000..9015dcf --- /dev/null +++ b/static/script.js @@ -0,0 +1,65 @@ +document.addEventListener("DOMContentLoaded", () => { + const form = document.getElementById("invoiceForm"); + + // Fonction pour afficher les messages + function showMessage(message, type = "success") { + const messageDiv = document.createElement("div"); + messageDiv.className = `${type}-message`; + messageDiv.textContent = message; + document.body.appendChild(messageDiv); + + // Afficher le message + setTimeout(() => messageDiv.classList.add("show"), 100); + + // Supprimer le message après 3 secondes + setTimeout(() => { + messageDiv.classList.remove("show"); + setTimeout(() => messageDiv.remove(), 300); + }, 3000); + } + + // Fonction pour valider le formulaire + function validateForm(formData) { + const requiredFields = [ + "invoice_number", + "amount", + "recipient_name", + "recipient_address", + "recipient_postal_code", + "recipient_town", + "recipient_country", + ]; + + for (const field of requiredFields) { + if (!formData.get(field)) { + showMessage(`Le champ ${field} est requis`, "error"); + return false; + } + } + + return true; + } + + // Gestionnaire de soumission du formulaire + form.addEventListener("submit", function (e) { + e.preventDefault(); + + // Récupérer les données du formulaire + 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, + }; + + // Rediriger vers la page de prévisualisation avec les données + const queryString = encodeURIComponent(JSON.stringify(formData)); + window.location.href = `/preview?data=${queryString}`; + }); +}); diff --git a/static/styles.css b/static/styles.css new file mode 100644 index 0000000..48ea30f --- /dev/null +++ b/static/styles.css @@ -0,0 +1,52 @@ +/* Styles personnalisés */ +input[type="text"], +input[type="number"], +select { + @apply border border-gray-300 rounded-md px-3 py-2; +} + +/* Animation de chargement */ +.loading { + position: relative; + pointer-events: none; +} + +.loading::after { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 20px; + height: 20px; + margin: -10px 0 0 -10px; + border: 2px solid #ffffff; + border-top-color: transparent; + border-radius: 50%; + animation: loading 0.6s linear infinite; +} + +@keyframes loading { + to { + transform: rotate(360deg); + } +} + +/* Message de succès */ +.success-message { + @apply fixed top-4 right-4 bg-green-500 text-white px-6 py-3 rounded-md shadow-lg transform transition-transform duration-300; + transform: translateX(120%); +} + +.success-message.show { + transform: translateX(0); +} + +/* Message d'erreur */ +.error-message { + @apply fixed top-4 right-4 bg-red-500 text-white px-6 py-3 rounded-md shadow-lg transform transition-transform duration-300; + transform: translateX(120%); +} + +.error-message.show { + transform: translateX(0); +} \ No newline at end of file