- Enabled SMTP debugging in PHPMailer for better error tracking. - Added a "Test send email" link in the Inventory Detail View for quick email testing. - Implemented automatic PDF generation and email sending upon Sales Order creation. - Created a new action for sending Sales Order emails with attached PDFs. - Added a new AJAX action for testing outgoing email server configurations. - Updated outgoing server settings to use new SMTP credentials. - Improved email templates for better user experience. - Added test scripts for validating PDF generation and email sending.
932 lines
34 KiB
PHP
932 lines
34 KiB
PHP
<?php
|
|
|
|
$dbvp = true;
|
|
|
|
require_once 'MonitoringVMHeader.php';
|
|
require_once 'MonitoringDBRequest.php';
|
|
|
|
?>
|
|
<?php
|
|
|
|
$userId = $current_user->get('id');
|
|
$event = $_GET["event"];
|
|
$objective = 0;
|
|
|
|
if (!isVPSuperviseur($roleid) && !isTopDG($roleid) && !isResponsableCommercial($roleid)) {
|
|
die("<div style='width:100%;height:100%;display:flex;justify-content: center;font-size: 2rem;font-style: normal;'> Vous n'êtes pas autorisé à lire cette ressource.</div>");
|
|
}
|
|
|
|
if (isTopDG($roleid)) {
|
|
$roleid = "H10";
|
|
}
|
|
// $datedeb = date("Y-m-d", strtotime("-1 month"));
|
|
// $datefin = date('Y-m-d');
|
|
|
|
$datedeb = '2026-11-04';
|
|
$datefin = '2026-11-30';
|
|
|
|
if ($event == "Saidalya") {
|
|
$datedeb = '2026-02-04';
|
|
$datefin = '2026-02-07';
|
|
$objective = 300000000.00; // Objective value (0-100)
|
|
echo getMonitoringMainBarVP(9);
|
|
} else if ($event == "November") {
|
|
$datedeb = '2026-01-13';
|
|
$datefin = '2026-02-15';
|
|
$objective = 500000000.00; // Objective value (0-100)
|
|
|
|
echo getMonitoringMainBarVP(10);
|
|
} else {
|
|
die("Unauthorized");
|
|
}
|
|
|
|
$vpFilter = "";
|
|
|
|
if ($event == "Saidalya") {
|
|
$vpFilter = " AND us.id IN (156,125,215,137,149,261,127,124,43,254,212,255,253,186,248,222)";
|
|
}
|
|
|
|
$currentValue = 0; // Current gauge value (0-100)
|
|
|
|
?>
|
|
|
|
|
|
<?php
|
|
global $adb;
|
|
$queryCA = "SELECT total_bc as bc FROM
|
|
(SELECT us.id ,CONCAT(first_name,' ', last_name) as fullname ,EXTRACT(YEAR FROM so.duedate) as YEAR,EXTRACT(MONTH FROM so.duedate) as month, sum(subtotal) as total_bc, cf_992
|
|
FROM vtiger_users us
|
|
JOIN vtiger_user2role usr ON usr.userid = us.id
|
|
JOIN vtiger_role ro ON ro.roleid = usr.roleid
|
|
JOIN vtiger_crmentity crm on crm.smownerid = us.id and crm.setype='SalesOrder' and crm.deleted <> 1
|
|
JOIN vtiger_salesorder so ON so.salesorderid = crm.crmid
|
|
JOIN vtiger_accountscf acf ON acf.accountid = so.accountid";
|
|
$queryCA = $queryCA . " WHERE so.duedate BETWEEN '" . $datedeb . "' and '" . $datefin . "' $vpFilter order by total_bc asc";
|
|
$queryCA = $queryCA . ") AS subquery order by total_bc desc; ";
|
|
|
|
$sql_get_result_ca = $adb->query($queryCA);
|
|
$result_ca = array();
|
|
while ($recordinfo = $adb->fetch_array($sql_get_result_ca)) {
|
|
$result_ca[] = $recordinfo;
|
|
}
|
|
$currentValue = $result_ca[0][0] ?? 0;
|
|
|
|
|
|
// CA Par Client
|
|
$query = "SELECT soc.cf_854 as accountname,
|
|
sum(so.subtotal) as totalmargin
|
|
FROM `vtiger_salesorder` so
|
|
JOIN vtiger_salesordercf soc on soc.salesorderid = so.salesorderid
|
|
JOIN vtiger_crmentity e on so.`salesorderid` = e.crmid and e.deleted <> 1 and e.setype='SalesOrder'
|
|
JOIN vtiger_users us on us.id = e.smownerid and us.status <> 'Inactive'
|
|
JOIN vtiger_user2role usr ON usr.userid = us.id
|
|
JOIN vtiger_role ro ON ro.roleid = usr.roleid";
|
|
$query = $query . " WHERE so.duedate BETWEEN '" . $datedeb . "' and '" . $datefin . "' $vpFilter";
|
|
$query = $query . " GROUP by accountname order by totalmargin desc";
|
|
$sql_get_result_client = $adb->query($query);
|
|
$result_client = array();
|
|
while ($recordinfo = $adb->fetch_array($sql_get_result_client)) {
|
|
$result_client[] = $recordinfo;
|
|
}
|
|
$json_data = json_encode($result_client);
|
|
|
|
|
|
// Par produit
|
|
$query_produit = "SELECT p.productname, sum(ip.quantity) as totalquantity , sum(ip.margin) as totalmargin
|
|
FROM `vtiger_salesorder` so
|
|
JOIN vtiger_inventoryproductrel ip on so.`salesorderid` = ip.id
|
|
JOIN vtiger_crmentity e on so.`salesorderid` = e.crmid and e.deleted = 0
|
|
JOIN vtiger_products p on p.productid = ip.productid
|
|
JOIN vtiger_users us on us.id = e.smownerid and us.status <> 'Inactive'
|
|
JOIN vtiger_user2role usr ON usr.userid = us.id
|
|
JOIN vtiger_role ro ON ro.roleid = usr.roleid";
|
|
$query_produit = $query_produit . " WHERE so.duedate BETWEEN '" . $datedeb . "' and '" . $datefin . "' $vpFilter";
|
|
$query_produit = $query_produit . " GROUP by p.productname order by totalmargin desc"; //, month , year";
|
|
|
|
|
|
$sql_get_result_product = $adb->query($query_produit);
|
|
$result_products_arr = array();
|
|
while ($recordinfo = $adb->fetch_array($sql_get_result_product)) {
|
|
$result_products_arr[] = $recordinfo;
|
|
}
|
|
|
|
$result_products = json_encode($result_products_arr);
|
|
// $totalMargins = array_column($result_products, 'totalmargin');
|
|
|
|
|
|
// PAR VP
|
|
$query_vp = "SELECT
|
|
fullname,
|
|
total_bc AS bc
|
|
FROM
|
|
(SELECT us.id,CONCAT(first_name, ' ', last_name) AS fullname,SUM(subtotal) AS total_bc
|
|
FROM vtiger_users us JOIN vtiger_user2role usr ON usr.userid = us.id
|
|
JOIN vtiger_crmentity crm ON crm.smownerid = us.id AND crm.setype = 'SalesOrder' AND crm.deleted <> 1
|
|
JOIN vtiger_salesorder so ON so.salesorderid = crm.crmid ";
|
|
$query_vp = $query_vp . " WHERE so.duedate BETWEEN '" . $datedeb . "' and '" . $datefin . "' $vpFilter";
|
|
$query_vp = $query_vp . " GROUP BY us.id,fullname) as s ";
|
|
|
|
$sql_get_result_vp = $adb->query($query_vp);
|
|
$result_vp = array();
|
|
while ($recordinfo = $adb->fetch_array($sql_get_result_vp)) {
|
|
$result_vp[] = $recordinfo;
|
|
}
|
|
|
|
$result_vp = json_encode($result_vp);
|
|
|
|
|
|
// echo "<pre>";
|
|
// print_r($result_products);
|
|
// echo "</pre>";
|
|
// // print_r($productNames);
|
|
// echo $result_vp;
|
|
// die();
|
|
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="en-US">
|
|
|
|
<head>
|
|
<meta charset="utf-8">
|
|
<meta http-equiv="X-UA-Compatible" content="IE=Edge">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
<title>Dashboard</title>
|
|
|
|
<!-- Fonts & Icons -->
|
|
<link href="file_upload/MyFont.css" rel="stylesheet">
|
|
|
|
<!-- Chart.js and Plugins -->
|
|
<script src="file_upload/Chart.bundle.js"></script>
|
|
<script src="file_upload/chartjs-gauge.js"></script>
|
|
<script src="file_upload/chartjs-plugin-datalabels.js"></script>
|
|
|
|
<!-- DataTables CSS -->
|
|
<link rel="stylesheet" href="file_upload/dataTables.bootstrap4.min.css">
|
|
|
|
<!-- jQuery + DataTables JS -->
|
|
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
|
|
<script src="file_upload/jquery.dataTables.min.js"></script>
|
|
<script src="file_upload/dataTables.bootstrap4.min.js"></script>
|
|
|
|
|
|
<!-- Modern Styles -->
|
|
<style>
|
|
:root {
|
|
--primary: #3f51b5;
|
|
--secondary: #f5f7fa;
|
|
--text: #333;
|
|
--card-bg: #fff;
|
|
--border: #e0e0e0;
|
|
}
|
|
|
|
* {
|
|
margin: 0;
|
|
padding: 0;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
body {
|
|
font-family: 'Inter', sans-serif;
|
|
background-color: var(--secondary);
|
|
color: var(--text);
|
|
padding: 20px;
|
|
}
|
|
|
|
h3 {
|
|
margin-bottom: 10px;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.dashboard {
|
|
display: grid;
|
|
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
|
|
gap: 20px;
|
|
margin-left: 280px;
|
|
padding-top: 20px;
|
|
}
|
|
|
|
.card {
|
|
background-color: var(--card-bg);
|
|
border-radius: 10px;
|
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.06);
|
|
padding: 40px;
|
|
border: 1px solid var(--border);
|
|
}
|
|
|
|
.chart-container {
|
|
position: relative;
|
|
width: 100%;
|
|
height: 500px;
|
|
}
|
|
|
|
.section-title {
|
|
margin-top: 40px;
|
|
margin-bottom: 15px;
|
|
font-size: 20px;
|
|
font-weight: 600;
|
|
color: var(--primary);
|
|
}
|
|
|
|
|
|
.all {
|
|
grid-column-start: 1;
|
|
grid-column-end: 3;
|
|
}
|
|
|
|
|
|
@media (max-width: 768px) {
|
|
.dashboard {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
|
|
<body>
|
|
|
|
<div class="container">
|
|
<div class="dashboard">
|
|
<div class="card all">
|
|
<h3>CA % Objectif (<?php echo "$datedeb-$datefin" ?>)</h3>
|
|
<div class="chart-container">
|
|
<canvas id="chart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card all">
|
|
<h3>CA % Client (<?php echo "$datedeb-$datefin" ?>)</h3>
|
|
<div class="chart-container">
|
|
<canvas id="myChart"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Par produit -->
|
|
<div class="card all">
|
|
<div class="section-title">Par Produit</div>
|
|
<div class="chart-container">
|
|
<canvas id="marginChart" width="600" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Par produit donuts -->
|
|
<div class="card mt-4">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Répartition Marge Totale (Top 10 Produits)</h5>
|
|
<canvas id="productDonutChart" width="400" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-4">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Répartition Quantité Totale (Top 10 Produits - Pie Chart)</h5>
|
|
<canvas id="productQuantityPieChart" width="400" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Marge par client</h5>
|
|
<canvas id="myDonutChart" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="card mt-4">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Répartition CA par VP (Top 10)</h5>
|
|
<canvas id="vpDonutChart" width="400" height="400"></canvas>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="card all">
|
|
<div class="section-title">Par VP</div>
|
|
<canvas id="vpChart" width="600" height="400"></canvas>
|
|
</div>
|
|
|
|
<div class="card all">
|
|
<div class="card-body">
|
|
<h5 class="card-title">CA par Client</h5>
|
|
<div class="table-responsive">
|
|
<table id="clientMarginTable" class="table table-striped table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Client</th>
|
|
<th>Marge Totale (D.A)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($result_client as $row): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($row['accountname']) ?></td>
|
|
<td><?= $row['totalmargin'] ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
|
|
<div class="card mt-4 all">
|
|
<div class="card-body">
|
|
<h5 class="card-title">Produits : Quantité & Marge Totale</h5>
|
|
<div class="table-responsive">
|
|
<table id="productTable" class="table table-striped table-bordered">
|
|
<thead>
|
|
<tr>
|
|
<th>Produit</th>
|
|
<th>Quantité Totale</th>
|
|
<th>Marge Totale (D.A)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<?php foreach ($result_products_arr as $row): ?>
|
|
<tr>
|
|
<td><?= htmlspecialchars($row['productname']) ?></td>
|
|
<td><?= $row['totalquantity'] ?></td>
|
|
<td><?= $row['totalmargin'] ?></td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<!-- Your Chart.js scripts here -->
|
|
<script>
|
|
const currentValue = <?php echo $currentValue; ?>;
|
|
const objective = <?php echo $objective; ?>;
|
|
const value = currentValue * 100 / objective;
|
|
|
|
const gaugeConfig = {
|
|
type: 'gauge',
|
|
data: {
|
|
// labels: ['Fail', 'Warning', 'Success'],
|
|
datasets: [{
|
|
data: [30, 80, 100],
|
|
value: value.toFixed(2),
|
|
backgroundColor: ['#f44336', '#ff9800', '#4caf50'],
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
title: {
|
|
display: true,
|
|
text: 'CA Réalisé : ' + currentValue.toLocaleString() + ' D.A',
|
|
fontSize: 16
|
|
},
|
|
layout: {
|
|
padding: {
|
|
bottom: 20
|
|
}
|
|
},
|
|
needle: {
|
|
radiusPercentage: 2,
|
|
widthPercentage: 2.2,
|
|
lengthPercentage: 60,
|
|
color: 'rgba(0,0,0,1)'
|
|
},
|
|
valueLabel: {
|
|
display: true
|
|
},
|
|
plugins: {
|
|
datalabels: {
|
|
display: true,
|
|
formatter: (value, context) => context.chart.data.labels[context.dataIndex],
|
|
color: '#000',
|
|
font: {
|
|
size: 10,
|
|
weight: 'bold'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
const ctx = document.getElementById('chart').getContext('2d');
|
|
new Chart(ctx, gaugeConfig);
|
|
|
|
//
|
|
const phpData = <?php echo $json_data; ?>;
|
|
const barLabels = phpData.map(item => item.accountname);
|
|
const barData = phpData.map(item => parseFloat(item.totalmargin));
|
|
|
|
var top10Clients = barData.sort((a, b) => parseFloat(b.totalmargin) - parseFloat(a.totalmargin)) // Descending sort
|
|
.slice(0, 10); // Take top 10
|
|
|
|
const barChart = new Chart(document.getElementById('myChart').getContext('2d'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: barLabels,
|
|
datasets: [{
|
|
label: 'Total CA par Client',
|
|
data: top10Clients,
|
|
backgroundColor: 'rgba(63, 81, 181, 0.6)',
|
|
borderColor: 'rgba(63, 81, 181, 1)',
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
yAxes: [{
|
|
ticks: {
|
|
beginAtZero: true,
|
|
callback: value => value.toLocaleString() + ' D.A'
|
|
}
|
|
}],
|
|
xAxes: [{
|
|
ticks: {
|
|
autoSkip: false,
|
|
maxRotation: 45,
|
|
minRotation: 45
|
|
}
|
|
}]
|
|
},
|
|
// ✅ Add this to show labels over bars
|
|
plugins: {
|
|
datalabels: {
|
|
anchor: 'end',
|
|
align: 'right',
|
|
color: '#000',
|
|
offset: 6,
|
|
clamp: true,
|
|
clip: false,
|
|
font: {
|
|
size: window.innerWidth < 768 ? 8 : 10,
|
|
weight: 'bold'
|
|
},
|
|
formatter: function(value, context) {
|
|
// Format with thousands separator and decimals
|
|
return Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}) + (context.dataset.label === 'Total Margin' ? ' D.A' : '');
|
|
}
|
|
}
|
|
},
|
|
layout: {
|
|
padding: {
|
|
left: 40
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// PRODUCTS
|
|
const result_products = <?php echo $result_products; ?>;
|
|
// Step 1: Sort by totalmargin in descending order
|
|
const top10Products = result_products
|
|
.sort((a, b) => parseFloat(b.totalmargin) - parseFloat(a.totalmargin)) // Descending sort
|
|
.slice(0, 10); // Take top 10
|
|
|
|
// Step 2: Extract data for use (e.g., charts)
|
|
const productNames = top10Products.map(item => item.productname);
|
|
const totalquantity = top10Products.map(item => parseFloat(item.totalquantity));
|
|
const totalMargins = top10Products.map(item => parseFloat(item.totalmargin));
|
|
const ctxMarginChart = document.getElementById('marginChart').getContext('2d');
|
|
const marginChart = new Chart(ctxMarginChart, {
|
|
type: 'horizontalBar', // horizontal bar in Chart.js 2.8
|
|
data: {
|
|
labels: productNames,
|
|
datasets: [{
|
|
label: 'Total',
|
|
data: totalquantity,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
},
|
|
{
|
|
label: 'Total Margin',
|
|
data: totalMargins,
|
|
backgroundColor: 'rgba(255, 99, 132, 0.6)',
|
|
borderColor: 'rgba(255, 99, 132, 1)',
|
|
borderWidth: 1
|
|
}
|
|
]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
xAxes: [{
|
|
ticks: {
|
|
beginAtZero: true,
|
|
callback: function(value) {
|
|
// Format ticks with thousands separator, 2 decimals for margin, 0 for quantity
|
|
// Assuming values for margin may have decimals; quantity probably integers
|
|
return Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 2
|
|
});
|
|
}
|
|
}
|
|
}],
|
|
yAxes: [{
|
|
barPercentage: 0.6
|
|
}]
|
|
},
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
tooltips: {
|
|
callbacks: {
|
|
label: function(tooltipItem, data) {
|
|
const datasetLabel = data.datasets[tooltipItem.datasetIndex].label || '';
|
|
const value = tooltipItem.xLabel;
|
|
// Format tooltip number with 2 decimals + thousands separator
|
|
return `${datasetLabel}: ${Number(value).toLocaleString('fr-FR', { minimumFractionDigits: 2, maximumFractionDigits: 2 })} ${datasetLabel === 'Total Margin' ? 'D.A' : ''}`;
|
|
}
|
|
}
|
|
},
|
|
// ✅ Add this to show labels over bars
|
|
plugins: {
|
|
datalabels: {
|
|
anchor: 'end',
|
|
align: 'right',
|
|
color: '#000',
|
|
font: {
|
|
weight: 'bold',
|
|
size: 10
|
|
},
|
|
formatter: function(value, context) {
|
|
// Format with thousands separator and decimals
|
|
return Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}) + (context.dataset.label === 'Total Margin' ? ' D.A' : '');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// Donuts Products
|
|
// 🎯 Donut chart for Top 10 Products by Total Margin
|
|
const productDonutColors = [
|
|
'#4caf50', '#ff9800', '#2196f3', '#e91e63', '#9c27b0',
|
|
'#00bcd4', '#ffc107', '#8bc34a', '#ff5722', '#3f51b5'
|
|
];
|
|
const ctxProductDonut = document.getElementById('productDonutChart').getContext('2d');
|
|
const productDonutChart = new Chart(ctxProductDonut, {
|
|
type: 'pie', // 🎯 Pie chart type
|
|
data: {
|
|
labels: productNames,
|
|
datasets: [{
|
|
data: totalMargins,
|
|
backgroundColor: productDonutColors,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
cutoutPercentage: 60,
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
boxWidth: 12
|
|
}
|
|
},
|
|
tooltips: {
|
|
callbacks: {
|
|
label: function(tooltipItem, data) {
|
|
const index = tooltipItem.index;
|
|
const name = data.labels[index];
|
|
const value = data.datasets[0].data[index];
|
|
return `${name}: ${Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
})} D.A`;
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
datalabels: {
|
|
color: '#000',
|
|
formatter: (value, ctx) => {
|
|
const total = ctx.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(1);
|
|
return percentage + '%';
|
|
},
|
|
font: {
|
|
weight: 'bold',
|
|
size: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const ctxProductQuantityPie = document.getElementById('productQuantityPieChart').getContext('2d');
|
|
|
|
const productQuantityPieChart = new Chart(ctxProductQuantityPie, {
|
|
type: 'pie',
|
|
data: {
|
|
labels: productNames,
|
|
datasets: [{
|
|
data: totalquantity,
|
|
backgroundColor: productDonutColors,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
boxWidth: 12
|
|
}
|
|
},
|
|
tooltips: {
|
|
callbacks: {
|
|
label: function(tooltipItem, data) {
|
|
const index = tooltipItem.index;
|
|
const name = data.labels[index];
|
|
const value = data.datasets[0].data[index];
|
|
return `${name}: ${Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
})}`;
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
datalabels: {
|
|
color: '#000',
|
|
formatter: (value, context) => {
|
|
const total = context.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(1);
|
|
return percentage + '%';
|
|
},
|
|
font: {
|
|
weight: 'bold',
|
|
size: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// VP
|
|
const result_vp = <?php echo $result_vp; ?>;
|
|
// Step 1: Sort by `bc` descending and take top 10
|
|
const top10VPs = result_vp
|
|
.sort((a, b) => parseFloat(b.bc) - parseFloat(a.bc)) // Sort high to low
|
|
.slice(0, 10); // Take top 10
|
|
|
|
// Step 2: Extract fullname and bc into arrays
|
|
const fullnames = top10VPs.map(item => item.fullname);
|
|
const bc = top10VPs.map(item => parseFloat(item.bc));
|
|
const ctxVphart = document.getElementById('vpChart').getContext('2d');
|
|
const vpChart = new Chart(ctxVphart, {
|
|
type: 'horizontalBar', // ✅ horizontal bar for Chart.js v2.8
|
|
data: {
|
|
labels: fullnames,
|
|
datasets: [{
|
|
label: 'Total',
|
|
data: bc,
|
|
backgroundColor: 'rgba(54, 162, 235, 0.6)',
|
|
borderColor: 'rgba(54, 162, 235, 1)',
|
|
borderWidth: 1
|
|
}, ]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
scales: {
|
|
xAxes: [{
|
|
ticks: {
|
|
beginAtZero: true,
|
|
callback: function(value) {
|
|
return parseFloat(value).toFixed(2).toLocaleString() + " D.A";
|
|
}
|
|
}
|
|
}],
|
|
yAxes: [{
|
|
barPercentage: 0.6
|
|
}]
|
|
},
|
|
legend: {
|
|
display: true,
|
|
position: 'top'
|
|
},
|
|
tooltips: {
|
|
callbacks: {
|
|
label: function(tooltipItem, data) {
|
|
const datasetLabel = (data.datasets[tooltipItem.datasetIndex].label || '');
|
|
const value = parseFloat(tooltipItem.xLabel).toFixed(2)
|
|
return `${datasetLabel}: ${Number(value).toLocaleString()} DA`;
|
|
}
|
|
}
|
|
},
|
|
// ✅ Add this to show labels over bars
|
|
plugins: {
|
|
datalabels: {
|
|
anchor: 'end',
|
|
align: 'top',
|
|
color: '#000',
|
|
font: {
|
|
weight: 'bold',
|
|
size: 10
|
|
},
|
|
formatter: function(value, context) {
|
|
// Format with thousands separator and decimals
|
|
return Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}) + (context.dataset.label === 'Total Margin' ? ' D.A' : '');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
|
|
// donuts top 10 vps
|
|
// 🎯 VP Donut Chart (Top 10)
|
|
const donutColors = [
|
|
'#3f51b5', '#e91e63', '#ff9800', '#4caf50', '#2196f3',
|
|
'#9c27b0', '#00bcd4', '#ffc107', '#8bc34a', '#ff5722'
|
|
];
|
|
|
|
const ctxVPDoughnut = document.getElementById('vpDonutChart').getContext('2d');
|
|
const vpDonutChart = new Chart(ctxVPDoughnut, {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: fullnames,
|
|
datasets: [{
|
|
data: bc,
|
|
backgroundColor: donutColors,
|
|
borderWidth: 1
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
cutoutPercentage: 60, // makes it a donut instead of a pie
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
boxWidth: 12
|
|
}
|
|
},
|
|
tooltips: {
|
|
callbacks: {
|
|
label: function(tooltipItem, data) {
|
|
const index = tooltipItem.index;
|
|
const name = data.labels[index];
|
|
const value = data.datasets[0].data[index];
|
|
return `${name}: ${Number(value).toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
})} D.A`;
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
datalabels: {
|
|
color: '#000',
|
|
formatter: (value, ctx) => {
|
|
const total = ctx.chart.data.datasets[0].data.reduce((a, b) => a + b, 0);
|
|
const percentage = ((value / total) * 100).toFixed(1);
|
|
return percentage + '%';
|
|
},
|
|
font: {
|
|
weight: 'bold',
|
|
size: 10
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
console.log('top10Clients', top10Clients);
|
|
|
|
|
|
const donutChart = new Chart(document.getElementById('myDonutChart').getContext('2d'), {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: barLabels,
|
|
datasets: [{
|
|
label: 'Répartition du CA par Client',
|
|
data: top10Clients,
|
|
backgroundColor: [
|
|
'rgba(255, 99, 132, 0.6)',
|
|
'rgba(54, 162, 235, 0.6)',
|
|
'rgba(255, 206, 86, 0.6)',
|
|
'rgba(75, 192, 192, 0.6)',
|
|
'rgba(153, 102, 255, 0.6)',
|
|
'rgba(255, 159, 64, 0.6)',
|
|
'rgba(63, 81, 181, 0.6)'
|
|
],
|
|
borderColor: 'white',
|
|
borderWidth: 2
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right'
|
|
},
|
|
tooltip: {
|
|
callbacks: {
|
|
label: function(context) {
|
|
const value = context.raw;
|
|
return `${context.label} : ${value.toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
})} D.A`;
|
|
}
|
|
}
|
|
},
|
|
datalabels: {
|
|
color: '#000',
|
|
font: {
|
|
size: 10,
|
|
weight: 'bold'
|
|
},
|
|
formatter: function(value) {
|
|
const total = top10Clients.reduce((acc, val) => acc + val, 0);
|
|
const percentage = (value / total * 100).toFixed(1);
|
|
return percentage + '%';
|
|
}
|
|
}
|
|
},
|
|
cutout: '60%' // Thickness of the donut hole
|
|
}
|
|
});
|
|
|
|
|
|
|
|
$(document).ready(function() {
|
|
$('#clientMarginTable').DataTable({
|
|
// language: {
|
|
// url: "//cdn.datatables.net/plug-ins/1.13.6/i18n/fr-FR.json"
|
|
// },
|
|
columnDefs: [{
|
|
targets: 1, // Marge totale
|
|
render: function(data, type) {
|
|
const floatVal = parseFloat(data);
|
|
if (type === 'sort' || type === 'type') {
|
|
return floatVal; // return raw number for sorting
|
|
}
|
|
return floatVal.toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}) + ' D.A'; // formatted for display
|
|
}
|
|
}]
|
|
});
|
|
|
|
$('#productTable').DataTable({
|
|
language: {
|
|
// url: "//cdn.datatables.net/plug-ins/1.13.6/i18n/fr-FR.json"
|
|
},
|
|
columnDefs: [{
|
|
targets: 1, // Quantité Totale
|
|
render: function(data, type) {
|
|
const num = parseFloat(data);
|
|
if (type === 'sort' || type === 'type') {
|
|
return num;
|
|
}
|
|
return num.toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 0,
|
|
maximumFractionDigits: 0
|
|
});
|
|
}
|
|
},
|
|
{
|
|
targets: 2, // Marge Totale
|
|
render: function(data, type) {
|
|
const value = parseFloat(data);
|
|
if (type === 'sort' || type === 'type') {
|
|
return value;
|
|
}
|
|
return value.toLocaleString('fr-FR', {
|
|
minimumFractionDigits: 2,
|
|
maximumFractionDigits: 2
|
|
}) + ' D.A';
|
|
}
|
|
}
|
|
]
|
|
});
|
|
});
|
|
</script>
|
|
</body>
|