{% extends 'base.html.twig' %}
{% block title %}Tableau de bord{% endblock %}
{% block body %}
<style>
.dashboard-page {
--dashboard-primary: #405189;
--dashboard-success: #0ab39c;
--dashboard-danger: #f06548;
--dashboard-warning: #f7b84b;
--dashboard-info: #299cdb;
--dashboard-border: #e7eaf3;
--dashboard-muted: #74788d;
}
.dashboard-hero {
border: 1px solid var(--dashboard-border);
border-radius: 8px;
background: linear-gradient(135deg, #ffffff 0%, #f8fbff 55%, #f4fbf9 100%);
box-shadow: 0 12px 28px rgba(15, 34, 58, .07);
padding: 24px;
position: relative;
overflow: hidden;
}
.dashboard-hero::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: 5px;
background: linear-gradient(180deg, var(--dashboard-primary), var(--dashboard-success));
}
.dashboard-hero-kicker {
color: var(--dashboard-primary);
font-size: 12px;
font-weight: 700;
letter-spacing: .08em;
text-transform: uppercase;
}
.dashboard-hero-title {
color: #24324b;
font-size: 24px;
font-weight: 700;
}
.dashboard-hero-subtitle {
color: var(--dashboard-muted);
max-width: 720px;
}
.dashboard-hero-pill {
border: 1px solid rgba(64, 81, 137, .12);
border-radius: 999px;
background: rgba(255, 255, 255, .78);
color: #344050;
font-size: 13px;
font-weight: 600;
padding: 7px 12px;
}
.dashboard-reset-btn {
border: 1px solid var(--dashboard-border);
border-radius: 6px;
background: #fff;
color: #344050;
font-weight: 600;
min-height: 40px;
}
.dashboard-reset-btn:hover {
border-color: rgba(64, 81, 137, .28);
color: var(--dashboard-primary);
}
.dashboard-filter-card,
.dashboard-kpi-card,
.dashboard-type-card,
.dashboard-chart-card,
.dashboard-table-card {
border: 1px solid var(--dashboard-border);
border-radius: 8px;
box-shadow: 0 10px 24px rgba(15, 34, 58, .06);
}
.dashboard-filter-card {
overflow: visible;
}
.dashboard-filter-card .card-body {
padding: 18px;
}
.dashboard-filter-card .form-label {
color: #344050;
font-size: 12px;
font-weight: 700;
letter-spacing: .03em;
margin-bottom: .45rem;
text-transform: uppercase;
}
.dashboard-filter-card .form-control,
.dashboard-filter-card .form-select,
.dashboard-filter-card .select2-container--default .select2-selection--single {
border-color: #dce2ef;
border-radius: 6px;
min-height: 42px;
}
.dashboard-filter-card .form-control:focus,
.dashboard-filter-card .form-select:focus {
border-color: rgba(64, 81, 137, .45);
box-shadow: 0 0 0 .15rem rgba(64, 81, 137, .1);
}
.dashboard-filter-card .input-group .form-control {
border-radius: 6px 0 0 6px;
}
.dashboard-filter-card .select2-container--default .select2-selection--single .select2-selection__rendered {
line-height: 40px;
}
.dashboard-filter-card .select2-container--default .select2-selection--single .select2-selection__arrow {
height: 40px;
}
.dashboard-filter-card .btn {
border-radius: 6px;
font-weight: 700;
min-height: 42px;
}
#datePickerButton {
border-radius: 0 6px 6px 0;
cursor: pointer;
justify-content: center;
min-width: 48px;
}
.dashboard-payment-filter {
min-height: 42px;
padding: .55rem .8rem;
border: 1px solid #dce2ef;
border-radius: 6px;
background: #fff;
}
.dashboard-payment-filter .form-check-input {
cursor: pointer;
height: 1.1rem;
width: 2.15rem;
}
.dashboard-payment-filter .form-check-input:checked {
background-color: var(--dashboard-danger);
border-color: var(--dashboard-danger);
}
.dashboard-kpi-card {
overflow: hidden;
position: relative;
}
.dashboard-kpi-card::before {
content: "";
height: 3px;
left: 0;
position: absolute;
right: 0;
top: 0;
}
.dashboard-kpi-card.metric-info::before {
background: var(--dashboard-info);
}
.dashboard-kpi-card.metric-primary::before {
background: var(--dashboard-primary);
}
.dashboard-kpi-card.metric-success::before {
background: var(--dashboard-success);
}
.dashboard-kpi-card.metric-danger::before {
background: var(--dashboard-danger);
}
.dashboard-kpi-card .card-body {
min-height: 148px;
padding: 22px 20px;
}
.dashboard-kpi-label,
.dashboard-type-label {
color: var(--dashboard-muted);
font-size: 12px;
font-weight: 700;
letter-spacing: .04em;
text-transform: uppercase;
}
.dashboard-kpi-value {
color: #26364f;
font-size: 24px;
font-weight: 800;
line-height: 1.25;
}
.dashboard-kpi-copy {
color: var(--dashboard-muted);
font-size: 13px;
}
.dashboard-icon-badge {
border-radius: 8px;
height: 48px;
width: 48px;
}
.dashboard-type-card {
transition: transform .15s ease, box-shadow .15s ease;
}
.dashboard-type-card:hover {
box-shadow: 0 14px 28px rgba(15, 34, 58, .09);
transform: translateY(-2px);
}
.dashboard-type-card .card-body {
min-height: 188px;
padding: 20px;
}
.dashboard-type-value {
color: #26364f;
font-size: 21px;
font-weight: 800;
}
.dashboard-type-meta {
color: var(--dashboard-muted);
font-size: 13px;
}
.dashboard-progress {
background: #edf1f7;
border-radius: 999px;
display: flex;
height: 7px;
overflow: hidden;
}
.dashboard-progress-paid {
background: var(--dashboard-success);
}
.dashboard-progress-remaining {
background: var(--dashboard-danger);
}
.dashboard-chart-card .card-header,
.dashboard-table-card .card-header {
background: #fff;
border-bottom: 1px solid var(--dashboard-border);
padding: 18px 20px;
}
.dashboard-chart-card .card-title,
.dashboard-table-card .card-title {
color: #26364f;
font-weight: 700;
}
.dashboard-table-scroll {
max-height: 774px;
overflow: auto;
border: 1px solid var(--dashboard-border);
border-radius: 8px;
}
.dashboard-table-scroll table {
margin-bottom: 0;
min-width: 1060px;
}
.dashboard-table-scroll thead th {
position: sticky;
top: 0;
z-index: 2;
background: #f6f8fb;
box-shadow: inset 0 -1px 0 var(--dashboard-border);
color: #566176;
font-size: 12px;
letter-spacing: .03em;
text-transform: uppercase;
}
.dashboard-table-scroll tbody tr {
height: 48px;
}
.dashboard-table-scroll tbody tr:hover {
background: #f8fbff;
}
.dashboard-table-scroll .btn {
border-radius: 6px;
font-weight: 700;
white-space: nowrap;
}
.dashboard-client-cell {
max-width: 280px;
white-space: normal;
}
.dashboard-money {
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.dashboard-empty-state {
color: var(--dashboard-muted);
padding: 34px 16px;
}
@media (max-width: 575.98px) {
.dashboard-hero {
padding: 18px;
}
.dashboard-hero-title {
font-size: 21px;
}
.dashboard-filter-card .d-flex.gap-2 {
flex-direction: column;
}
}
</style>
<div class="page-content dashboard-page">
<div class="container-fluid">
<div class="row mb-4">
<div class="col-12">
<div class="dashboard-hero d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-3">
<div>
<div class="dashboard-hero-kicker mb-2">Suivi paiements</div>
<h4 class="dashboard-hero-title mb-2">Tableau de bord des restes a payer</h4>
<p class="dashboard-hero-subtitle mb-3">Consultez les factures, BL, BLP et BS avec recherche par periode, client, type de fichier et statut de paiement.</p>
<div class="d-flex flex-wrap gap-2">
<span class="dashboard-hero-pill">
<i class="ri-file-list-3-line align-bottom me-1"></i>{{ totals.count }} document{{ totals.count > 1 ? 's' : '' }}
</span>
<span class="dashboard-hero-pill">
<i class="ri-error-warning-line align-bottom me-1"></i>{{ totals.remaining|number_format(3, '.', ' ') }} DT reste a payer
</span>
{% if filters.unpaid_only %}
<span class="dashboard-hero-pill text-danger">
<i class="ri-filter-3-line align-bottom me-1"></i>Non payes seulement
</span>
{% endif %}
</div>
</div>
<div>
<a href="{{ path('app_home') }}" class="btn dashboard-reset-btn">
<i class="ri-refresh-line align-bottom me-1"></i> Reinitialiser
</a>
</div>
</div>
</div>
</div>
<div class="card dashboard-filter-card mb-4">
<div class="card-body">
<form method="get" action="{{ path('app_home') }}" class="row g-3 align-items-end">
<div class="col-xl-3 col-md-6">
<label for="dashboard_client" class="form-label">Client</label>
<select id="dashboard_client" name="client" class="form-select">
<option value="">Tous les clients</option>
{% for client in clients %}
{% set clientLabel = client.raisonsocial ?: ((client.nom ~ ' ' ~ client.prenom)|trim) %}
<option value="{{ client.id }}" {% if filters.client == client.id %}selected{% endif %}>
{{ clientLabel ?: ('Client #' ~ client.id) }}
</option>
{% endfor %}
</select>
</div>
<div class="col-xl-3 col-md-6">
<label for="dateRangePicker" class="form-label d-block">Période</label>
<div class="input-group">
<input
id="dateRangePicker"
name="dateRangePicker"
type="text"
class="form-control"
placeholder="Choisissez une plage de dates"
readonly
value="{{ filters.date_range }}"
/>
<span id="datePickerButton" class="input-group-text bg-primary border-primary text-white">
<i class="ri-calendar-2-line"></i>
</span>
</div>
</div>
<div class="col-xl-2 col-md-6">
<label for="dashboard_type" class="form-label">Type fichier</label>
<select id="dashboard_type" name="type" class="form-select">
<option value="">Tous les types</option>
{% for type, item in stats %}
<option value="{{ type }}" {% if filters.type == type %}selected{% endif %}>
{{ item.shortLabel }} - {{ item.label }}
</option>
{% endfor %}
</select>
</div>
<div class="col-xl-2 col-md-6">
<label for="dashboard_unpaid_only" class="form-label">Paiement</label>
<div class="form-check form-switch dashboard-payment-filter d-flex align-items-center gap-2">
<input class="form-check-input m-0" type="checkbox" id="dashboard_unpaid_only" name="unpaid_only" value="1" {% if filters.unpaid_only %}checked{% endif %}>
<label class="form-check-label small mb-0" for="dashboard_unpaid_only">Non payé</label>
</div>
</div>
<div class="col-xl-2 col-md-12">
<div class="d-flex gap-2">
<button type="submit" class="btn btn-primary flex-fill">
<i class="ri-search-line align-bottom me-1"></i> Filtrer
</button>
<button type="submit" formaction="{{ path('app_home_export_excel') }}" formmethod="get" class="btn btn-success flex-fill">
<i class="ri-file-excel-2-line align-bottom me-1"></i> Excel
</button>
</div>
</div>
</form>
</div>
</div>
<div class="row">
<div class="col-xl-3 col-md-6">
<div class="card card-animate dashboard-kpi-card metric-info">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1 overflow-hidden">
<p class="dashboard-kpi-label text-truncate mb-0">Total documents</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title dashboard-icon-badge bg-soft-info fs-3">
<i class="ri-stack-line text-info"></i>
</span>
</div>
</div>
<div class="mt-4">
<h4 class="dashboard-kpi-value ff-secondary mb-2">{{ totals.count }}</h4>
<p class="dashboard-kpi-copy mb-0">Documents trouves avec les filtres actifs.</p>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-animate dashboard-kpi-card metric-primary">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1 overflow-hidden">
<p class="dashboard-kpi-label text-truncate mb-0">Montant total</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title dashboard-icon-badge bg-soft-primary fs-3">
<i class="ri-money-dollar-circle-line text-primary"></i>
</span>
</div>
</div>
<div class="mt-4">
<h4 class="dashboard-kpi-value ff-secondary mb-2">{{ totals.total|number_format(3, '.', ' ') }} DT</h4>
<p class="dashboard-kpi-copy mb-0">Total TTC des documents.</p>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-animate dashboard-kpi-card metric-success">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1 overflow-hidden">
<p class="dashboard-kpi-label text-truncate mb-0">Montant paye</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title dashboard-icon-badge bg-soft-success fs-3">
<i class="ri-checkbox-circle-line text-success"></i>
</span>
</div>
</div>
<div class="mt-4">
<h4 class="dashboard-kpi-value ff-secondary mb-2">{{ totals.paid|number_format(3, '.', ' ') }} DT</h4>
<p class="dashboard-kpi-copy mb-0">Montant deja regle.</p>
</div>
</div>
</div>
</div>
<div class="col-xl-3 col-md-6">
<div class="card card-animate dashboard-kpi-card metric-danger">
<div class="card-body">
<div class="d-flex align-items-center">
<div class="flex-grow-1 overflow-hidden">
<p class="dashboard-kpi-label text-truncate mb-0">Reste a payer</p>
</div>
<div class="avatar-sm flex-shrink-0">
<span class="avatar-title dashboard-icon-badge bg-soft-danger fs-3">
<i class="ri-error-warning-line text-danger"></i>
</span>
</div>
</div>
<div class="mt-4">
<h4 class="dashboard-kpi-value ff-secondary mb-2">{{ totals.remaining|number_format(3, '.', ' ') }} DT</h4>
<p class="dashboard-kpi-copy mb-0">Somme restante sur la selection.</p>
</div>
</div>
</div>
</div>
</div>
<div class="row">
{% for key, item in stats %}
{% set paidPercent = item.total > 0 ? ((item.paid / item.total) * 100)|round(0, 'floor') : 0 %}
{% set paidPercent = paidPercent > 100 ? 100 : paidPercent %}
{% set remainingPercent = 100 - paidPercent %}
<div class="col-xl-3 col-md-6">
<div class="card dashboard-type-card">
<div class="card-body">
<div class="d-flex align-items-center mb-3">
<div class="avatar-sm flex-shrink-0 me-3">
<span class="avatar-title dashboard-icon-badge bg-soft-{{ item.color }} text-{{ item.color }} fs-3">
<i class="{{ item.icon }}"></i>
</span>
</div>
<div class="flex-grow-1">
<h5 class="mb-1">{{ item.label }}</h5>
<p class="dashboard-type-meta mb-0">{{ item.count }} document{{ item.count > 1 ? 's' : '' }}</p>
</div>
</div>
<p class="dashboard-type-label mb-1">Reste a payer</p>
<h4 class="dashboard-type-value mb-2">{{ item.remaining|number_format(3, '.', ' ') }} DT</h4>
<div class="d-flex justify-content-between dashboard-type-meta">
<span>Total: {{ item.total|number_format(3, '.', ' ') }}</span>
<span>Paye: {{ item.paid|number_format(3, '.', ' ') }}</span>
</div>
<div class="dashboard-progress mt-3" aria-label="Progression paiement {{ item.shortLabel }}">
<span class="dashboard-progress-paid" style="width: {{ paidPercent }}%"></span>
<span class="dashboard-progress-remaining" style="width: {{ remainingPercent }}%"></span>
</div>
<div class="d-flex justify-content-between dashboard-type-meta mt-2">
<span>{{ paidPercent }}% paye</span>
<span>{{ remainingPercent }}% reste</span>
</div>
<div class="mt-3">
<a href="{{ path(item.route) }}" class="link-{{ item.color }}">
Voir la liste <i class="ri-arrow-right-line align-bottom"></i>
</a>
</div>
</div>
</div>
</div>
{% endfor %}
</div>
<div class="row">
<div class="col-xl-8">
<div class="card dashboard-chart-card">
<div class="card-header">
<h4 class="card-title mb-1">Reste a payer par type</h4>
<p class="text-muted mb-0">Comparaison entre total TTC, montant paye et reste.</p>
</div>
<div class="card-body">
<div id="dashboard_reste_bar" class="apex-charts" style="min-height: 330px;"></div>
</div>
</div>
</div>
<div class="col-xl-4">
<div class="card dashboard-chart-card">
<div class="card-header">
<h4 class="card-title mb-1">Repartition du reste</h4>
<p class="text-muted mb-0">Poids de chaque type dans le montant restant.</p>
</div>
<div class="card-body">
<div id="dashboard_reste_donut" class="apex-charts" style="min-height: 330px;"></div>
</div>
</div>
</div>
</div>
<div class="row">
<div class="col-12">
<div class="card dashboard-table-card">
<div class="card-header d-flex flex-column flex-lg-row align-items-lg-center justify-content-between gap-2">
<div>
<h4 class="card-title mb-1">Documents avec reste a payer</h4>
<p class="text-muted mb-0">15 lignes visibles, puis defilement vertical pour le reste.</p>
</div>
<div class="d-flex flex-wrap align-items-center gap-2">
{% if filters.type %}
<span class="badge bg-soft-info text-info">Type: {{ stats[filters.type].shortLabel }}</span>
{% endif %}
{% if filters.unpaid_only %}
<span class="badge bg-soft-danger text-danger">Non payés seulement</span>
{% endif %}
<span class="badge bg-soft-primary text-primary">{{ documents|length }} ligne{{ documents|length > 1 ? 's' : '' }}</span>
</div>
</div>
<div class="card-body">
<div class="table-responsive dashboard-table-scroll">
<table class="table table-hover table-nowrap align-middle">
<thead class="table-light">
<tr>
<th>Type</th>
<th>Numero</th>
<th>Date</th>
<th>Client</th>
<th class="text-end">Total TTC</th>
<th class="text-end">Paye</th>
<th class="text-end">Reste a payer</th>
<th class="text-end">Action</th>
</tr>
</thead>
<tbody>
{% for document in documents %}
<tr>
<td>
<span class="badge bg-soft-{{ document.typeColor }} text-{{ document.typeColor }}">{{ document.typeLabel }}</span>
</td>
<td class="fw-medium">{{ document.number }}</td>
<td>{{ document.date ? document.date|date('d/m/Y') : '-' }}</td>
<td class="dashboard-client-cell">{{ document.client }}</td>
<td class="text-end dashboard-money">{{ document.total|number_format(3, '.', ' ') }} DT</td>
<td class="text-end text-success dashboard-money">{{ document.paid|number_format(3, '.', ' ') }} DT</td>
<td class="text-end fw-semibold text-danger dashboard-money">{{ document.remaining|number_format(3, '.', ' ') }} DT</td>
<td class="text-end">
<a href="{{ document.url }}" class="btn btn-sm btn-soft-primary">
Voir
</a>
</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="text-center dashboard-empty-state">
<div class="fs-2 mb-2"><i class="ri-search-eye-line"></i></div>
<div class="fw-semibold">Aucun document trouve</div>
<div class="small">Changez la periode, le client ou le type de fichier.</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function () {
var dateRangeInput = document.getElementById('dateRangePicker');
var datePickerButton = document.getElementById('datePickerButton');
if (dateRangeInput && typeof flatpickr !== 'undefined') {
var datePickerInstance = flatpickr(dateRangeInput, {
mode: 'range',
dateFormat: 'Y-m-d',
allowInput: true
});
if (datePickerButton) {
datePickerButton.addEventListener('click', function () {
datePickerInstance.open();
});
}
}
if (window.jQuery && jQuery.fn.select2) {
jQuery('#dashboard_client').select2({
width: '100%',
placeholder: 'Tous les clients',
allowClear: true
});
jQuery('#dashboard_type').select2({
width: '100%',
minimumResultsForSearch: Infinity
});
}
if (typeof ApexCharts === 'undefined') {
return;
}
var labels = {{ chart.labels|json_encode|raw }};
var remaining = {{ chart.remaining|json_encode|raw }};
var paid = {{ chart.paid|json_encode|raw }};
var total = {{ chart.total|json_encode|raw }};
var colors = ['#405189', '#0ab39c', '#f7b84b', '#f06548'];
var barElement = document.querySelector('#dashboard_reste_bar');
if (barElement) {
new ApexCharts(barElement, {
series: [
{name: 'Reste a payer', data: remaining},
{name: 'Montant paye', data: paid},
{name: 'Total TTC', data: total}
],
chart: {
type: 'bar',
height: 330,
fontFamily: 'inherit',
foreColor: '#74788d',
toolbar: {show: false}
},
grid: {
borderColor: '#edf1f7',
strokeDashArray: 4
},
plotOptions: {
bar: {
borderRadius: 6,
columnWidth: '42%'
}
},
dataLabels: {enabled: false},
colors: ['#f06548', '#0ab39c', '#405189'],
xaxis: {
categories: labels,
axisBorder: {show: false},
axisTicks: {show: false}
},
yaxis: {
labels: {
formatter: function (value) {
return Number(value || 0).toFixed(3);
}
}
},
tooltip: {
theme: 'light',
y: {
formatter: function (value) {
return Number(value || 0).toFixed(3) + ' DT';
}
}
},
legend: {
horizontalAlign: 'right',
markers: {radius: 99},
position: 'top'
}
}).render();
}
var donutElement = document.querySelector('#dashboard_reste_donut');
if (donutElement) {
new ApexCharts(donutElement, {
series: remaining,
labels: labels,
chart: {
type: 'donut',
height: 330,
fontFamily: 'inherit',
foreColor: '#74788d'
},
colors: colors,
dataLabels: {
dropShadow: {enabled: false}
},
plotOptions: {
pie: {
donut: {
size: '68%',
labels: {
show: true,
total: {
show: true,
label: 'Reste',
formatter: function (w) {
var sum = w.globals.seriesTotals.reduce(function (a, b) {
return a + b;
}, 0);
return Number(sum || 0).toFixed(3) + ' DT';
}
},
value: {
formatter: function (value) {
return Number(value || 0).toFixed(3);
}
}
}
}
}
},
stroke: {width: 0},
legend: {
markers: {radius: 99},
position: 'bottom'
},
tooltip: {
y: {
formatter: function (value) {
return Number(value || 0).toFixed(3) + ' DT';
}
}
},
noData: {
text: 'Aucune donnee'
}
}).render();
}
});
</script>
{% endblock %}