feat: Enhance email functionality and PDF generation for Sales Orders
- 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.
This commit is contained in:
932
PharmexObjective.php
Normal file
932
PharmexObjective.php
Normal file
@@ -0,0 +1,932 @@
|
||||
<?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>
|
||||
Reference in New Issue
Block a user