fix: corrections et améliorations du 01-04-2026

## Recouvrement
- Liste triée par date décroissante
- Pop-up de confirmation anti-doublons
- Affichage avec permission "Voir" seule

## Mise à jour produit
- Correction erreur "Column count doesn't match" (triggers MySQL corrigés)
- Formulaire corrigé (action, catégorie, champs null)
- Catégorie et date d'arrivage correctement préremplis
- Historique affiche le nom de l'utilisateur qui modifie

## Espace commercial
- Colonne "Disponibilité" ajoutée avec statut "En attente de livraison"
- Bouton panier caché pour les motos commandées
- Surbrillance jaune pour les motos en attente

## Notifications
- Caissière reçoit les notifications via notifCommande
- notifSortieCaisse réservé à Direction/Admin

## Avances
- Colonne "N° Série" ajoutée dans toutes les listes
- Compteurs sur les boutons Incomplètes/Complètes

## Facture / BL
- Total, Remise, Total à payer affichés
- "Ariary" ne se répète plus
- Prix individuel par moto
- Impression automatique : 1 produit = Facture, 2+ = BL
- Remise multiple : colonne product changée en TEXT

## Rapports
- Filtre par date dans le rapport stock
- Filtre par commercial et mécanicien dans les performances
- Correction rapport stock (GROUP BY marque)
- Liens absolus (correction erreur 404)

## Sidebar
- Marge en haut supprimée (production)
- Padding en bas ajouté pour scroll complet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
andrymodeste 2026-04-01 19:56:34 +02:00
parent a195d24e78
commit fe80b9c4f8
23 changed files with 534 additions and 313 deletions

View File

@ -226,7 +226,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->get('detail/stock', [ReportController::class, 'stockDetail']);
// Corrections fetct → fetch
$routes->get('detail/fetchData/(:num)', [ReportController::class, 'fetchProductSold/$1']);
$routes->get('detail/fetchData/(:num)', [ReportController::class, 'fetchProductSodled/$1']);
$routes->get('detail/fetchDataStock/(:num)', [ReportController::class, 'fetchProductStock/$1']);
$routes->get('detail/fetchDataStock2/(:num)', [ReportController::class, 'fetchProductStock2/$1']);

View File

@ -24,7 +24,7 @@ abstract class AdminController extends BaseController
$group_data = $Groups->getUserGroupByUserId($userId);
$this->permission = unserialize($group_data['permission']);
}
}

View File

