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:
BACHIR SOULDI
2026-02-17 15:59:31 +01:00
parent 2794e62571
commit 2a647b138a
46 changed files with 25100 additions and 1296 deletions

932
PharmexObjective.php Normal file
View 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>