r
This commit is contained in:
parent
616a3c53bd
commit
cef2cdc005
|
|
@ -21,6 +21,7 @@ class ControleurFicheadherent extends Controleur {
|
|||
|
||||
$_SESSION['modeDevis'] = "0";
|
||||
}
|
||||
|
||||
public function index() {
|
||||
|
||||
$idPolice = $_SESSION['idPolice_C'];
|
||||
|
|
@ -38,8 +39,47 @@ class ControleurFicheadherent extends Controleur {
|
|||
|
||||
$limite_adherent = $this->garantieadherent->getLimitesAdherent($idAdherent);
|
||||
|
||||
$this->genererVue(array('adherent' => $adherent, 'beneficiaires' => $beneficiaires, 'totalbeneficiaires' => $totalbeneficiaires,
|
||||
'garantieadherents' => $garantieadherents, 'limite_adherent' => $limite_adherent));
|
||||
$prestationsParMois = $this->adherent->getPrestationsmensuelles($idAdherent);
|
||||
$prestationsParLiens = $this->adherent->getPrestationsLiensParente($idAdherent);
|
||||
|
||||
// Prestations par mois
|
||||
$tabPrestationsParMois = [
|
||||
'mois' => array_column($prestationsParMois, 'mois'),
|
||||
'consos' => array_column($prestationsParMois, 'consommationAdherent')
|
||||
];
|
||||
|
||||
// Prestations par liens de parenté
|
||||
$tabPrestationsParLiens = [
|
||||
'lienparente' => array_column($prestationsParLiens, 'lienparente'),
|
||||
'consos_liens' => array_column($prestationsParLiens, 'consommation')
|
||||
];
|
||||
|
||||
$nbreLienParente = count($tabPrestationsParLiens['lienparente']);
|
||||
|
||||
// Consommations par garanties
|
||||
$tabConsoParGaranties = [
|
||||
'garanties' => array_column($garantieadherents, 'codeGarantie'),
|
||||
'depenses' => array_column($garantieadherents, 'consommationFamille'),
|
||||
'plafonds' => array_column($garantieadherents, 'plafondFamille')
|
||||
];
|
||||
|
||||
// Encodage JSON
|
||||
$dataConsoParMois = json_encode($tabPrestationsParMois, JSON_NUMERIC_CHECK);
|
||||
$dataConsoParLiens = json_encode($tabPrestationsParLiens, JSON_NUMERIC_CHECK);
|
||||
$dataConsoParGaranties = json_encode($tabConsoParGaranties, JSON_NUMERIC_CHECK);
|
||||
|
||||
|
||||
$this->genererVue(array(
|
||||
'adherent' => $adherent,
|
||||
'beneficiaires' => $beneficiaires,
|
||||
'totalbeneficiaires' => $totalbeneficiaires,
|
||||
'garantieadherents' => $garantieadherents,
|
||||
'limite_adherent' => $limite_adherent,
|
||||
'dataConsoParMois' => $dataConsoParMois,
|
||||
'dataConsoParLiens' => $dataConsoParLiens,
|
||||
'dataConsoParGaranties' => $dataConsoParGaranties,
|
||||
'nbreLienParente' => $nbreLienParente
|
||||
));
|
||||
}
|
||||
|
||||
public function supprimer() {
|
||||
|
|
|
|||
|
|
@ -847,4 +847,33 @@ class Adherent extends Modele {
|
|||
return $resultat->fetch(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
// Ajouté par Sorel le 12-01-2026 => Gestion des graphiques de la famille
|
||||
|
||||
public function getPrestationsmensuelles($idAdherent){
|
||||
|
||||
if (isset($_SESSION['lang']) && $_SESSION['lang']=="en_US"){
|
||||
$sql = 'call prestations_mensuelles_famille_eng(?)';
|
||||
}else{
|
||||
$sql = 'call prestations_mensuelles_famille(?)';
|
||||
}
|
||||
|
||||
$resultat = $this->executerRequete($sql, array($idAdherent));
|
||||
|
||||
return $resultat->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
public function getPrestationsLiensParente($idAdherent){
|
||||
|
||||
if (isset($_SESSION['lang']) && $_SESSION['lang']=="en_US"){
|
||||
|
||||
$sql = 'call prestations_lienparente_famille_eng(?)';
|
||||
}else{
|
||||
$sql = 'call prestations_lienparente_famille(?)';
|
||||
}
|
||||
|
||||
$resultat = $this->executerRequete($sql, array($idAdherent));
|
||||
|
||||
return $resultat->fetchAll(PDO::FETCH_ASSOC);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -265,6 +265,68 @@
|
|||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<!-- Responsive Area Chart -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-xl-8 col-lg-7 mb-4">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-header py-3 d-flex flex-column flex-md-row justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary"><?= _("Dépenses mensuelles") ?></h6>
|
||||
<div class="mt-2 mt-md-0">
|
||||
<button id="exportTrendBtn" class="btn btn-sm btn-danger mr-2">
|
||||
<i class="fas fa-download"></i> PDF
|
||||
</button>
|
||||
<small class="text-muted"><?= _("Évolution sur 12 mois") ?></small>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container" style="position: relative; height:300px; width:100%">
|
||||
<canvas id="expenseTrendChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Responsive Pie Chart -->
|
||||
<div class="col-xl-4 col-lg-5 mb-4">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary"><?= _("Dépenses par lien parenté") ?></h6>
|
||||
<button id="exportPieBtn" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-download"></i> PDF
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body d-flex flex-column">
|
||||
<div class="chart-container" style="position: relative; height:250px; width:100%">
|
||||
<canvas id="expenseDistributionChart"></canvas>
|
||||
</div>
|
||||
<div id="pieChartLegend" class="mt-3 text-center"></div>
|
||||
<div class="mt-auto pt-3">
|
||||
<small class="text-muted"><?= _("Répartition en pourcentage") ?></small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Bar Chart Section -->
|
||||
<div class="row mt-4">
|
||||
<div class="col-12 mb-4">
|
||||
<div class="card shadow h-100">
|
||||
<div class="card-header py-3 d-flex justify-content-between align-items-center">
|
||||
<h6 class="m-0 font-weight-bold text-primary"><?= _("Dépenses par garantie") ?></h6>
|
||||
<button id="exportBarBtn" class="btn btn-sm btn-danger">
|
||||
<i class="fas fa-download"></i> PDF
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="chart-container" style="position: relative; height:400px; width:100%">
|
||||
<canvas id="depensesChart"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
|
@ -275,3 +337,352 @@
|
|||
.input-group-text { background-color: #f8f9fa; color: #6c757d; }
|
||||
input[readonly] { cursor: default; }
|
||||
</style>
|
||||
|
||||
<script>
|
||||
// Configuration commune
|
||||
(function() {
|
||||
// Récupération de la devise depuis PHP
|
||||
const userCurrency = '<?= isset($_SESSION['devise_C']) ? $_SESSION['devise_C'] : 'FRW' ?>';
|
||||
|
||||
// Configuration des devises
|
||||
const currencyFormats = {
|
||||
'FRW': {
|
||||
symbol: 'FRW',
|
||||
format: (value) => new Intl.NumberFormat('fr-FR').format(value) + ' FRW'
|
||||
},
|
||||
'€': {
|
||||
symbol: '€',
|
||||
format: (value) => new Intl.NumberFormat('fr-FR', {style: 'currency', currency: 'EUR'}).format(value)
|
||||
},
|
||||
'$': {
|
||||
symbol: '$',
|
||||
format: (value) => new Intl.NumberFormat('fr-FR', {style: 'currency', currency: 'USD'}).format(value)
|
||||
},
|
||||
'XOF': {
|
||||
symbol: 'FRW',
|
||||
format: (value) => new Intl.NumberFormat('fr-FR').format(value) + ' FRW'
|
||||
}
|
||||
};
|
||||
|
||||
// Format monétaire dynamique
|
||||
const formatMoney = (value) => {
|
||||
const currencyConfig = currencyFormats[userCurrency] || currencyFormats['FRW'];
|
||||
return currencyConfig.format(value);
|
||||
};
|
||||
|
||||
// Format pourcentage
|
||||
const formatPercentage = (value, total) => {
|
||||
const percentage = (value * 100 / total).toFixed(1);
|
||||
return percentage + '%';
|
||||
};
|
||||
|
||||
// Détection mobile
|
||||
const isMobile = window.matchMedia("(max-width: 768px)").matches;
|
||||
|
||||
// Génération des couleurs
|
||||
const generateColors = (count) => {
|
||||
const palette = [
|
||||
'#4e73df', '#1cc88a', '#36b9cc', '#f6c23e',
|
||||
'#e74a3b', '#858796', '#5a5c69', '#3a3b45',
|
||||
'#2e59a9', '#17a673', '#2c9faf', '#dda20a'
|
||||
];
|
||||
return palette.slice(0, count).concat(
|
||||
Array.from({length: Math.max(0, count - palette.length)}, (_, i) => {
|
||||
const hue = Math.floor(360 * (i / Math.max(1, count - palette.length)));
|
||||
return `hsl(${hue}, 70%, 60%)`;
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
// Fonction pour générer une légende personnalisée
|
||||
function generateCustomLegend(chart, containerId) {
|
||||
const legendContainer = document.getElementById(containerId);
|
||||
legendContainer.innerHTML = '';
|
||||
|
||||
const items = chart.data.datasets[0].data.map((value, i) => {
|
||||
const total = chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
|
||||
const percentage = formatPercentage(value, total);
|
||||
|
||||
return `
|
||||
<div class="d-inline-block mx-2 my-1">
|
||||
<span class="legend-color" style="
|
||||
display: inline-block;
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background-color: ${chart.data.datasets[0].backgroundColor[i]};
|
||||
border: 1px solid #fff;
|
||||
vertical-align: middle;
|
||||
"></span>
|
||||
<span class="legend-text small ml-1">
|
||||
${chart.data.labels[i]}: ${formatMoney(value)} (${percentage})
|
||||
</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
|
||||
legendContainer.innerHTML = items.join('');
|
||||
}
|
||||
|
||||
// Fonction pour exporter un graphique en PDF
|
||||
function exportChartToPDF(chartId, fileName) {
|
||||
const { jsPDF } = window.jspdf;
|
||||
const canvas = document.getElementById(chartId);
|
||||
|
||||
html2canvas(canvas).then(canvasImage => {
|
||||
const imgData = canvasImage.toDataURL('image/png');
|
||||
const pdf = new jsPDF({
|
||||
orientation: canvasImage.width > canvasImage.height ? 'landscape' : 'portrait'
|
||||
});
|
||||
|
||||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||||
|
||||
const ratio = canvasImage.height / canvasImage.width;
|
||||
let imgWidth = pageWidth - 20;
|
||||
let imgHeight = imgWidth * ratio;
|
||||
|
||||
if (imgHeight > pageHeight - 20) {
|
||||
imgHeight = pageHeight - 20;
|
||||
imgWidth = imgHeight / ratio;
|
||||
}
|
||||
|
||||
pdf.addImage(imgData, 'PNG',
|
||||
(pageWidth - imgWidth) / 2,
|
||||
(pageHeight - imgHeight) / 2,
|
||||
imgWidth,
|
||||
imgHeight
|
||||
);
|
||||
|
||||
pdf.save(fileName + '.pdf');
|
||||
});
|
||||
}
|
||||
|
||||
// Graphique d'évolution des dépenses
|
||||
const dataMois = <?= $dataConsoParMois ?>;
|
||||
const trendCtx = document.getElementById('expenseTrendChart').getContext('2d');
|
||||
const trendChart = new Chart(trendCtx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: dataMois.mois,
|
||||
datasets: [{
|
||||
label: '<?= _("Montant dépensé") ?>',
|
||||
data: dataMois.consos,
|
||||
backgroundColor: 'rgba(78, 115, 223, 0.05)',
|
||||
borderColor: 'rgba(78, 115, 223, 1)',
|
||||
borderWidth: 2,
|
||||
pointBackgroundColor: 'rgba(78, 115, 223, 1)',
|
||||
pointRadius: isMobile ? 3 : 4,
|
||||
pointHoverRadius: 6,
|
||||
fill: true,
|
||||
tension: 0.3
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
mode: 'index',
|
||||
intersect: false,
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
return context.dataset.label + ': ' + formatMoney(context.parsed.y);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
grid: {
|
||||
display: false
|
||||
},
|
||||
ticks: {
|
||||
maxRotation: isMobile ? 45 : 0,
|
||||
autoSkip: true,
|
||||
maxTicksLimit: isMobile ? 6 : 12
|
||||
}
|
||||
},
|
||||
y: {
|
||||
beginAtZero: false,
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return formatMoney(value);
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
color: 'rgba(0, 0, 0, 0.05)'
|
||||
}
|
||||
}
|
||||
},
|
||||
interaction: {
|
||||
mode: 'nearest',
|
||||
axis: 'x',
|
||||
intersect: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Graphique de répartition
|
||||
const dataLiens = <?= $dataConsoParLiens ?>;
|
||||
const distributionCtx = document.getElementById('expenseDistributionChart').getContext('2d');
|
||||
const distributionChart = new Chart(distributionCtx, {
|
||||
type: 'pie',
|
||||
data: {
|
||||
labels: dataLiens.lienparente,
|
||||
datasets: [{
|
||||
data: dataLiens.consos_liens,
|
||||
backgroundColor: generateColors(<?= $nbreLienParente; ?>),
|
||||
borderColor: '#fff',
|
||||
borderWidth: 2
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
legend: {
|
||||
display: false,
|
||||
},
|
||||
datalabels: {
|
||||
formatter: (value, ctx) => {
|
||||
const sum = ctx.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
|
||||
return formatPercentage(value, sum);
|
||||
},
|
||||
color: '#fff',
|
||||
font: {
|
||||
weight: 'bold',
|
||||
size: isMobile ? 10 : 12
|
||||
}
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const label = context.label || '';
|
||||
const value = context.raw || 0;
|
||||
const sum = context.dataset.data.reduce((a, b) => a + b, 0);
|
||||
const percentage = formatPercentage(value, sum);
|
||||
return `${label}: ${formatMoney(value)} (${percentage})`;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
cutout: isMobile ? '60%' : '50%'
|
||||
},
|
||||
plugins: [ChartDataLabels]
|
||||
});
|
||||
|
||||
// Graphique des dépenses par garantie
|
||||
const dataConso = <?= $dataConsoParGaranties ?>;
|
||||
const barCtx = document.getElementById('depensesChart').getContext('2d');
|
||||
const barChart = new Chart(barCtx, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: dataConso.garanties,
|
||||
datasets: [{
|
||||
label: 'Dépenses',
|
||||
data: dataConso.depenses,
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.7)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1
|
||||
}]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
plugins: {
|
||||
annotation: {
|
||||
annotations: dataConso.plafonds.map((plafond, index) => {
|
||||
if (plafond === null) return null;
|
||||
|
||||
return {
|
||||
type: 'line',
|
||||
yMin: plafond,
|
||||
yMax: plafond,
|
||||
borderColor: 'rgba(255, 99, 132, 0.7)',
|
||||
borderWidth: 2,
|
||||
borderDash: [6, 6],
|
||||
label: {
|
||||
content: `Plafond: ${formatMoney(plafond)}`,
|
||||
enabled: true,
|
||||
position: 'right'
|
||||
}
|
||||
};
|
||||
}).filter(annotation => annotation !== null)
|
||||
},
|
||||
legend: {
|
||||
display: false
|
||||
},
|
||||
tooltip: {
|
||||
callbacks: {
|
||||
label: function(context) {
|
||||
const plafond = dataConso.plafonds[context.dataIndex];
|
||||
let tooltip = `Dépenses: ${formatMoney(context.parsed.y)}`;
|
||||
|
||||
if (plafond !== null) {
|
||||
const pourcentage = Math.min(100, Math.round((context.parsed.y / plafond) * 100));
|
||||
tooltip += ` (${pourcentage}% du plafond)`;
|
||||
} else {
|
||||
tooltip += ' (plafond illimité)';
|
||||
}
|
||||
|
||||
return tooltip;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
scales: {
|
||||
y: {
|
||||
beginAtZero: true,
|
||||
title: {
|
||||
display: true,
|
||||
text: `Montant des dépenses (${userCurrency})`,
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
ticks: {
|
||||
callback: function(value) {
|
||||
return formatMoney(value);
|
||||
}
|
||||
},
|
||||
suggestedMax: Math.max(...dataConso.depenses) * 1.5
|
||||
},
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Garanties',
|
||||
font: {
|
||||
weight: 'bold'
|
||||
}
|
||||
},
|
||||
grid: {
|
||||
display: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Générer la légende au chargement et lors du redimensionnement
|
||||
generateCustomLegend(distributionChart, 'pieChartLegend');
|
||||
window.addEventListener('resize', function() {
|
||||
generateCustomLegend(distributionChart, 'pieChartLegend');
|
||||
});
|
||||
|
||||
// Boutons d'export PDF
|
||||
document.getElementById('exportTrendBtn').addEventListener('click', () => {
|
||||
exportChartToPDF('expenseTrendChart', 'evolution_depenses_mensuelles');
|
||||
});
|
||||
|
||||
document.getElementById('exportPieBtn').addEventListener('click', () => {
|
||||
exportChartToPDF('expenseDistributionChart', 'repartition_depenses_parente');
|
||||
});
|
||||
|
||||
document.getElementById('exportBarBtn').addEventListener('click', () => {
|
||||
exportChartToPDF('depensesChart', 'depenses_par_garantie');
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
Loading…
Reference in New Issue
Block a user