@ -129,7 +129,8 @@ public function loginPost()
$buttons .= " <a href='/ventes/show/" . $value['id'] . "' class='btn btn-default'><i class='fa fa-eye'></i></a>";
}
if (is_array($this->permission) && in_array('createOrder', $this->permission)) {
$productSoldStatus = (int)($value['product_sold'] ?? 0);
if (is_array($this->permission) && in_array('createOrder', $this->permission) && $productSoldStatus === 0) {
$buttons .= ($value['qty'] == 1)
? " <a href='/orders/createFromEspace/" . $value['id'] . "' class='btn btn-default'><i class='fa fa-shopping-cart'></i></a>"
: " <button class='btn btn-default' title='0 en stock'><i class='fa fa-shopping-cart'></i></button>";
@ -139,6 +140,16 @@ public function loginPost()
$img = '<img src="' . base_url('assets/images/product_image/' . $value['image']) . '" alt="' . $value['image'] . '" class="img-circle" width="50" height="50" />';
// Statut basé sur product_sold
$productSold = (int)($value['product_sold'] ?? 0);
if ($productSold === 2) {
$statut = '<span class="label label-warning">En attente de livraison</span>';
} elseif ($productSold === 1) {
$statut = '<span class="label label-default">Livré</span>';
} else {
$statut = '<span class="label label-success">Disponible</span>';
}
// Populate the result data
$result['data'][] = [
$img,
@ -147,6 +158,7 @@ public function loginPost()
number_format($value['prix_vente'], 0, ',', ' '),
$value['puissance'] . ' CC',
$value['numero_de_moteur'],
$statut,
$buttons
];
}

View File

@ -116,21 +116,23 @@ private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCai
{
$date_time = date('d-m-Y h:i a', strtotime($value['avance_date']));
// ✅ Gestion sécurisée du nom du produit
// Gestion du nom du produit et numéro de série
$productSku = '';
if ($value['type_avance'] === 'mere') {
$productName = $value['product_name'] ?? 'Produit sur mer';
} else {
$productName = !empty($value['product_name'])
? $value['product_name']
: $product->getProductNameById($value['product_id'] ?? 0);
$productData = !empty($value['product_id']) ? $product->find($value['product_id']) : null;
$productName = $productData['name'] ?? ($value['product_name'] ?? 'N/A');
$productSku = $productData['sku'] ?? '';
}
if ($isAdmin) {
return [
$value['customer_name'],
$value['customer_phone'],
$value['customer_address'],
$productName,
$productSku,
number_format((int)$value['gross_amount'], 0, ',', ' '),
number_format((int)$value['avance_amount'], 0, ',', ' '),
number_format((int)$value['amount_due'], 0, ',', ' '),
@ -141,6 +143,7 @@ private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCai
return [
$value['avance_id'],
$productName,
$productSku,
number_format((int)$value['avance_amount'], 0, ',', ' '),
number_format((int)$value['amount_due'], 0, ',', ' '),
$date_time,
@ -228,12 +231,13 @@ private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData')
$date_time = date('d-m-Y h:i a', strtotime($value['avance_date']));
$productSku = '';
if ($value['type_avance'] === 'mere') {
$productName = $value['product_name'] ?? 'Produit sur mer';
} else {
$productName = !empty($value['product_name'])
? $value['product_name']
: $product->getProductNameById($value['product_id'] ?? 0);
$productData = !empty($value['product_id']) ? $product->find($value['product_id']) : null;
$productName = $productData['name'] ?? ($value['product_name'] ?? 'N/A');
$productSku = $productData['sku'] ?? '';
}
if ($isAdmin) {
@ -242,6 +246,7 @@ private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData')
$value['customer_phone'],
$value['customer_address'],
$productName,
$productSku,
number_format((int)$value['gross_amount'], 0, ',', ' '),
number_format((int)$value['avance_amount'], 0, ',', ' '),
$status,
@ -252,6 +257,7 @@ private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData')
$result['data'][] = [
$value['avance_id'],
$productName,
$productSku,
number_format((int)$value['avance_amount'], 0, ',', ' '),
$status,
$date_time,

View File

@ -71,6 +71,7 @@ class HistoriqueController extends AdminController
$row['product_name'] ?? 'N/A',
$row['sku'] ?? 'N/A',
$row['store_name'] ?? 'N/A',
$row['user_name'] ?? 'N/A',
$this->getActionBadge($row['action']),
$row['description'] ?? ''
];

View File

@ -298,7 +298,7 @@ class MecanicienController extends AdminController
$date_debut = date("d/m/Y", strtotime($repa['reparation_debut']));
$date_fin = date("d/m/Y", strtotime($repa['reparation_fin']));
$storeName = $repa['store_name'] ?? 'N/A';
$result['data'][$key] = [
$user_name,
$image,

View File

@ -663,16 +663,21 @@ class OrderController extends AdminController
$Notification->notifyGroupsByPermissionAllStores('notifRemise', $message, 'remise/');
} else {
// Commande sans remise
$Notification->notifyGroupsByPermission(
'notifSortieCaisse',
"📦 Nouvelle commande à valider : {$bill_no}<br>" .
// Notifier la Caissière (notifCommande) et l'admin/direction (notifSortieCaisse)
$messageCommande = "📦 Nouvelle commande (avec remise) à traiter : {$bill_no}<br>" .
"Client : {$data['customer_name']}<br>" .
"Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar",
(int)$users['store_id'],
"orders"
);
"Montant demandé : " . number_format($discount, 0, ',', ' ') . " Ar<br>" .
"En attente de validation remise";
$Notification->notifyGroupsByPermission('notifCommande', $messageCommande, (int)$users['store_id'], "orders");
$Notification->notifyGroupsByPermission('notifSortieCaisse', $messageCommande, (int)$users['store_id'], "orders");
} else {
// Commande sans remise - notifier Caissière (notifCommande) et admin/direction (notifSortieCaisse)
$messageCommande = "📦 Nouvelle commande à traiter : {$bill_no}<br>" .
"Client : {$data['customer_name']}<br>" .
"Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar";
$Notification->notifyGroupsByPermission('notifCommande', $messageCommande, (int)$users['store_id'], "orders");
$Notification->notifyGroupsByPermission('notifSortieCaisse', $messageCommande, (int)$users['store_id'], "orders");
}
if ($users["group_name"] != "COMMERCIALE") {
@ -1721,23 +1726,18 @@ public function update(int $id)
}
public function printDiv(int $id)
public function printDiv(int $id)
{
$Orders = new Orders();
$OrderItems = new OrderItems();
$order = $Orders->getOrdersData($id);
$docType = $order['document_type'] ?? 'facture';
// Rediriger vers la bonne méthode selon le type
switch($docType) {
case 'facture':
return $this->print31($id); // Factures individuelles
case 'bl':
return $this->print7($id); // Bon de livraison
case 'both':
return $this->print31($id); // Factures + Bon de livraison
default:
return $this->print31($id);
$items = $OrderItems->getOrdersItemData($id);
// Plus d'1 produit = BL, 1 seul produit = Facture
if (count($items) > 1) {
return $this->print7($id); // Bon de livraison
} else {
return $this->print5($id); // Facture
}
}
@ -2007,23 +2007,26 @@ public function print5(int $id)
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount;
$inWords = strtoupper($this->numberToWords((int) round($totalTTC)));
$totalAPayer = ($discount > 0) ? $discount : $grossAmount;
$remiseAmount = ($discount > 0) ? ($grossAmount - $discount) : 0;
$inWords = strtoupper($this->numberToWords((int) round($totalAPayer)));
// Construire les lignes du tableau
$tableRows = '';
$totalPrixIndividuels = 0;
foreach ($items as $it) {
$details = $this->getOrderItemDetails($it);
if (!$details) continue;
$qty = isset($it['qty']) ? (int)$it['qty'] : 1;
$prix = ($discount > 0) ? ($discount / $qty) : $details['prix'];
$prixItem = $details['prix'] * $qty;
$totalPrixIndividuels += $prixItem;
$tableRows .= '<tr>
<td>'.esc($details['marque']).'</td>
<td>'.esc($details['numero_chassis']).'</td>
<td>'.esc($details['puissance']).'</td>
<td>'.number_format($prix * $qty, 0, ' ', ' ').' MGA</td>
<td>'.number_format($prixItem, 0, ' ', ' ').'</td>
</tr>';
}
@ -2043,7 +2046,7 @@ public function print5(int $id)
$companyPhone = esc($company['phone']);
$companyPhone2 = esc($company['phone2']);
$companyAddress = esc($company['address'] ?? '');
$totalFormatted = number_format($totalTTC, 0, ' ', ' ');
$totalFormatted = number_format($totalAPayer, 0, ' ', ' ');
$qrValue = "FACTURE N° {$billNo} | Client: {$customerName} | Montant: {$totalFormatted} MGA | Date: {$today} | Facebook: https://www.facebook.com/MOTORBIKESTORE2021/";
$html = '<!DOCTYPE html>
@ -2058,7 +2061,8 @@ public function print5(int $id)
body {
font-family: Arial, sans-serif;
font-size: 11px;
font-size: 13px;
line-height: 1.3;
color: #000;
}
@ -2084,32 +2088,33 @@ public function print5(int $id)
/* Header */
.f-title {
font-size: 22px;
font-size: 24px;
font-weight: bold;
text-decoration: underline;
margin-bottom: 10px;
margin-bottom: 12px;
}
.f-company {
font-size: 10px;
line-height: 1.5;
font-size: 12px;
line-height: 1.3;
margin-bottom: 8px;
}
.f-company strong { font-size: 12px; }
.f-company strong { font-size: 14px; }
.f-doit {
font-weight: bold;
font-size: 12px;
margin-bottom: 5px;
font-size: 14px;
margin-bottom: 6px;
}
.f-client {
font-size: 10px;
font-size: 12px;
line-height: 1.3;
margin-bottom: 10px;
}
.f-client .field { margin: 2px 0; }
.f-client .field { margin: 3px 0; }
/* Table */
.f-table {
@ -2121,9 +2126,9 @@ public function print5(int $id)
.f-table th, .f-table td {
border-left: 2px solid #000;
border-right: 2px solid #000;
padding: 6px 8px;
padding: 8px 10px;
text-align: center;
font-size: 10px;
font-size: 12px;
}
.f-table th {
@ -2132,7 +2137,7 @@ public function print5(int $id)
border-bottom: 2px solid #000;
}
.f-table td { height: 28px; }
.f-table td { height: 30px; }
.f-table tbody tr:last-child td {
border-bottom: 2px solid #000;
@ -2140,7 +2145,8 @@ public function print5(int $id)
/* Total en lettres */
.f-words {
font-size: 10px;
font-size: 12px;
line-height: 1.3;
margin-bottom: 12px;
}
@ -2160,7 +2166,7 @@ public function print5(int $id)
.f-sig-box .sig-label {
font-weight: bold;
font-size: 11px;
font-size: 13px;
}
/* Date + N° + QR en bas */
@ -2172,7 +2178,7 @@ public function print5(int $id)
}
.f-bottom .f-date-info {
font-size: 10px;
font-size: 12px;
}
.f-bottom canvas {
@ -2196,13 +2202,13 @@ public function print5(int $id)
display: flex;
flex-direction: column;
border-bottom: 2px dashed #aaa;
line-height: 1.6;
line-height: 1.3;
}
.cond-box:last-child { border-bottom: none; }
.cond-title {
font-size: 14px;
font-size: 16px;
font-weight: bold;
font-style: italic;
text-decoration: underline;
@ -2212,7 +2218,8 @@ public function print5(int $id)
.cond-box ul {
list-style: none;
padding: 0;
font-size: 10px;
font-size: 12px;
line-height: 1.3;
}
.cond-box li {
@ -2261,6 +2268,8 @@ public function print5(int $id)
$qrId = 'qr_recto_'.$i;
$html .= '
<div class="facture-box">
<div class="f-title" style="text-align:center;">FACTURE</div>
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:8px;">
<div class="f-company" style="margin-bottom:0;">
<strong>'.$companyName.'</strong><br>
@ -2269,15 +2278,13 @@ public function print5(int $id)
Contact : '.$companyPhone.' / '.$companyPhone2.'<br>
'.$companyAddress.'
</div>
<div style="text-align:right; font-size:10px;">
<div style="text-align:right; font-size:12px;">
<div>DATE : '.$today.'</div>
<div> : '.$billNo.'</div>
<canvas id="'.$qrId.'" style="margin-top:5px;"></canvas>
</div>
</div>
<div class="f-title" style="text-align:center;">FACTURE</div>
<div class="f-doit">DOIT</div>
<div class="f-client">
@ -2302,7 +2309,14 @@ public function print5(int $id)
</table>
<div class="f-words">
<strong>Arrête à la somme de :</strong> '.$inWords.' Ariary
<strong>Total :</strong> '.number_format($totalPrixIndividuels, 0, ' ', ' ').' Ariary<br>';
if ($remiseAmount > 0) {
$html .= '<strong>Remise :</strong> -'.number_format($remiseAmount, 0, ' ', ' ').' Ariary<br>
<strong>Total à payer :</strong> '.number_format($totalAPayer, 0, ' ', ' ').' Ariary<br>';
}
$html .= '<strong>Arrête à la somme de :</strong> '.$inWords.'
</div>
<div class="f-signatures">
@ -2532,22 +2546,26 @@ public function print7(int $id)
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
$totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalAPayer = ($discount > 0) ? $discount : $grossAmount;
$remiseAmount = ($discount > 0) ? ($grossAmount - $discount) : 0;
$inWords = strtoupper($this->numberToWords((int) round($totalAPayer)));
// Construire les lignes du tableau
// Construire les lignes du tableau - prix individuel de chaque moto
$tableRows = '';
$totalPrixIndividuels = 0;
foreach ($items as $item) {
$details = $this->getOrderItemDetails($item);
if (!$details) continue;
$qty = isset($item['qty']) ? (int)$item['qty'] : 1;
$prix = ($discount > 0) ? ($discount / $qty) : $details['prix'];
$prixItem = $details['prix'] * $qty;
$totalPrixIndividuels += $prixItem;
$tableRows .= '<tr>
<td>'.esc($details['marque']).'</td>
<td>'.esc($details['numero_moteur']).'</td>
<td>'.esc($details['puissance']).'</td>
<td>'.number_format($prix * $qty, 0, '', ' ').'</td>
<td>'.number_format($prixItem, 0, '', ' ').'</td>
</tr>';
}
@ -2573,8 +2591,8 @@ public function print7(int $id)
body {
font-family: Arial, sans-serif;
font-size: 13px;
line-height: 1.5;
font-size: 14px;
line-height: 1.3;
background: #fff;
}
@ -2596,12 +2614,12 @@ public function print7(int $id)
}
.bl-company {
font-size: 12px;
line-height: 1.6;
font-size: 13px;
line-height: 1.3;
}
.bl-company strong {
font-size: 15px;
font-size: 16px;
}
.bl-title-block {
@ -2629,11 +2647,12 @@ public function print7(int $id)
/* Client info */
.bl-client {
margin-bottom: 12px;
font-size: 13px;
font-size: 14px;
line-height: 1.3;
}
.bl-client .field {
margin: 3px 0;
margin: 4px 0;
}
.bl-client .field strong {
@ -2643,7 +2662,7 @@ public function print7(int $id)
.bl-doit {
font-weight: bold;
font-size: 14px;
font-size: 15px;
margin-bottom: 8px;
}
@ -2651,29 +2670,24 @@ public function print7(int $id)
.bl-table {
width: 100%;
border-collapse: collapse;
flex: 1;
}
.bl-table th, .bl-table td {
border-left: 2px solid #000;
border-right: 2px solid #000;
padding: 10px 15px;
padding: 5px 10px;
text-align: center;
font-size: 13px;
font-size: 14px;
}
.bl-table th {
font-weight: bold;
font-size: 13px;
font-size: 14px;
background: #fff;
border-top: 2px solid #000;
border-bottom: 2px solid #000;
}
.bl-table td {
height: 35px;
}
.bl-table tbody tr:last-child td {
border-bottom: 2px solid #000;
}
@ -2684,7 +2698,8 @@ public function print7(int $id)
}
.bl-total {
font-size: 14px;
font-size: 15px;
line-height: 1.3;
margin-bottom: 15px;
}
@ -2758,7 +2773,14 @@ public function print7(int $id)
<!-- Footer -->
<div class="bl-footer">
<div class="bl-total">
<strong>Arrête à la somme de :</strong> '.number_format($totalTTC, 0, '', ' ').' Ariary
<strong>Total :</strong> '.number_format($totalPrixIndividuels, 0, '', ' ').' Ariary<br>';
if ($remiseAmount > 0) {
$html .= '<strong>Remise :</strong> -'.number_format($remiseAmount, 0, '', ' ').' Ariary<br>
<strong>Total à payer :</strong> '.number_format($totalAPayer, 0, '', ' ').' Ariary<br>';
}
$html .= '<strong>Arrête à la somme de :</strong> '.$inWords.'
</div>
<div class="bl-signatures">
<div class="bl-sig-box">
@ -2775,7 +2797,7 @@ public function print7(int $id)
<script>
new QRious({
element: document.getElementById("qrcode"),
value: "BON DE LIVRAISON\\nN° '.esc($order['bill_no']).'\\nClient: '.esc($order['customer_name']).'\\nMontant: '.number_format($totalTTC, 0, '', ' ').' Ar\\nDate: '.$today.'\\nFacebook: https://www.facebook.com/MOTORBIKESTORE2021/",
value: "BON DE LIVRAISON\\nN° '.esc($order['bill_no']).'\\nClient: '.esc($order['customer_name']).'\\nMontant: '.number_format($totalAPayer, 0, '', ' ').' Ar\\nDate: '.$today.'\\nFacebook: https://www.facebook.com/MOTORBIKESTORE2021/",
size: 90,
level: "H"
});

View File

@ -101,15 +101,20 @@ class ProductCOntroller extends AdminController
$store_name = $store_info && isset($store_info['name']) ? $store_info['name'] : "Inconnu";
}
// Disponibilité basée sur qty ET availability
// Disponibilité basée sur qty, availability et product_sold
$productSold = (int)($value['product_sold'] ?? 0);
$isInStock = ((int)$value['qty'] > 0);
$isAvailable = ((int)$value['availability'] === 1);
$isProductAvailable = $isInStock && $isAvailable;
$availability = $isProductAvailable ?
'<span class="label label-success">En stock</span>' :
'<span class="label label-danger">Rupture</span>';
if ($productSold === 2) {
$availability = '<span class="label label-warning">En attente de livraison</span>';
} elseif ($productSold === 1) {
$availability = '<span class="label label-default">Livré</span>';
} elseif ($isInStock && $isAvailable) {
$availability = '<span class="label label-success">En stock</span>';
} else {
$availability = '<span class="label label-danger">Rupture</span>';
}
// Construction des boutons
$buttons = '';
@ -224,7 +229,7 @@ class ProductCOntroller extends AdminController
'date_arivage' => $this->request->getPost('datea'),
'puissance' => $this->request->getPost('puissance'),
'cler' => $this->request->getPost('cler'),
'categorie_id' => json_encode($this->request->getPost('categorie[]')),
'categorie_id' => $this->request->getPost('categorie') ? implode(',', $this->request->getPost('categorie')) : '',
'etats' => $this->request->getPost('etats'),
'infoManquekit' => $this->request->getPost('infoManquekit'),
'info' => $this->request->getPost('info'),
@ -297,6 +302,11 @@ class ProductCOntroller extends AdminController
public function update(int $id)
{
log_message('error', '=== PRODUCT UPDATE CALLED === ID: ' . $id . ' METHOD: ' . $_SERVER['REQUEST_METHOD']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
log_message('error', 'POST DATA KEYS: ' . implode(', ', array_keys($_POST)));
}
$Products = new Products();
$Stores = new Stores();
$Category = new Category();
@ -315,33 +325,33 @@ class ProductCOntroller extends AdminController
$product_data = $Products->getProductData($id);
$prix_minimal_data = $FourchettePrix->where('product_id', $id)->first();
$prix_minimal = $prix_minimal_data['prix_minimal'] ?? '';
if (strtolower($this->request->getMethod()) === 'post' && $validation->withRequest($this->request)->run()) {
if ($_SERVER['REQUEST_METHOD'] === 'POST' && $validation->withRequest($this->request)->run()) {
$availabilityValue = (int)$this->request->getPost('availability');
$availability = ($availabilityValue === 1) ? 1 : 0;
$data = [
'name' => $this->request->getPost('nom_de_produit'),
'sku' => $this->request->getPost('numero_de_serie'),
'price' => $this->request->getPost('price'),
'name' => $this->request->getPost('nom_de_produit') ?? '',
'sku' => $this->request->getPost('numero_de_serie') ?? '',
'price' => $this->request->getPost('price') ?? '0',
'qty' => 1,
'description' => $this->request->getPost('description'),
'numero_de_moteur' => $this->request->getPost('numero_de_moteur'),
'marque' => $this->request->getPost('marque'),
'chasis' => $this->request->getPost('chasis'),
'description' => $this->request->getPost('description') ?? '',
'numero_de_moteur' => $this->request->getPost('numero_de_moteur') ?? '',
'marque' => $this->request->getPost('marque') ?? '0',
'chasis' => $this->request->getPost('chasis') ?? '',
'store_id' => (int)$this->request->getPost('store'),
'availability'=> $availability,
'prix_vente' => $this->request->getPost('price_vente'),
'date_arivage'=> $this->request->getPost('datea'),
'puissance' => $this->request->getPost('puissance'),
'cler' => $this->request->getPost('cler'),
'categorie_id'=> json_encode($this->request->getPost('categorie[]')),
'etats' => $this->request->getPost('etats'),
'infoManquekit'=> $this->request->getPost('infoManquekit'),
'info' => $this->request->getPost('info'),
'infoManque' => $this->request->getPost('infoManque'),
'type' => $this->request->getPost('type'),
'prix_vente' => $this->request->getPost('price_vente') ?? '0',
'date_arivage'=> $this->request->getPost('datea') ?: date('Y-m-d'),
'puissance' => $this->request->getPost('puissance') ?? '',
'cler' => $this->request->getPost('cler') ?? '',
'categorie_id'=> $this->request->getPost('categorie') ? implode(',', $this->request->getPost('categorie')) : '',
'etats' => $this->request->getPost('etats') ?? '',
'infoManquekit'=> $this->request->getPost('infoManquekit') ?? '',
'info' => $this->request->getPost('info') ?? '',
'infoManque' => $this->request->getPost('infoManque') ?? '',
'type' => $this->request->getPost('type') ?? '',
];
// ---- GESTION PRIX MINIMAL ----
@ -367,17 +377,39 @@ class ProductCOntroller extends AdminController
}
// Upload image si fournie
if ($this->request->getFile('product_image')->isValid()) {
$productImage = $this->request->getFile('product_image');
if ($productImage && $productImage->isValid() && !$productImage->hasMoved()) {
$uploadImage = $this->uploadImage();
$Products->update($id, ['image' => $uploadImage]);
}
// Mise à jour du produit
if ($Products->updateProduct($data, $id)) {
session()->setFlashdata('success', 'Produit mis à jour avec succès.');
return redirect()->to('/products');
} else {
session()->setFlashdata('errors', 'Une erreur est survenue lors de la mise à jour.');
try {
$db = \Config\Database::connect();
$session = session();
$currentUser = $session->get('user');
$userName = ($currentUser['firstname'] ?? '') . ' ' . ($currentUser['lastname'] ?? '');
$userId = $currentUser['id'] ?? 0;
// Passer l'utilisateur au trigger via variables MySQL
$db->query("SET @current_user_id = ?", [$userId]);
$db->query("SET @current_user_name = ?", [trim($userName)]);
$builder = $db->table('products');
$result = $builder->where('id', $id)->update($data);
if ($result) {
session()->setFlashdata('success', 'Produit mis à jour avec succès.');
return redirect()->to('/products');
} else {
log_message('error', 'Update produit failed - DB error: ' . $db->error()['message']);
session()->setFlashdata('errors', 'Une erreur est survenue lors de la mise à jour.');
return redirect()->to('/products/update/' . $id);
}
} catch (\Exception $e) {
log_message('error', 'Erreur update produit: ' . $e->getMessage());
log_message('error', 'Data: ' . json_encode($data));
session()->setFlashdata('errors', 'Erreur: ' . $e->getMessage());
return redirect()->to('/products/update/' . $id);
}

View File

@ -138,24 +138,30 @@ class RecouvrementController extends AdminController
"data" => []
];
foreach ($data as $key => $value) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
$hasActions = in_array('updateRecouvrement', $this->permission) || in_array('deleteRecouvrement', $this->permission);
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$result['data'][$key] = [
foreach ($data as $key => $value) {
$row = [
$value['recouvrement_id'],
number_format($value['recouvrement_montant'], 0, '.', ' '),
$value['recouvrement_date'],
$value['recouvrement_personnel'],
$value['send_money'],
$value['get_money'],
$buttons
];
if ($hasActions) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$row[] = $buttons;
}
$result['data'][$key] = $row;
}
return $this->response->setJSON($result);
}
@ -170,24 +176,30 @@ class RecouvrementController extends AdminController
"data" => []
];
foreach ($data as $key => $value) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
$hasActions = in_array('updateRecouvrement', $this->permission) || in_array('deleteRecouvrement', $this->permission);
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$result['data'][$key] = [
foreach ($data as $key => $value) {
$row = [
$value['recouvrement_id'],
number_format($value['recouvrement_montant'], 0, '.', ' '),
$value['recouvrement_date'],
$value['recouvrement_personnel'],
$value['send_money'],
$value['get_money'],
$buttons
];
if ($hasActions) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$row[] = $buttons;
}
$result['data'][$key] = $row;
}
return $this->response->setJSON($result);
}

View File

@ -201,7 +201,7 @@ class ReportController extends AdminController
{
$Products = new Products();
$produitStock = $Products->getProductData2($id);
$produitStock = $Products->getStockByBrand($id);
$result = ['data' => []];
foreach ($produitStock as $key => $value) {
@ -251,10 +251,24 @@ class ReportController extends AdminController
$data['page_title'] = $this->pageTitle;
$Stores = new Stores();
// echo '<pre>';
// die(var_dump($orderTest));
$data['stores'] = $Stores->getActiveStore();
// Récupérer les commerciaux et mécaniciens pour les filtres
$db = \Config\Database::connect();
$data['commerciaux'] = $db->table('users u')
->select('u.id, u.firstname, u.lastname')
->join('user_group ug', 'u.id = ug.user_id')
->join('groups g', 'ug.group_id = g.id')
->where('g.group_name', 'COMMERCIALE')
->get()->getResultArray();
$data['mecaniciens'] = $db->table('users u')
->select('u.id, u.firstname, u.lastname')
->join('user_group ug', 'u.id = ug.user_id')
->join('groups g', 'ug.group_id = g.id')
->where('g.group_name', 'MECANICIEN')
->get()->getResultArray();
return $this->render_template('reports/performance', $data);
}
@ -265,32 +279,27 @@ class ReportController extends AdminController
$session = session();
$users = $session->get('user');
// ✅ RÉCUPÉRER LES PARAMÈTRES DE FILTRE
// Récupérer les paramètres de filtre
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$pvente = $this->request->getGet('pvente');
// ✅ CORRECTION : Bonne concaténation des chaînes
log_message('debug', 'Filtres Commercial reçus - startDate: ' . $startDate . ', endDate: ' . $endDate . ', pvente: ' . $pvente);
$commercialId = $this->request->getGet('commercial');
// Pour Direction et Conseil : afficher TOUTES les performances AVEC FILTRES
if ($users['group_name'] === "DAF" || $users['group_name'] === "Direction" || $users['group_name'] === "SuperAdmin") {
// ✅ PASSER LES FILTRES AU MODÈLE - UNIQUEMENT POUR L'ADMIN
$orderPaid = $Orders->getPerformanceByOrders($startDate, $endDate, $pvente);
$orderPaid = $Orders->getPerformanceByOrders($startDate, $endDate, $pvente, $commercialId);
foreach ($orderPaid as $key => $value) {
// Déterminer le prix de vente réel
$prix_vente_reel = (!empty($value['discount']) && $value['discount'] > 0)
? $value['discount']
$prix_vente_reel = (!empty($value['discount']) && $value['discount'] > 0)
? $value['discount']
: $value['prix_vente'];
// Calculer le bénéfice
$benefice = $prix_vente_reel - $value['price'];
$result['data'][$key] = [
$value['firstname'] . ' ' . $value['lastname'],
$value['email'],
($value['sku'] == "" ? $value['motoname'] : $value['sku']),
(new DateTime($value['datevente']))->format('Y-m-d'),
(new \DateTime($value['datevente']))->format('Y-m-d'),
number_format($value['price'], 0, '.', ' '),
number_format($prix_vente_reel, 0, '.', ' '),
$this->returnName($value['store_id']),

View File

@ -274,15 +274,15 @@ class Orders extends Model
$orderItemModel->where('order_id', $id)->delete();
// Insert new items
$count_product = count($data['product']);
$count_product = is_array($data['product']) ? count($data['product']) : 0;
for ($x = 0; $x < $count_product; $x++) {
$items = [
'order_id' => $id,
'product_id' => $data['product'][$x],
'rate' => $data['rate_value'][$x],
'qty' => isset($data['qty'][$x]) ? (int)$data['qty'][$x] : 1,
'puissance' => $data['puissance'][$x],
'amount' => $data['amount_value'][$x],
'product_id' => is_array($data['product']) ? $data['product'][$x] : $data['product'],
'rate' => is_array($data['rate_value']) ? ($data['rate_value'][$x] ?? 0) : ($data['rate_value'] ?? 0),
'qty' => is_array($data['qty']) ? (int)($data['qty'][$x] ?? 1) : 1,
'puissance' => is_array($data['puissance']) ? ($data['puissance'][$x] ?? '') : ($data['puissance'] ?? ''),
'amount' => is_array($data['amount_value']) ? ($data['amount_value'][$x] ?? 0) : ($data['amount_value'] ?? 0),
];
$orderItemModel->insert($items);
@ -605,7 +605,7 @@ class Orders extends Model
return $order;
}
public function getPerformanceByOrders($startDate = null, $endDate = null, $pvente = null)
public function getPerformanceByOrders($startDate = null, $endDate = null, $pvente = null, $commercialId = null)
{
$builder = $this->db->table('orders')
->select('orders.id as orderid, orders.net_amount, orders.date_time as datevente, orders.discount, products.price, products.sku, products.prix_vente, products.name as motoname, stores.id as store_id, users.firstname, users.lastname, users.email')
@ -614,8 +614,7 @@ class Orders extends Model
->join('products', 'products.id = orders_item.product_id')
->join('users', 'users.id = orders.user_id')
->whereIn('orders.paid_status', [1, 3]);
// ✅ AJOUT : FILTRES PAR DATE
if (!empty($startDate) && !empty($endDate)) {
$builder->where('DATE(orders.date_time) >=', $startDate);
$builder->where('DATE(orders.date_time) <=', $endDate);
@ -624,14 +623,17 @@ class Orders extends Model
} elseif (!empty($endDate)) {
$builder->where('DATE(orders.date_time) <=', $endDate);
}
// ✅ AJOUT : FILTRE PAR POINT DE VENTE
if (!empty($pvente) && $pvente !== 'TOUS') {
$builder->where('stores.name', $pvente);
}
if (!empty($commercialId) && $commercialId !== 'TOUS') {
$builder->where('orders.user_id', $commercialId);
}
$builder->orderBy('orders.date_time', 'DESC');
return $builder->get()->getResultArray();
}

View File

@ -62,6 +62,23 @@ class Products extends Model
->findAll();
}
public function getStockByBrand(int $id = 0)
{
$builder = $this->db->table('products')
->select('brands.name as brand_name, products.store_id, COUNT(*) as total_product')
->join('brands', 'brands.id = products.marque')
->where('products.is_piece', 0)
->where('products.product_sold', 0);
if ($id > 0) {
$builder->where('products.store_id', $id);
}
return $builder->groupBy('brands.name, products.store_id')
->orderBy('total_product', 'DESC')
->get()->getResultArray();
}
public function getProductData3(int $id)
{
if ($id == 0) {

View File

@ -42,12 +42,16 @@ $isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Ca
<div class="col-md-3">
<button id="avance_no_order" class="btn btn-info w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-hourglass-half me-2"></i> Avances Incomplètes
<i class="fa fa-hourglass-half me-2"></i>
<span class="badge badge-light" id="incomplete-count">0</span>
Avances Incomplètes
</button>
</div>
<div class="col-md-3">
<button id="avance_order" class="btn btn-success w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-check-circle me-2"></i> Avances Complètes
<i class="fa fa-check-circle me-2"></i>
<span class="badge badge-light" id="complete-count">0</span>
Avances Complètes
</button>
</div>
<div class="col-md-3">
@ -82,6 +86,7 @@ $isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Ca
<th>Téléphone</th>
<th>Adresse</th>
<th>Produit</th>
<th> Série</th>
<th>Prix</th>
<th>Avance</th>
<th>Reste à payer</th>
@ -91,11 +96,12 @@ $isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Ca
<?php endif;?>
</tr>
<?php endif;?>
<?php if ($isCommerciale || $isCaissier): ?>
<tr>
<th>#</th>
<th>Produit</th>
<th> Série</th>
<th>Avance</th>
<th>Reste à payer</th>
<th>Date</th>
@ -500,6 +506,38 @@ $(document).ready(function() {
}
}
// Compteurs avances
function loadAvanceCounts() {
$.ajax({
url: base_url + 'avances/fetchAvanceData',
type: 'GET',
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#incomplete-count').text(count);
if (count > 0) {
$('#incomplete-count').removeClass('badge-light').addClass('badge-danger');
} else {
$('#incomplete-count').removeClass('badge-danger').addClass('badge-light');
}
}
});
$.ajax({
url: base_url + 'avances/fetchAvanceBecameOrder',
type: 'GET',
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#complete-count').text(count);
if (count > 0) {
$('#complete-count').removeClass('badge-light').addClass('badge-success');
} else {
$('#complete-count').removeClass('badge-success').addClass('badge-light');
}
}
});
}
// Variables de rôles
var isCaissier = <?php echo json_encode($isCaissier ?? false); ?>;
var isCommerciale = <?php echo json_encode($isCommerciale ?? false); ?>;
@ -516,19 +554,20 @@ $(document).ready(function() {
{ title: "Téléphone" },
{ title: "Adresse" },
{ title: "Produit" },
{
{ title: "N° Série" },
{
title: "Prix",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{
{
title: "Avance",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{
{
title: "Reste à payer",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
@ -542,6 +581,10 @@ $(document).ready(function() {
var manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns);
// Charger les compteurs
loadAvanceCounts();
setInterval(loadAvanceCounts, 30000);
$('#avance_no_order').on('click', function() {
$('#table-title').text('Avances Incomplètes');
manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns);
@ -552,6 +595,7 @@ $(document).ready(function() {
{ title: "Téléphone" },
{ title: "Adresse" },
{ title: "Produit" },
{ title: "N° Série" },
{
title: "Prix",
render: function(data, type, row) {
@ -587,7 +631,8 @@ $(document).ready(function() {
var userColumns = [
{ title: "#" },
{ title: "Produit" },
{
{ title: "N° Série" },
{
title: "Avance",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
@ -617,12 +662,13 @@ $(document).ready(function() {
$('#avanceTable').DataTable().destroy();
}
// Reconstruire les headers (format caissière/commercial)
// Reconstruire les headers (format caissière/commercial)
rebuildTableHeaders([
'#',
'Produit',
'Avance',
'Reste à payer',
'#',
'Produit',
'N° Série',
'Avance',
'Reste à payer',
'Date',
'Action'
]);
@ -653,6 +699,7 @@ $('#avance_order').on('click', function() {
rebuildTableHeaders([
'#',
'Produit',
'N° Série',
'Avance',
'Statut',
'Date',
@ -662,6 +709,7 @@ $('#avance_order').on('click', function() {
var completedUserColumns = [
{ title: "#" },
{ title: "Produit" },
{ title: "N° Série" },
{ title: "Avance" },
{ title: "Statut" },
{ title: "Date" }
@ -693,14 +741,15 @@ $('#avance_expired').on('click', function() {
}
rebuildTableHeaders([
'#',
'Produit',
'Avance',
'Reste à payer',
'#',
'Produit',
'N° Série',
'Avance',
'Reste à payer',
'Date',
'Action'
]);
manageTable = $('#avanceTable').DataTable({
ajax: {
url: base_url + 'avances/fetchExpiredAvance',
@ -714,8 +763,10 @@ $('#avance_expired').on('click', function() {
console.log('✅ DataTable Expirées initialisée');
});
// Charger le compteur d'avances en attente
// Charger les compteurs d'avances
loadPendingCount();
loadAvanceCounts();
setInterval(loadAvanceCounts, 30000);
setInterval(loadPendingCount, 30000);
console.log('✅ Module caissière initialisé');
@ -1582,18 +1633,17 @@ window.validateAvanceFunc = function(avance_id) {
};
// =====================================================
// 🔥 CHARGER COMPTEUR AVANCES EN ATTENTE
// CHARGER COMPTEURS AVANCES
// =====================================================
function loadPendingCount() {
$.ajax({
url: base_url + 'avances/fetchPendingValidation',
type: 'POST', // ✅ CORRECTION
type: 'POST',
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#pending-count').text(count);
if (count > 0) {
$('#pending-count').removeClass('badge-light').addClass('badge-danger');
$('#avance_pending').addClass('btn-pulse');
@ -1601,13 +1651,12 @@ function loadPendingCount() {
$('#pending-count').removeClass('badge-danger').addClass('badge-light');
$('#avance_pending').removeClass('btn-pulse');
}
},
error: function() {
console.error('Erreur chargement compteur avances en attente');
}
});
}
// loadAvanceCounts defini plus haut
// =====================================================
// 🔥 TRAITER AVANCES EXPIRÉES (ADMIN)
// =====================================================

View File

@ -27,6 +27,7 @@
<th>Prix</th>
<th>Puissances</th>
<th> Moteur</th>
<th>Disponibilité</th>
<th>Action</th>
</tr>
</thead>
@ -72,10 +73,15 @@ $.extend(true, $.fn.dataTable.defaults, {
'ajax': `<?= base_url('ventes/fetchProductVente') ?>/${id}`,
'order': [],
'columnDefs': [{
targets: 6,
targets: 7,
className: 'text-right'
} // Column index 3 corresponds to "Prix"
]
}
],
'createdRow': function(row, data, dataIndex) {
if (data[6] && data[6].indexOf('En attente de livraison') !== -1) {
$(row).css({'background-color': '#fff3cd', 'opacity': '0.85'});
}
}
});
})
</script>

View File

@ -75,6 +75,7 @@
<th>Produit</th>
<th>SKU</th>
<th>Magasin</th>
<th>Utilisateur</th>
<th>Action</th>
<th>Description</th>
</tr>
@ -238,13 +239,14 @@ $(document).ready(function() {
"complete": function() { $("#loading").hide(); }
},
"columns": [
{ "data": 0, "width": "15%" },
{ "data": 1, "width": "20%" },
{ "data": 0, "width": "13%" },
{ "data": 1, "width": "15%" },
{ "data": 2, "width": "10%" },
{ "data": 3, "width": "12%" },
{
"data": 4,
"width": "10%",
{ "data": 4, "width": "10%" },
{
"data": 5,
"width": "8%",
"render": function(data) {
let badgeClass = "badge bg-gray";
if (data === "CREATE") badgeClass = "badge bg-green";
@ -254,7 +256,7 @@ $(document).ready(function() {
return '<span class="'+badgeClass+'">'+data+'</span>';
}
},
{ "data": 5, "width": "33%" }
{ "data": 6, "width": "32%" }
],
"order": [[0, "desc"]],
"pageLength": 25,

View File

@ -43,7 +43,7 @@
<h3 class="box-title">Mise à jours Moto</h3>
</div>
<form role="form" action="<?php base_url('users/update') ?>" method="post" enctype="multipart/form-data">
<form role="form" action="<?php echo base_url('products/update/' . $product_data['id']); ?>" method="post" enctype="multipart/form-data">
<div class="box-body">
<!-- Image actuelle -->
<div class="form-group">
@ -111,7 +111,7 @@
<!-- Date d'arrivage -->
<div class="form-group">
<label for="datea">Date d'arrivage</label>
<input type="date" class="form-control" id="datea" name="datea" autocomplete="off" value="<?php echo $product_data['date_arivage']; ?>" />
<input type="date" class="form-control" id="datea" name="datea" autocomplete="off" value="<?php echo !empty($product_data['date_arivage']) ? date('Y-m-d', strtotime($product_data['date_arivage'])) : ''; ?>" />
</div>
<!-- Puissance -->
@ -143,19 +143,23 @@
<label for="categorie">Catégories</label>
<?php
$rawCats = $product_data['categorie_id'] ?? null;
$catIds = [];
if (is_array($rawCats)) {
$catIds = array_map('intval', $rawCats);
} elseif (is_string($rawCats)) {
$catIds = array_filter(array_map('intval', explode(',', $rawCats)), fn($id) => $id > 0);
$decoded = json_decode($rawCats, true);
if (is_array($decoded)) {
$catIds = array_map('intval', $decoded);
} else {
$catIds = array_filter(array_map('intval', explode(',', $rawCats)), fn($id) => $id > 0);
}
} elseif (is_int($rawCats) || ctype_digit((string)$rawCats)) {
$catIds = [(int) $rawCats];
} else {
$catIds = [];
}
?>
<select class="form-control select_group" id="categorie" name="categorie[]" multiple="multiple">
<?php foreach ($categorie as $k => $v): ?>
<option value="<?= $v['id']; ?>" <?= in_array($v['id'], $catIds, true) ? 'selected="selected"' : '' ?>><?= esc($v['name']); ?></option>
<option value="<?= $v['id']; ?>" <?= in_array((int)$v['id'], $catIds) ? 'selected="selected"' : '' ?>><?= esc($v['name']); ?></option>
<?php endforeach; ?>
</select>
</div>

View File

@ -155,7 +155,7 @@
<thead>
<tr>
<th>Image</th>
<th>UGS</th>
<th>SERIE </th>
<th>Nom de produit</th>
<th>Prix</th>
<th>Magasin</th>
@ -340,7 +340,12 @@
'columnDefs': [{
targets: 3,
className: 'text-right'
}]
}],
'createdRow': function(row, data, dataIndex) {
if (data[5] && data[5].indexOf('En attente de livraison') !== -1) {
$(row).css({'background-color': '#fff3cd', 'opacity': '0.85'});
}
}
});
// CORRECTION: Utilisation de la délégation d'événements pour les boutons

View File

@ -67,7 +67,9 @@
<td></td>
<td></td>
<td></td>
<td></td>
<?php if (in_array('updateRecouvrement', $user_permission) || in_array('deleteRecouvrement', $user_permission)): ?>
<td></td>
<?php endif; ?>
</tr>
</tbody>
</table>
@ -275,7 +277,7 @@
// Initialisation du DataTable
manageTable = $('#manageTable').DataTable({
'ajax': '<?= base_url('recouvrement/fetchRecouvrementData') ?>',
'order': [],
'order': [[2, 'desc']],
'columnDefs': [{
targets: 1,
className: 'text-right rowmontant'
@ -306,18 +308,36 @@
})
// ====== CRÉATION DE RECOUVREMENT ======
$("#create_form").unbind('submit').on('submit', function() {
$("#create_form").unbind('submit').on('submit', function(e) {
e.preventDefault();
var form = $(this);
var submitBtn = form.find('button[type="submit"]');
// Supprimer les messages d'erreur
$(".text-danger").remove();
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
dataType: 'json',
success: function(response) {
// Pop-up de confirmation
Swal.fire({
title: 'Confirmer la création',
text: 'Voulez-vous vraiment enregistrer ce recouvrement ?',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Oui, enregistrer',
cancelButtonText: 'Annuler'
}).then((result) => {
if (!result.isConfirmed) return;
// Désactiver le bouton pour éviter les doublons
submitBtn.prop('disabled', true).text('Enregistrement...');
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(),
dataType: 'json',
success: function(response) {
if (response.success === true) {
// Recharger immédiatement les données
manageTable.ajax.reload(null, false);
@ -374,9 +394,15 @@ $("#create_form").unbind('submit').on('submit', function() {
'<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
'<strong>Erreur!</strong> Une erreur est survenue lors de la création.' +
'</div>');
},
complete: function() {
// Réactiver le bouton
submitBtn.prop('disabled', false).text('Enregistrer');
}
});
}); // fin Swal.then
return false;
});
// ====== SUPPRESSION DE RECOUVREMENT ======

View File

@ -25,7 +25,8 @@
</select>
</div>
<button type="submit" class="btn btn-default">Envoyer</button>
<a href="detail/stock" class="btn btn-sm btn-success">Détails</a>
<a href="<?= base_url('reports/detail/stock') ?>" class="btn btn-sm btn-success">Details Stock</a>
<a href="<?= base_url('reports/detail/performance') ?>" class="btn btn-sm btn-primary">Performances</a>
</form>
</div>

View File

@ -73,27 +73,28 @@
<label for="endDate" class="form-label">Date de fin</label>
<input type="date" id="endDate" class="form-control">
</div>
<div class="col-md-3">
<div class="col-md-2">
<label for="pvente" class="form-label">Points de ventes</label>
<select name="" id="pvente" class="form-control">
<select id="pvente" class="form-control">
<option value="TOUS">TOUS</option>
<?php
foreach ($stores as $value) {
?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?>
</option>
<?php
}
?>
<?php foreach ($stores as $value): ?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="col-md-2">
<label for="commercialFilter" class="form-label">Commercial</label>
<select id="commercialFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($commerciaux as $com): ?>
<option value="<?= $com['id']; ?>"><?= $com['firstname'] . ' ' . $com['lastname']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<br>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer
🔍</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter
📤</button>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter</button>
</div>
</div>
<table id="commperformance" class="table table-hover table-striped">
@ -102,7 +103,7 @@
<th>Nom et prénom</th>
<th>Email</th>
<th>Motos vendue</th>
<th>Date de vente</th><!-- return 2025-04-18 -->
<th>Date de vente</th>
<th>Prix d'achat</th>
<th>Prix de vente</th>
<th>Point de ventes</th>
@ -112,9 +113,9 @@
<tfoot>
<tr>
<th colspan="5" style="text-align:right">Total:</th>
<th></th> <!-- total Prix de vente -->
<th></th>
<th></th> <!-- total Bénéfices -->
<th></th>
<th></th>
</tr>
</tfoot>
</table>
@ -135,48 +136,48 @@
</div>
<div class="card-body">
<div class="row g-3 align-items-center mb-4" style="margin: 5px 0 5px 5px;">
<div class="col-md-3">
<div class="col-md-2">
<label for="startDate2" class="form-label">Date de début</label>
<input type="date" id="startDate" class="form-control">
<input type="date" id="startDate2" class="form-control">
</div>
<div class="col-md-3">
<div class="col-md-2">
<label for="endDate2" class="form-label">Date de fin</label>
<input type="date" id="endDate" class="form-control">
<input type="date" id="endDate2" class="form-control">
</div>
<div class="col-md-3">
<label for="pvente" class="form-label">Points de ventes</label>
<select name="" id="pvente2" class="form-control">
<div class="col-md-2">
<label for="pvente2" class="form-label">Points de ventes</label>
<select id="pvente2" class="form-control">
<option value="TOUS">TOUS</option>
<?php
foreach ($stores as $value) {
?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?>
</option>
<?php
}
?>
<?php foreach ($stores as $value): ?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="col-md-2">
<label for="mecanicienFilter" class="form-label">Mécanicien</label>
<select id="mecanicienFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($mecaniciens as $mec): ?>
<option value="<?= $mec['id']; ?>"><?= $mec['firstname'] . ' ' . $mec['lastname']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<br>
<button id="filteredB2" class="btn btn-primary w-100">Filtrer
🔍</button>
<button id="ExportBTN2" class="btn btn-success w-100">Exporter
📤</button>
<button id="filteredB2" class="btn btn-primary w-100">Filtrer</button>
<button id="ExportBTN2" class="btn btn-success w-100">Exporter</button>
</div>
</div>
<table id="mecperformance" class="table table-hover table-striped">
<thead>
<tr>
<th>Nom et prénom</th>
<th>Email</th>
<th>Motos vendue</th>
<th>Date de vente</th><!-- return 2025-04-18 -->
<th>Prix d'achat</th>
<th>Prix de vente</th>
<th>Point de ventes</th>
<th>Bénefices</th>
<th>Mécanicien</th>
<th>Image</th>
<th>Moto réparée</th>
<th> Série</th>
<th>Point de vente</th>
<th>Date début</th>
<th>Date fin</th>
</tr>
</thead>
</table>
@ -200,8 +201,10 @@
// Initialize the datatable
var baseUrl = '<?= base_url() ?>';
manageTable = $('#commperformance').DataTable({
'ajax': 'fetchPerformances',
'ajax': baseUrl + 'reports/detail/fetchPerformances',
'order': [],
'pageLength': 5,
'lengthMenu': [
@ -241,49 +244,44 @@
}
});
// Filtre commercial - côté serveur
$('#filteredB1').on('click', function () {
const startDate = $('#startDate').val();
const endDate = $('#endDate').val();
const pvente = $('#pvente').val();
var startDate = $('#startDate').val();
var endDate = $('#endDate').val();
var pvente = $('#pvente').val();
var commercial = $('#commercialFilter').val();
// Get all original data (you may need to fetch from server or already loaded)
manageTable.ajax.url('fetchPerformances').load(function () {
const filteredData = [];
var url = baseUrl + 'reports/detail/fetchPerformances?startDate=' + startDate + '&endDate=' + endDate + '&pvente=' + pvente + '&commercial=' + commercial;
manageTable.ajax.url(url).load();
});
manageTable.rows().every(function () {
const data = this.data();
const saleDate = data[3].split(' ')[0]; // extract YYYY-MM-DD from date
const store = data[6];
// DataTable mécanicien
var mecTable = $('#mecperformance').DataTable({
'ajax': baseUrl + 'mecanicien/fetchMecanicienPerformances',
'order': [],
'pageLength': 10
});
// Filter logic
const dateMatch = (!startDate && !endDate) ||
(startDate && endDate && saleDate >= startDate && saleDate <= endDate) ||
(startDate && !endDate && saleDate >= startDate) ||
(!startDate && endDate && saleDate <= endDate);
// Filtre mécanicien - côté serveur
$('#filteredB2').on('click', function () {
var startDate = $('#startDate2').val();
var endDate = $('#endDate2').val();
var mecanic = $('#mecanicienFilter').val();
const storeMatch = (pvente === 'TOUS' || pvente === store);
if (dateMatch && storeMatch) {
filteredData.push(data);
}
});
// Clear and reload table with filtered data
manageTable.clear().rows.add(filteredData).draw();
});
var url = baseUrl + 'mecanicien/fetchMecanicienPerformances?startDate=' + startDate + '&endDate=' + endDate + '&mecanic_id=' + (mecanic === 'TOUS' ? '' : mecanic);
mecTable.ajax.url(url).load();
});
});
// Export commercial
document.getElementById('ExportBTN1').addEventListener('click', function () {
// Select your table
var table = document.getElementById('commperformance');
// Convert it to a workbook
var wb = XLSX.utils.table_to_book(table, {
sheet: "Feuille1"
});
// Export it
var wb = XLSX.utils.table_to_book(document.getElementById('commperformance'), { sheet: "Commercial" });
XLSX.writeFile(wb, 'export-commercial-performance.xlsx');
});
// Export mécanicien
document.getElementById('ExportBTN2').addEventListener('click', function () {
var wb = XLSX.utils.table_to_book(document.getElementById('mecperformance'), { sheet: "Mecanicien" });
XLSX.writeFile(wb, 'export-mecanicien-performance.xlsx');
});
</script>

View File

@ -57,9 +57,17 @@
</div>
<div class="card-body">
<!-- Filter Bar -->
<div class="row g-3">
<div class="row g-3 align-items-center mb-3" style="margin: 5px 0;">
<div class="col-md-2">
<label for="startDateStock" class="form-label fw-bold">Date de début</label>
<input type="date" id="startDateStock" class="form-control">
</div>
<div class="col-md-2">
<label for="endDateStock" class="form-label fw-bold">Date de fin</label>
<input type="date" id="endDateStock" class="form-control">
</div>
<div class="col-md-3">
<label for="storeFilter" class="form-label fw-bold">🏪 Points de ventes</label>
<label for="storeFilter" class="form-label fw-bold">Points de ventes</label>
<select id="storeFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($stores as $value) { ?>
@ -67,9 +75,9 @@
<?php } ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="col-md-2 d-flex align-items-end">
<br>
<button id="filterBtn" class="btn btn-primary w-100">🔍 Filtrer</button>
<button id="filterBtn" class="btn btn-primary w-100">Filtrer</button>
</div>
</div>
@ -188,7 +196,7 @@
manageTable = $('#venteTable').DataTable({
'ajax': 'fetctData/' + 0,
'ajax': 'fetchData/' + 0,
'order': [],
'pageLength': 5, // Set default rows per page
'lengthMenu': [
@ -198,7 +206,7 @@
});
manageTable2 = $('#stockTable').DataTable({
'ajax': 'fetctDataStock/' + 0,
'ajax': 'fetchDataStock/' + 0,
'order': [],
'pageLength': 5, // Set default rows per page
'lengthMenu': [
@ -209,7 +217,7 @@
manageTable3 = $('#export1').DataTable({
ajax: {
url: 'fetctDataStock2/' + 0,
url: 'fetchDataStock2/' + 0,
dataSrc: 'data'
},
order: [],
@ -226,8 +234,8 @@
let filterValue = filterBtn.value === "TOUS" ? "0" : filterBtn.value;
// Update the DataTable dynamically without reinitialization
manageTable.ajax.url('fetctData/' + filterValue).load();
manageTable2.ajax.url('fetctDataStock/' + filterValue).load();
manageTable.ajax.url('fetchData/' + filterValue).load();
manageTable2.ajax.url('fetchDataStock/' + filterValue).load();
});
let productsSold = <?= $ventes ?>;
@ -249,6 +257,14 @@
// Trigger the filter on button click
$('#filterBtn').click(function () {
let storeId = $('#storeFilter').val();
let filterValue = storeId === "TOUS" ? "0" : storeId;
let startDate = $('#startDateStock').val();
let endDate = $('#endDateStock').val();
let params = '?startDate=' + startDate + '&endDate=' + endDate;
manageTable.ajax.url('fetchData/' + filterValue + params).load();
manageTable2.ajax.url('fetchDataStock/' + filterValue + params).load();
manageTable3.ajax.url('fetchDataStock2/' + filterValue + params).load();
applyFilter(storeId);
});

View File

@ -200,7 +200,7 @@
<thead>
<tr>
<th>Image</th>
<th>UGS</th>
<th>SERIE </th>
<th>Désignation</th>
<th>Statut</th>
<th>Action</th>
@ -272,7 +272,7 @@
<th>Image</th>
<th> Facture</th>
<th>Désignation</th>
<th>UGS</th>
<th>SERIE </th>
<th>Marque</th>
<th>Client</th>
<th>Magasin</th>

View File

@ -278,6 +278,7 @@ body {
transition: width 0.3s ease;
box-shadow: 2px 0 20px rgba(0, 0, 0, 0.3);
padding-top: 0 !important;
padding-bottom: 80px;
}
.main-sidebar .logo {