invoice-manager/static/accounting.js

453 lines
14 KiB
JavaScript

document.addEventListener("DOMContentLoaded", () => {
// Éléments du DOM
const yearFilter = document.getElementById("yearFilter");
const categoryFilter = document.getElementById("categoryFilter");
const typeFilter = document.getElementById("typeFilter");
const applyFilters = document.getElementById("applyFilters");
const transactionsList = document.getElementById("transactionsList");
const totalRevenue = document.getElementById("totalRevenue");
const totalExpenses = document.getElementById("totalExpenses");
const balanceAmount = document.getElementById("balanceAmount");
const revenuesByCategoryList = document.getElementById(
"revenuesByCategoryList"
);
const expensesByCategoryList = document.getElementById(
"expensesByCategoryList"
);
// Boutons d'onglets
const tabButtons = document.querySelectorAll(".tab-button");
const tabContents = document.querySelectorAll(".tab-content");
// Modal d'importation
const importButton = document.getElementById("importButton");
const importModal = document.getElementById("importModal");
const closeImportModal = document.getElementById("closeImportModal");
const importRevenues = document.getElementById("importRevenues");
const importExpenses = document.getElementById("importExpenses");
// Variables pour les graphiques
let revenuesChart = null;
let expensesChart = null;
// Initialiser la page
initPage();
// Gestionnaire d'événements pour les filtres
applyFilters.addEventListener("click", () => {
loadData({
year: yearFilter.value,
category: categoryFilter.value,
type: typeFilter.value,
});
});
// Gestionnaires d'événements pour les onglets
tabButtons.forEach((button) => {
button.addEventListener("click", () => {
const tabId = button.id.replace("tab", "").toLowerCase();
switchTab(tabId);
});
});
// Gestionnaires d'événements pour le modal d'importation
importButton.addEventListener("click", () => {
importModal.classList.remove("hidden");
});
closeImportModal.addEventListener("click", () => {
importModal.classList.add("hidden");
});
importRevenues.addEventListener("click", () => {
importData("revenue");
});
importExpenses.addEventListener("click", () => {
importData("expense");
});
// Fonction d'initialisation
function initPage() {
// Années pour le filtre (année courante et les deux précédentes)
const currentYear = new Date().getFullYear();
yearFilter.innerHTML = `<option value="">Toutes</option>`;
for (let year = currentYear; year >= currentYear - 2; year--) {
yearFilter.innerHTML += `<option value="${year}">${year}</option>`;
}
// Charger les données initiales
loadData();
// Générer les couleurs pour les graphiques
generateChartColors();
}
// Fonction pour charger les données
async function loadData(filters = {}) {
try {
const response = await fetch(
"/api/accounting?" + new URLSearchParams(filters)
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// Mettre à jour les statistiques
updateStatistics(data.statistics);
// Mettre à jour les transactions
displayTransactions(data.transactions);
// Mettre à jour les catégories de filtres
updateCategoryFilters(data.categories);
// Mettre à jour les graphiques
updateCharts(data.statistics);
} catch (error) {
console.error("Erreur lors du chargement des données:", error);
showMessage("Erreur lors du chargement des données", "error");
}
}
// Fonction pour importer des données
async function importData(type) {
try {
const response = await fetch(`/api/accounting/import/${type}`, {
method: "POST",
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
showMessage(result.message || "Importation réussie");
importModal.classList.add("hidden");
// Recharger les données
loadData();
} catch (error) {
console.error("Erreur lors de l'importation:", error);
showMessage("Erreur lors de l'importation", "error");
}
}
// Fonction pour mettre à jour les statistiques
function updateStatistics(stats) {
totalRevenue.textContent = `${stats.total_revenue.toFixed(2)}`;
totalExpenses.textContent = `${stats.total_expenses.toFixed(2)}`;
const balance = stats.balance;
balanceAmount.textContent = `${Math.abs(balance).toFixed(2)}`;
if (balance >= 0) {
balanceAmount.classList.remove("text-red-500");
balanceAmount.classList.add("text-green-500");
} else {
balanceAmount.classList.remove("text-green-500");
balanceAmount.classList.add("text-red-500");
}
}
// Fonction pour mettre à jour les catégories du filtre
function updateCategoryFilters(categories) {
categoryFilter.innerHTML = '<option value="">Toutes</option>';
categories.forEach((category) => {
categoryFilter.innerHTML += `<option value="${category}">${category}</option>`;
});
}
// Fonction pour afficher les transactions
function displayTransactions(transactions) {
transactionsList.innerHTML = "";
transactions.forEach((transaction) => {
const row = document.createElement("tr");
const date = new Date(transaction.date);
const formattedDate = date.toLocaleDateString();
const isRevenue = transaction.type === "revenue";
const amount = `${isRevenue ? "+" : "-"} ${Math.abs(
transaction.amount
).toFixed(2)}`;
const amountClass = isRevenue ? "text-green-500" : "text-red-500";
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${formattedDate}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${transaction.description}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium ${amountClass}">
${amount}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${transaction.category || "Non catégorisé"}
</td>
<td class="px-6 py-4 whitespace-nowrap">
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full
${
isRevenue
? "bg-green-100 text-green-800"
: "bg-red-100 text-red-800"
}">
${isRevenue ? "Revenu" : "Dépense"}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium">
<button onclick="editTransaction(${
transaction.id
})" class="text-blue-600 hover:text-blue-900 mr-3">
Éditer
</button>
<button onclick="deleteTransaction(${
transaction.id
})" class="text-red-600 hover:text-red-900">
Supprimer
</button>
</td>
`;
transactionsList.appendChild(row);
});
if (transactions.length === 0) {
transactionsList.innerHTML = `
<tr>
<td colspan="6" class="px-6 py-4 text-center text-sm text-gray-500">
Aucune transaction trouvée
</td>
</tr>
`;
}
}
// Fonction pour changer d'onglet
function switchTab(tabId) {
// Mettre à jour les classes des boutons
tabButtons.forEach((button) => {
const buttonTabId = button.id.replace("tab", "").toLowerCase();
if (buttonTabId === tabId) {
button.classList.add("active", "border-blue-500", "text-blue-600");
button.classList.remove("border-transparent", "text-gray-500");
} else {
button.classList.remove("active", "border-blue-500", "text-blue-600");
button.classList.add("border-transparent", "text-gray-500");
}
});
// Afficher le contenu de l'onglet sélectionné
tabContents.forEach((content) => {
const contentId = content.id;
if (contentId === `${tabId}Content`) {
content.classList.remove("hidden");
} else {
content.classList.add("hidden");
}
});
}
// Génération de couleurs pour les graphiques
function generateChartColors(count = 10) {
const colors = [
"#4F46E5",
"#10B981",
"#F59E0B",
"#EF4444",
"#EC4899",
"#8B5CF6",
"#06B6D4",
"#84CC16",
"#F97316",
"#6366F1",
];
// Si on a besoin de plus de couleurs, on les génère aléatoirement
if (count > colors.length) {
for (let i = colors.length; i < count; i++) {
const r = Math.floor(Math.random() * 200);
const g = Math.floor(Math.random() * 200);
const b = Math.floor(Math.random() * 200);
colors.push(`rgb(${r}, ${g}, ${b})`);
}
}
return colors;
}
// Mise à jour des graphiques
function updateCharts(statistics) {
updateRevenuesChart(statistics.revenue_by_category);
updateExpensesChart(statistics.expenses_by_category);
}
// Mise à jour du graphique des revenus
function updateRevenuesChart(revenuesByCategory) {
const ctx = document.getElementById("revenuesByCategoryChart");
const colors = generateChartColors(Object.keys(revenuesByCategory).length);
// Détruire le graphique précédent s'il existe
if (revenuesChart) {
revenuesChart.destroy();
}
// Créer le nouveau graphique
revenuesChart = new Chart(ctx, {
type: "pie",
data: {
labels: Object.keys(revenuesByCategory),
datasets: [
{
data: Object.values(revenuesByCategory),
backgroundColor: colors,
borderWidth: 1,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "right",
},
tooltip: {
callbacks: {
label: function (context) {
const value = context.raw;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${value.toFixed(2)} € (${percentage}%)`;
},
},
},
},
},
});
// Mettre à jour la liste des revenus par catégorie
updateCategoryList(revenuesByCategoryList, revenuesByCategory);
}
// Mise à jour du graphique des dépenses
function updateExpensesChart(expensesByCategory) {
const ctx = document.getElementById("expensesByCategoryChart");
const colors = generateChartColors(Object.keys(expensesByCategory).length);
// Détruire le graphique précédent s'il existe
if (expensesChart) {
expensesChart.destroy();
}
// Créer le nouveau graphique
expensesChart = new Chart(ctx, {
type: "pie",
data: {
labels: Object.keys(expensesByCategory),
datasets: [
{
data: Object.values(expensesByCategory),
backgroundColor: colors,
borderWidth: 1,
},
],
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
position: "right",
},
tooltip: {
callbacks: {
label: function (context) {
const value = context.raw;
const total = context.dataset.data.reduce((a, b) => a + b, 0);
const percentage = ((value / total) * 100).toFixed(1);
return `${value.toFixed(2)} € (${percentage}%)`;
},
},
},
},
},
});
// Mettre à jour la liste des dépenses par catégorie
updateCategoryList(expensesByCategoryList, expensesByCategory);
}
// Mise à jour des listes de catégories
function updateCategoryList(listElement, categoryData) {
listElement.innerHTML = "";
const total = Object.values(categoryData).reduce((a, b) => a + b, 0);
Object.entries(categoryData)
.sort((a, b) => b[1] - a[1]) // Trier par montant décroissant
.forEach(([category, amount]) => {
const percentage = ((amount / total) * 100).toFixed(1);
const row = document.createElement("tr");
row.innerHTML = `
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${category}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${amount.toFixed(2)}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${percentage}%
</td>
`;
listElement.appendChild(row);
});
}
// Fonctions globales pour l'édition et la suppression
window.editTransaction = async (transactionId) => {
alert("Fonctionnalité d'édition à implémenter");
// TODO: Implémenter l'édition des transactions
};
window.deleteTransaction = async (transactionId) => {
if (confirm("Êtes-vous sûr de vouloir supprimer cette transaction ?")) {
try {
const response = await fetch(
`/api/accounting/transaction/${transactionId}`,
{
method: "DELETE",
}
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
showMessage("Transaction supprimée avec succès");
loadData();
} catch (error) {
console.error("Erreur lors de la suppression:", error);
showMessage("Erreur lors de la suppression", "error");
}
}
};
// Fonction pour afficher des messages à l'utilisateur
function showMessage(message, type = "success") {
// On pourrait implémenter un système de notification plus élaboré ici
alert(message);
}
});