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:
robin 2025-05-31 17:41:17 +02:00
parent ee48d0dbe6
commit 22d6330205
10 changed files with 601 additions and 38 deletions

83
README.md Normal file
View 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.

View File

@ -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

Binary file not shown.

View File

@ -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 [

View File

@ -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>

View File

@ -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();

View File

@ -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>

View File

@ -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);
}
}
});

View File

@ -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
addInvoiceItem();
// 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}`;

View File

@ -49,4 +49,27 @@ 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;
}