{% extends 'sidebar.twig' %}
{% block title %}Dashboard Trésorerie{% endblock %}
{% block page_emoji %}💰{% endblock %}
{% block page_titre %}Trésorerie{% endblock %}
{% block page_info %}Vue d'ensemble de vos revenus{% endblock %}
{% block admin_content %}
<style>
.dark .dark\:bg-gray-800 {
background-color: #1f2937;
}
.dark .dark\:text-white {
color: #ffffff;
}
.dark .dark\:text-gray-400 {
color: #9ca3af;
}
.transition-all {
transition-property: all;
transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
transition-duration: 150ms;
}
.hover\:shadow-md:hover {
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1), 0 2px 4px -1px rgba(0, 0, 0, 0.06);
}
</style>
{% set moisFr = {
'1': 'Janvier',
'2': 'Février',
'3': 'Mars',
'4': 'Avril',
'5': 'Mai',
'6': 'Juin',
'7': 'Juillet',
'8': 'Août',
'9': 'Septembre',
'10': 'Octobre',
'11': 'Novembre',
'12': 'Décembre'
} %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div class="space-y-8">
{# Première section : KPIs principaux #}
{# Première section : KPIs principaux #}
<div class="grid grid-cols-1 md:grid-cols-5 gap-4">
{# CA Annuel #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">CA Annuel {{ "now"|date('Y') }}</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white mt-2">
{{ caAnnuel|number_format(2, ',', ' ') }} €
</p>
</div>
<div class="p-3 bg-blue-50 dark:bg-blue-900/20 rounded-full">
<svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
</div>
</div>
{# CA Mois en cours #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">
CA {{ moisFr[("now"|date('n'))] }}
</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white mt-2">
{{ caMoisEnCours|number_format(2, ',', ' ') }} €
</p>
</div>
<div class="p-3 bg-green-50 dark:bg-green-900/20 rounded-full">
<svg class="w-6 h-6 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"></path>
</svg>
</div>
</div>
</div>
{# Card CB #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm hover:shadow-md transition-all cursor-pointer" onclick="openModal('cb')">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Carte Bancaire</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white mt-2">
{{ paiementCB|number_format(2, ',', ' ') }} €
</p>
</div>
<div class="p-3 bg-indigo-50 dark:bg-indigo-900/20 rounded-full">
<svg class="w-6 h-6 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 10h18M7 15h1m4 0h1m-7 4h12a3 3 0 003-3V8a3 3 0 00-3-3H6a3 3 0 00-3 3v8a3 3 0 003 3z"></path>
</svg>
</div>
</div>
</div>
{# Card Acomptes #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm hover:shadow-md transition-all cursor-pointer" onclick="openModal('acompte')">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Planity</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white mt-2">
{{ acomptes|number_format(2, ',', ' ') }} €
</p>
</div>
<div class="p-3 bg-purple-50 dark:bg-purple-900/20 rounded-full">
<svg class="w-6 h-6 text-purple-600 dark:text-purple-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"></path>
</svg>
</div>
</div>
</div>
{# Card Espèces #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm hover:shadow-md transition-all cursor-pointer" onclick="openModal('especes')">
<div class="flex items-center justify-between">
<div>
<p class="text-sm font-medium text-gray-600 dark:text-gray-400">Espèces</p>
<p class="text-2xl font-bold text-gray-900 dark:text-white mt-2">
{{ paiementEspeces|number_format(2, ',', ' ') }} €
</p>
</div>
<div class="p-3 bg-yellow-50 dark:bg-yellow-900/20 rounded-full">
<svg class="w-6 h-6 text-yellow-600 dark:text-yellow-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 9V7a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2m2 4h10a2 2 0 002-2v-6a2 2 0 00-2-2H9a2 2 0 00-2 2v6a2 2 0 002 2z"></path>
</svg>
</div>
</div>
</div>
</div>
{# Graphique d'évolution #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-6">Évolution mensuelle du CA</h3>
<div class="h-[400px]">
<canvas id="evolutionChart"></canvas>
</div>
</div>
{# Comparaison périodes #}
<div class="bg-white dark:bg-gray-800 rounded-xl p-6 shadow-sm">
<h3 class="text-lg font-medium text-gray-900 dark:text-white mb-6">Comparaison des périodes</h3>
<div class="grid grid-cols-1 md:grid-cols-2 gap-8 relative">
{# Période actuelle #}
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-xl p-6">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<svg class="w-5 h-5 text-blue-600 dark:text-blue-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/>
</svg>
</div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
1-{{ "now"|date('j') }} {{ moisFr[("now"|date('n'))] }}
</span>
</div>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{{ comparaisonMois.mois_actuel|number_format(2, ',', ' ') }} €
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">Période en cours</p>
</div>
{# Période précédente #}
<div class="bg-gray-50 dark:bg-gray-700/50 rounded-xl p-6">
<div class="flex items-center space-x-3 mb-4">
<div class="p-2 bg-gray-100 dark:bg-gray-600 rounded-lg">
<svg class="w-5 h-5 text-gray-600 dark:text-gray-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
</div>
<span class="text-sm font-medium text-gray-900 dark:text-white">
1-{{ "now"|date('j') }} {{ moisFr[("first day of last month"|date('n'))] }}
</span>
</div>
<p class="text-2xl font-bold text-gray-900 dark:text-white">
{{ comparaisonMois.mois_precedent|number_format(2, ',', ' ') }} €
</p>
<p class="text-sm text-gray-500 dark:text-gray-400 mt-2">Même période mois précédent</p>
</div>
</div>
{% set evolution = ((comparaisonMois.mois_actuel - comparaisonMois.mois_precedent) / comparaisonMois.mois_precedent * 100)|round %}
<div class="mt-6 p-4 rounded-xl {% if evolution > 0 %}bg-green-50 dark:bg-green-900/20{% else %}bg-red-50 dark:bg-red-900/20{% endif %}">
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="p-2 rounded-lg {% if evolution > 0 %}bg-green-100 dark:bg-green-800{% else %}bg-red-100 dark:bg-red-800{% endif %}">
<svg class="w-5 h-5 {% if evolution > 0 %}text-green-600 dark:text-green-400{% else %}text-red-600 dark:text-red-400{% endif %}" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="{% if evolution > 0 %}M13 7h8m0 0v8m0-8l-8 8-4-4-6 6{% else %}M13 17h8m0 0V9m0 8l-8-8-4 4-6-6{% endif %}"/>
</svg>
</div>
<div>
<p class="font-medium {% if evolution > 0 %}text-green-800 dark:text-green-400{% else %}text-red-800 dark:text-red-400{% endif %}">
{% if evolution > 0 %}
Progression de {{ evolution }}%
{% else %}
Baisse de {{ evolution|abs }}%
{% endif %}
</p>
<p class="text-sm text-gray-600 dark:text-gray-400">par rapport au mois précédent</p>
</div>
</div>
<div class="text-2xl">
{% if evolution > 0 %}📈{% else %}📉{% endif %}
</div>
</div>
</div>
{# Info supplémentaire #}
<div class="mt-4 flex items-center space-x-2 text-sm text-gray-600 dark:text-gray-400">
<svg class="w-4 h-4 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
</svg>
<span>Comparaison basée sur les {{ "now"|date('j') }} premiers jours de chaque mois</span>
</div>
</div>
</div>
</div>
<!-- Modals -->
<div id="modalContainer" class="hidden fixed inset-0 bg-gray-600 bg-opacity-50 overflow-y-auto h-full w-full z-50">
<div class="relative top-20 mx-auto p-5 border w-11/12 max-w-4xl shadow-lg rounded-md bg-white">
<div class="flex justify-between items-center mb-4">
<h3 class="text-lg font-medium text-gray-900" id="modalTitle"></h3>
<button onclick="closeModal()" class="text-gray-400 hover:text-gray-500">
<svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
<div id="modalContent" class="mt-4"></div>
</div>
</div>
<style>
.modal-enter {
animation: fadeIn 0.3s ease-out;
}
@keyframes fadeIn {
from {
opacity: 0;
transform: translateY(-20px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
</style>
{% endblock %}
{% block javascripts %}
{{ parent() }}
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
const ctx = document.getElementById('evolutionChart').getContext('2d');
const data = {{ evolutionMensuelle|json_encode|raw }};
const months = ['Janvier', 'Février', 'Mars', 'Avril', 'Mai', 'Juin',
'Juillet', 'Août', 'Septembre', 'Octobre', 'Novembre', 'Décembre'];
new Chart(ctx, {
type: 'line',
data: {
labels: data.map(item => months[item.mois - 1]),
datasets: [{
label: 'Chiffre d\'affaires',
data: data.map(item => item.total),
borderColor: '#4f46e5',
backgroundColor: 'rgba(79, 70, 229, 0.1)',
tension: 0.4,
fill: true,
pointBackgroundColor: '#4f46e5',
pointBorderColor: '#ffffff',
pointBorderWidth: 2,
pointRadius: 4,
pointHoverRadius: 6
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
display: false
},
tooltip: {
backgroundColor: '#1f2937',
titleColor: '#ffffff',
bodyColor: '#ffffff',
padding: 12,
displayColors: false,
callbacks: {
label: function(context) {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR'
}).format(context.parsed.y);
}
}
}
},
scales: {
x: {
grid: {
display: false
},
ticks: {
color: '#6b7280'
}
},
y: {
beginAtZero: true,
grid: {
color: '#e5e7eb'
},
ticks: {
color: '#6b7280',
callback: function(value) {
return new Intl.NumberFormat('fr-FR', {
style: 'currency',
currency: 'EUR',
maximumFractionDigits: 0
}).format(value);
}
}
}
}
}
});
});
function openModal(type) {
const modal = document.getElementById('modalContainer');
const title = document.getElementById('modalTitle');
modal.classList.remove('hidden');
switch(type) {
case 'cb':
title.textContent = 'Détails des paiements par Carte Bancaire';
loadDetails('cb');
break;
case 'acompte':
title.textContent = 'Détails des acomptes Planity';
loadDetails('acompte');
break;
case 'especes':
title.textContent = 'Détails des paiements en espèces';
loadDetails('especes');
break;
}
}
function closeModal() {
document.getElementById('modalContainer').classList.add('hidden');
}
function loadDetails(type) {
const routes = {
'cb': '{{ path('app_dashboard_cb_details') }}',
'acompte': '{{ path('app_dashboard_acompte_details') }}',
'especes': '{{ path('app_dashboard_especes_details') }}'
};
fetch(routes[type])
.then(response => response.json())
.then(data => {
document.getElementById('modalContent').innerHTML = generateTableHTML(data);
});
}
function generateTableHTML(data) {
return `
<div class="mb-4 grid grid-cols-1 sm:grid-cols-3 gap-4">
<!-- Filtres -->
<div class="relative">
<input type="text"
id="dateFilter"
placeholder="Filtrer par date"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
onkeyup="filterTable()">
</div>
<div class="relative">
<input type="text"
id="clientFilter"
placeholder="Filtrer par client"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
onkeyup="filterTable()">
</div>
<div class="relative">
<input type="text"
id="montantFilter"
placeholder="Filtrer par montant"
class="w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:ring-indigo-500 focus:border-indigo-500"
onkeyup="filterTable()">
</div>
</div>
<div class="overflow-x-auto">
<div class="inline-block min-w-full align-middle">
<div class="overflow-hidden shadow-sm ring-1 ring-black ring-opacity-5">
<!-- Version mobile -->
<div class="block sm:hidden" id="mobileContent">
${generateMobileRows(data)}
</div>
<!-- Version desktop -->
<table class="hidden sm:table min-w-full divide-y divide-gray-300">
<thead class="bg-gray-50">
<tr>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 cursor-pointer" onclick="sortTable(0)">
<div class="flex items-center">
Date
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
</svg>
</div>
</th>
<th class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900 cursor-pointer" onclick="sortTable(1)">
<div class="flex items-center">
Client
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
</svg>
</div>
</th>
<th class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900 cursor-pointer" onclick="sortTable(2)">
<div class="flex items-center justify-end">
Montant
<svg class="w-4 h-4 ml-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16V4m0 0L3 8m4-4l4 4m6 0v12m0 0l4-4m-4 4l-4-4"/>
</svg>
</div>
</th>
</tr>
</thead>
<tbody class="divide-y divide-gray-200 bg-white" id="tableBody">
${generateTableRows(data)}
</tbody>
<tfoot class="bg-gray-50">
<tr>
<td colspan="2" class="px-3 py-3.5 text-left text-sm font-semibold text-gray-900">Total</td>
<td class="px-3 py-3.5 text-right text-sm font-semibold text-gray-900" id="totalMontant">
${calculateTotal(data)} €
</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
`;
}
// Fonction pour générer les lignes du tableau
function generateTableRows(data) {
return data.map(item => `
<tr class="hover:bg-gray-50">
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-500">${item.date}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-gray-900">${item.client}</td>
<td class="whitespace-nowrap px-3 py-4 text-sm text-right font-medium text-gray-900">${item.montant} €</td>
</tr>
`).join('');
}
// Fonction pour générer les lignes mobiles
function generateMobileRows(data) {
return data.map(item => `
<div class="bg-white border-b border-gray-200 p-4">
<div class="flex justify-between items-center mb-2">
<div class="font-medium text-gray-900">${item.client}</div>
<div class="text-lg font-bold text-gray-900">${item.montant} €</div>
</div>
<div class="text-sm text-gray-500">${item.date}</div>
</div>
`).join('');
}
// Fonction pour calculer le total
function calculateTotal(data) {
return data.reduce((sum, item) => sum + parseFloat(item.montant.replace(',', '.')), 0)
.toFixed(2)
.replace('.', ',');
}
// Fonction de filtrage
function filterTable() {
const dateFilter = document.getElementById('dateFilter').value.toLowerCase();
const clientFilter = document.getElementById('clientFilter').value.toLowerCase();
const montantFilter = document.getElementById('montantFilter').value.toLowerCase();
const rows = document.getElementById('tableBody').getElementsByTagName('tr');
let totalMontant = 0;
for (let row of rows) {
const dateCell = row.cells[0].textContent.toLowerCase();
const clientCell = row.cells[1].textContent.toLowerCase();
const montantCell = row.cells[2].textContent.toLowerCase();
const dateMatch = dateCell.includes(dateFilter);
const clientMatch = clientCell.includes(clientFilter);
const montantMatch = montantCell.includes(montantFilter);
if (dateMatch && clientMatch && montantMatch) {
row.style.display = '';
totalMontant += parseFloat(montantCell.replace('€', '').replace(',', '.').trim());
} else {
row.style.display = 'none';
}
}
// Mise à jour du total
document.getElementById('totalMontant').textContent =
totalMontant.toFixed(2).replace('.', ',') + ' €';
}
// Fonction de tri
let sortDirection = 1;
function sortTable(columnIndex) {
const table = document.getElementById('tableBody');
const rows = Array.from(table.getElementsByTagName('tr'));
rows.sort((a, b) => {
let aValue = a.cells[columnIndex].textContent;
let bValue = b.cells[columnIndex].textContent;
// Pour les montants, convertir en nombres
if (columnIndex === 2) {
aValue = parseFloat(aValue.replace('€', '').replace(',', '.').trim());
bValue = parseFloat(bValue.replace('€', '').replace(',', '.').trim());
}
if (aValue < bValue) return -1 * sortDirection;
if (aValue > bValue) return 1 * sortDirection;
return 0;
});
// Inverser la direction pour le prochain tri
sortDirection *= -1;
// Réinsérer les lignes triées
rows.forEach(row => table.appendChild(row));
}
</script>
{% endblock %}