From 67b33ad2e612741064bf4f248b52d42fd6118924 Mon Sep 17 00:00:00 2001 From: Sarobidy22 Date: Mon, 27 Oct 2025 07:49:22 +0300 Subject: [PATCH] 27102025 --- app/Config/Routes.php | 47 +- app/Controllers/AdminController.php | 7 +- app/Controllers/AvanceController.php | 1688 ++++++++++++++++++-- app/Controllers/Dashboard.php | 112 +- app/Controllers/NotificationController.php | 21 + app/Controllers/OrderController.php | 771 ++++++--- app/Controllers/ProductCOntroller.php | 12 +- app/Helpers/alerts_helper.php | 220 ++- app/Models/Avance.php | 448 +++++- app/Models/Products.php | 130 +- app/Views/avances/avance.php | 1117 ++++++++++--- app/Views/dashboard.php | 431 +++-- app/Views/login.php | 255 ++- app/Views/orders/createbyid.php | 272 +++- app/Views/orders/edit.php | 586 ++++--- app/Views/products/create.php | 2 + app/Views/products/index.php | 7 +- app/Views/templates/header.php | 1 + app/Views/templates/header_menu.php | 42 +- 19 files changed, 4946 insertions(+), 1223 deletions(-) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 86209a30..7126fd64 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -56,6 +56,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) { $routes->get('/ventes/show/(:num)', [Auth::class, 'getSingle']); $routes->post('/ventes/moreimage/(:num)', [Auth::class, 'uploadImagePub']); $routes->post('/ventes/moreimage/supp/(:num)', [Auth::class, 'delete']); + /** * route to logout @@ -176,6 +177,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) { // $routes->get('generateqrcode/(:num)', [QrCodeController::class, 'generate']); $routes->post('assign_store', [ProductCOntroller::class, 'assign_store']); $routes->post('createByExcel', [ProductCOntroller::class, 'createByExcel']); + }); /** @@ -261,6 +263,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) { $routes->group('/notifications', function ($routes) { $routes->get('/', [NotificationController::class, 'getNotification']); $routes->post('markAsRead/(:num)', [NotificationController::class, 'markAsRead']); + $routes->post('markAllAsRead', [NotificationController::class, 'markAllAsRead']); }); // routes for sortie caisse $routes->group('/sortieCaisse', function ($routes) { @@ -288,19 +291,37 @@ $routes->group('/sortieCaisse', function ($routes) { $routes->post('updateRemise/(:num)', [RemiseController::class, 'updateRemise']); }); - // avance - $routes->group('/avances', function ($routes) { - $routes->get('/', [AvanceController::class, 'index']); - $routes->get('fetchAvanceData', [AvanceController::class, 'fetchAvanceData']); - $routes->get('fetchCompletedAvances', [AvanceController::class, 'fetchCompletedAvances']); - $routes->get('fetchIncompleteAvances', [AvanceController::class, 'fetchIncompleteAvances']); - $routes->get('fetchAvanceBecameOrder', 'AvanceController::fetchAvanceBecameOrder'); - $routes->get('fetchExpiredAvance', [AvanceController::class, 'fetcheExpiredAvance']); - $routes->get('fetchSingleAvance/(:num)', [AvanceController::class, 'fetchSingleAvance']); - $routes->post('createAvance', [AvanceController::class, 'createAvance']); - $routes->post('deleteAvance', [AvanceController::class, 'removeAvance']); - $routes->post('updateAvance/(:num)', [AvanceController::class, 'updateAvance']); - }); +// avance +$routes->group('/avances', function ($routes) { + $routes->get('/', [AvanceController::class, 'index']); + + // ✅ Routes pour récupérer les données (GET) + $routes->get('fetchAvanceData', [AvanceController::class, 'fetchAvanceData']); + $routes->get('fetchAvanceBecameOrder', [AvanceController::class, 'fetchAvanceBecameOrder']); + $routes->get('fetchExpiredAvance', [AvanceController::class, 'fetchExpiredAvance']); + + // Routes pour une avance spécifique + $routes->get('fetchSingleAvance/(:num)', [AvanceController::class, 'fetchSingleAvance/$1']); + $routes->get('getInvoicePreview/(:num)', [AvanceController::class, 'getInvoicePreview/$1']); + $routes->get('getFullInvoiceForPrint/(:num)', [AvanceController::class, 'getFullInvoiceForPrint/$1']); + $routes->get('printInvoice/(:num)', [AvanceController::class, 'printInvoice/$1']); + + // ✅ Routes POST pour modifications + $routes->post('createAvance', [AvanceController::class, 'createAvance']); + $routes->post('updateAvance', [AvanceController::class, 'updateAvance']); + $routes->post('deleteAvance', [AvanceController::class, 'removeAvance']); + $routes->post('notifyPrintInvoice', [AvanceController::class, 'notifyPrintInvoice']); + + // ✅ AJOUTER CETTE ROUTE MANQUANTE + $routes->post('processExpiredAvances', [AvanceController::class, 'processExpiredAvances']); + + // ✅ Route CRON (optionnel) + $routes->get('checkDeadlineAlerts', [AvanceController::class, 'checkDeadlineAlerts']); + + $routes->post('payAvance', 'AvanceController::payAvance'); + $routes->get('forceConvertToOrder/(:num)', 'AvanceController::forceConvertToOrder/$1'); + $routes->post('checkAndConvertCompleted', 'AvanceController::checkAndConvertCompleted'); +}); // historique $routes->group('historique', ['filter' => 'auth'], static function ($routes) { $routes->get('/', 'HistoriqueController::index'); diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php index ef5f6eca..897b7cb9 100644 --- a/app/Controllers/AdminController.php +++ b/app/Controllers/AdminController.php @@ -12,7 +12,6 @@ use CodeIgniter\Logger\LoggerInterface; abstract class AdminController extends BaseController { protected $permission = []; - public function __construct() { if (empty(session()->get('user'))) { @@ -20,15 +19,15 @@ abstract class AdminController extends BaseController } else { $userIfo = session()->get('user'); $userId = $userIfo['id']; - + $Groups = new Groups(); $group_data = $Groups->getUserGroupByUserId($userId); - + $this->permission = unserialize($group_data['permission']); + } } - /** * finction to verify role of users * @return mixed diff --git a/app/Controllers/AvanceController.php b/app/Controllers/AvanceController.php index 69bb6394..ed911e2b 100644 --- a/app/Controllers/AvanceController.php +++ b/app/Controllers/AvanceController.php @@ -41,114 +41,127 @@ class AvanceController extends AdminController private function isCaissier($user) { - return in_array($user['group_name'], ['Caissier']); + return in_array($user['group_name'], ['Caissière']); } - - private function buildActionButtons($value, $isAdmin, $isOwner) - { - $buttons = ''; - - if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner)) { - $buttons .= ' '; - } - - if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner)) { - $buttons .= ' '; - } - - if (in_array('viewAvance', $this->permission) && !$isAdmin) { - $buttons .= '' - . ''; - } - - - return $buttons; +/** + * Modifier la méthode buildActionButtons pour ajouter l'icône œil pour la Direction + */ +private function buildActionButtons($value, $isAdmin, $isOwner, $isCaissier = false) +{ + $session = session(); + $users = $session->get('user'); + $isDirection = in_array($users['group_name'], ['Direction', 'Conseil']); + + $buttons = ''; + + // ✅ Bouton Voir pour Caissière (toujours visible) + if ($isCaissier && in_array('viewAvance', $this->permission)) { + $buttons .= ' '; + } + + // ✅ Bouton Voir pour Direction (toujours visible, sans impression) + if ($isDirection && in_array('viewAvance', $this->permission) && !$isCaissier) { + $buttons .= ' '; } + + // ✅ MODIFIÉ : Bouton Modifier - Le caissier peut maintenant modifier toutes les avances + if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) { + $buttons .= ' '; + } + + // ✅ MODIFIÉ : Bouton Supprimer - Le caissier peut maintenant supprimer toutes les avances + if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) { + $buttons .= ' '; + } + + return $buttons; +} - private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons) - { - $date_time = date('d-m-Y h:i a', strtotime($value['avance_date'])); +private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons) +{ + $date_time = date('d-m-Y h:i a', strtotime($value['avance_date'])); - // ✅ Afficher product_name si disponible, sinon récupérer depuis la BDD + // ✅ Afficher product_name si disponible, sinon récupérer depuis la BDD $productName = !empty($value['product_name']) - ? $value['product_name'] - : $product->getProductNameById($value['product_id']); - - if ($isAdmin) { - return [ - $value['customer_name'], - $value['customer_phone'], - $value['customer_address'], - $productName, // ✅ Utiliser la variable - number_format((int)$value['gross_amount'], 0, ',', ' '), - number_format((int)$value['avance_amount'], 0, ',', ' '), - number_format((int)$value['amount_due'], 0, ',', ' '), - $date_time, - $buttons, - ]; - } elseif ($isCommerciale || $isCaissier) { - return [ - $value['avance_id'], - $productName, // ✅ Utiliser la variable - number_format((int)$value['avance_amount'], 0, ',', ' '), - number_format((int)$value['amount_due'], 0, ',', ' '), - $date_time, - $buttons, - ]; - } - - return []; + ? $value['product_name'] + : $product->getProductNameById($value['product_id']); + + if ($isAdmin) { + return [ + $value['customer_name'], + $value['customer_phone'], + $value['customer_address'], + $productName, + number_format((int)$value['gross_amount'], 0, ',', ' '), + number_format((int)$value['avance_amount'], 0, ',', ' '), + number_format((int)$value['amount_due'], 0, ',', ' '), + $date_time, + $buttons, + ]; + } elseif ($isCommerciale || $isCaissier) { + return [ + $value['avance_id'], + $productName, + number_format((int)$value['avance_amount'], 0, ',', ' '), + number_format((int)$value['amount_due'], 0, ',', ' '), + $date_time, + $buttons, + ]; } + + return []; +} - private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData') - { - helper(['url', 'form']); - $Avance = new Avance(); - $product = new Products(); - $result = ['data' => []]; +private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData') +{ + helper(['url', 'form']); + $Avance = new Avance(); + $product = new Products(); + $result = ['data' => []]; + + $data = $Avance->$methodName(); + $session = session(); + $users = $session->get('user'); + + $isAdmin = $this->isAdmin($users); + $isCommerciale = $this->isCommerciale($users); + $isCaissier = $this->isCaissier($users); + + foreach ($data as $key => $value) { + $isOwner = $users['id'] === $value['user_id']; - $data = $Avance->$methodName(); - $session = session(); - $users = $session->get('user'); + // ✅ MODIFIÉ : Passer $isCaissier aux boutons d'action + $buttons = $this->buildActionButtons($value, $isAdmin, $isOwner, $isCaissier); - $isAdmin = $this->isAdmin($users); - $isCommerciale = $this->isCommerciale($users); - $isCaissier = $this->isCaissier($users); - - foreach ($data as $key => $value) { - $isOwner = $users['id'] === $value['user_id']; - - $buttons = $this->buildActionButtons($value, $isAdmin, $isOwner); - - $row = $this->buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons); - - if (!empty($row)) { - $result['data'][] = $row; - } + $row = $this->buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons); + + if (!empty($row)) { + $result['data'][] = $row; } - - return $this->response->setJSON($result); } - public function fetchAvanceData() - { - // Avances incomplètes (reste à payer > 0) - return $this->fetchAvanceDataGeneric('getIncompleteAvances'); - } - - public function fetchAvanceBecameOrder() - { - // Avances complètes (reste à payer = 0) - return $this->fetchAvanceDataGeneric('getCompletedAvances'); - } - + return $this->response->setJSON($result); +} + +public function fetchAvanceData() +{ + return $this->fetchAvanceDataGeneric('getIncompleteAvances'); +} + +public function fetchAvanceBecameOrder() +{ + return $this->fetchAvanceDataGeneric('getCompletedAvances'); +} + +public function fetchExpiredAvance() +{ + return $this->fetchAvanceDataGeneric('getAllAvanceData2'); +} - public function fetcheExpiredAvance() - { - return $this->fetchAvanceDataGeneric('getAllAvanceData2'); - } /** * Méthode pour vérifier et envoyer des emails d'alerte 3 jours avant deadline @@ -441,7 +454,6 @@ class AvanceController extends AdminController // ✅ Ajouter le produit selon le type if ($type_avance === 'mere') { $data['product_name'] = $this->request->getPost('product_name_text'); - // $data['product_id'] = null; $data['commentaire'] = $this->request->getPost('commentaire'); } else { $data['product_id'] = (int)$this->request->getPost('id_product'); @@ -456,13 +468,24 @@ class AvanceController extends AdminController $Products->update($data['product_id'], ['product_sold' => 1]); } + // ✅ NOUVELLE FONCTIONNALITÉ : Envoyer notification au Conseil $Notification->createNotification( 'Une nouvelle avance a été créée', - "Conseil", + "DAF", (int)$users['store_id'], 'avances' ); + // ✅ NOUVELLE FONCTIONNALITÉ : Envoyer notification à la Caissière si l'utilisateur est COMMERCIALE + if ($this->isCommerciale($users)) { + $Notification->createNotification( + 'Une nouvelle avance a été créée par un commercial', + "Caissière", + (int)$users['store_id'], + 'avances' + ); + } + return $this->response->setJSON([ 'success' => true, 'messages' => 'Avance créée avec succès !', @@ -482,6 +505,7 @@ class AvanceController extends AdminController ]); } } + public function updateAvance() { @@ -502,10 +526,8 @@ class AvanceController extends AdminController $Products = new Products(); $Notification = new NotificationController(); - // ✅ Récupérer le type AVANT la validation $type_avance = $this->request->getPost('type_avance_edit'); - // ✅ Validation conditionnelle selon le type $validation = \Config\Services::validation(); $baseRules = [ @@ -537,7 +559,6 @@ class AvanceController extends AdminController $avance_id = $this->request->getPost('id'); - // Vérifier si l'avance existe $existingAvance = $Avance->fetchSingleAvance($avance_id); if (!$existingAvance) { return $this->response->setJSON([ @@ -546,18 +567,18 @@ class AvanceController extends AdminController ]); } - // Vérifier les permissions (admin ou propriétaire) $isAdmin = $this->isAdmin($users); $isOwner = $users['id'] === $existingAvance['user_id']; + $isCaissier = $this->isCaissier($users); - if (!$isAdmin && !$isOwner) { + // ✅ MODIFIÉ : Le caissier peut maintenant modifier toutes les avances + if (!$isAdmin && !$isOwner && !$isCaissier) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Vous n\'avez pas les droits pour modifier cette avance' ]); } - // Recalculer la deadline si le type d'avance a changé $current_deadline = $existingAvance['deadline']; if ($type_avance !== $existingAvance['type_avance']) { @@ -570,7 +591,6 @@ class AvanceController extends AdminController $old_product_id = $existingAvance['product_id']; - // Préparer les données communes $data = [ 'type_avance' => $type_avance, 'type_payment' => $this->request->getPost('type_payment_edit'), @@ -581,13 +601,12 @@ class AvanceController extends AdminController 'deadline' => $current_deadline, 'gross_amount' => (float)$this->request->getPost('gross_amount_edit'), 'avance_amount' => (float)$this->request->getPost('avance_amount_edit'), - 'amount_due' => (float)$this->request->getPost('amount_due_edit'), + 'amount_due' => (float)$this->request->getPost('amount_due_edit') ]; - - // ✅ Gérer le produit selon le type + if ($type_avance === 'mere') { $data['product_name'] = $this->request->getPost('product_name_text_edit'); - $data['product_id'] = null; // ⚠️ Sera NULL si la colonne l'accepte + $data['product_id'] = null; $data['commentaire'] = $this->request->getPost('commentaire_edit'); $new_product_id = null; } else { @@ -597,29 +616,25 @@ class AvanceController extends AdminController $data['commentaire'] = null; } - // Mettre à jour l'avance if ($Avance->updateAvance($avance_id, $data)) { - // ✅ Gérer le changement de produit UNIQUEMENT pour "terre" if ($type_avance === 'terre') { if ($old_product_id && $old_product_id !== $new_product_id) { - // Libérer l'ancien produit $Products->update($old_product_id, ['product_sold' => 0]); } if ($new_product_id) { - // Marquer le nouveau produit comme vendu $Products->update($new_product_id, ['product_sold' => 1]); } } else { - // Si on passe de "terre" à "mere", libérer l'ancien produit if ($old_product_id && $existingAvance['type_avance'] === 'terre') { $Products->update($old_product_id, ['product_sold' => 0]); } } + // ✅ Notification simplifiée $Notification->createNotification( 'Une avance a été modifiée', - "Conseil", + "Caissière", (int)$users['store_id'], 'avances' ); @@ -635,7 +650,7 @@ class AvanceController extends AdminController 'messages' => 'Erreur lors de la modification de l\'avance' ]); } - + } catch (\Exception $e) { log_message('error', "Erreur modification avance: " . $e->getMessage()); return $this->response->setJSON([ @@ -651,6 +666,9 @@ class AvanceController extends AdminController $this->verifyRole('deleteAvance'); try { + $session = session(); + $users = $session->get('user'); + $avance_id = $this->request->getPost('avance_id'); $product_id = $this->request->getPost('product_id'); @@ -663,6 +681,27 @@ class AvanceController extends AdminController $Avance = new Avance(); $Products = new Products(); + + // ✅ AJOUT : Vérifier les permissions pour la suppression + $existingAvance = $Avance->fetchSingleAvance($avance_id); + if (!$existingAvance) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Avance non trouvée' + ]); + } + + $isAdmin = $this->isAdmin($users); + $isOwner = $users['id'] === $existingAvance['user_id']; + $isCaissier = $this->isCaissier($users); + + // ✅ MODIFIÉ : Le caissier peut maintenant supprimer toutes les avances + if (!$isAdmin && !$isOwner && !$isCaissier) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Vous n\'avez pas les droits pour supprimer cette avance' + ]); + } if ($Avance->removeAvance($avance_id)) { $Products->update($product_id, ['product_sold' => 0]); @@ -715,6 +754,1425 @@ class AvanceController extends AdminController } } +/** + * Méthode pour l'impression directe (si vous l'utilisez encore) + */ +public function printInvoice($avance_id) +{ + $this->verifyRole('viewAvance'); + + try { + $session = session(); + $users = $session->get('user'); + + if (!$this->isCaissier($users)) { + return redirect()->back()->with('error', 'Seule la caissière peut imprimer les factures'); + } + + $Avance = new Avance(); + $Products = new Products(); + + $avance = $Avance->fetchSingleAvance($avance_id); + + if (!$avance) { + return redirect()->back()->with('error', 'Avance non trouvée'); + } + + if ($avance['store_id'] !== $users['store_id']) { + return redirect()->back()->with('error', 'Accès non autorisé'); + } + + // ✅ CORRECTION SIMPLIFIÉE + if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) { + $productName = $avance['product_name']; + $productDetails = [ + 'marque' => $avance['product_name'], + 'numero_moteur' => '', + 'puissance' => '' + ]; + } else { + $product = $Products->find($avance['product_id']); + + if (!$product) { + return redirect()->back()->with('error', 'Produit non trouvé'); + } + + $productName = $product['name'] ?? 'N/A'; + + // ✅ Récupérer le nom de la marque + $brandName = 'N/A'; + if (!empty($product['marque'])) { + $db = \Config\Database::connect(); + $brandQuery = $db->table('brands') + ->select('name') + ->where('id', $product['marque']) + ->get(); + $brandResult = $brandQuery->getRowArray(); + if ($brandResult) { + $brandName = $brandResult['name']; + } + } + + $productDetails = [ + 'marque' => $brandName, + 'numero_moteur' => $product['numero_de_moteur'] ?? '', + 'puissance' => $product['puissance'] ?? '' + ]; + } + + $html = $this->generatePrintableInvoiceHTML($avance, $productName, $productDetails); + + return $this->response->setBody($html); + + } catch (\Exception $e) { + log_message('error', "Erreur impression facture: " . $e->getMessage()); + return redirect()->back()->with('error', 'Erreur lors de l\'impression'); + } +} +// private function generateInvoiceHTML($avance, $productName, $productDetails) +// { +// $avanceDate = date('d/m/Y', strtotime($avance['avance_date'])); +// $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); +// $customerName = strtoupper(esc($avance['customer_name'])); +// $customerPhone = esc($avance['customer_phone']); +// $grossAmount = number_format($avance['gross_amount'], 0, ',', ' '); +// $avanceAmount = number_format($avance['avance_amount'], 0, ',', ' '); +// $amountDue = number_format($avance['amount_due'], 0, ',', ' '); +// $marque = esc($productDetails['marque']) ?: $productName; +// $numeroMoteur = esc($productDetails['numero_moteur']); +// $puissance = esc($productDetails['puissance']); + +// return << +// +// +// +// +// Facture Avance - KELY SCOOTERS +// +// +// +// + +//
+// +//
+//

KELY SCOOTERS

+//
+//
+//
NIF: 401 840 5554
+//
STAT: 46101 11 2024 00317
+//
+//
+//
Contact: +261 34 27 946 35 / +261 34 07 079 69
+//
Antsakaviro en face WWF
+//
+//
+//
+ +// +//
+//
+//

FACTURE

+//
Date: {$avanceDate}
+//
N°: {$avanceNumber}CI 2025
+//
+//
DOIT ORIGINAL
+//
+ +// +//
+//
NOM: {$customerName} ({$customerPhone})
+//
PC: {$grossAmount} Ar
+//
AVANCE: {$avanceAmount} Ar
+//
RAP: {$amountDue} Ar
+//
+ +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +// +//
MARQUEN°MOTEURPUISSANCERAP (Ariary)
{$marque}{$numeroMoteur}{$puissance}{$amountDue}
+ +// +//
+//

FIFANEKENA ARA-BAROTRA (Réservations)

+//

Ry mpanjifa hajaina,

+//

Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.

+ +//
+// Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA +//

Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).

+//
+ +//
+// Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE) +//

Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.

+//

Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.

+//
+ +//
+// Andininy faha-3: FAMERENANA VOLA +//

Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.

+//
+ +//
+// Andininy faha-4: FEPETRA FANAMPINY +//
    +//
  • Tsy misafidy raha toa ka mamafa no ifanarahana.
  • +//
  • Tsy azo atao ny mamerina ny entana efa nofandrahana.
  • +//
  • Tsy azo atao ny manakalo ny entana efa nofandrahana.
  • +//
+//
+//
+ +// +//
+//
+//

NY MPAMANDRIKA

+//
Signature
+//
+//
+//

NY MPIVAROTRA

+//
+// KELY SCOOTERS
+// NIF: 401 840 5554 +//
+//
+//
+//
+ +// +// +// +// HTML; +// } + +/** + * Récupérer la prévisualisation de la facture pour le modal + */ +/** + * Récupérer la prévisualisation de la facture pour le modal + */ +public function getInvoicePreview($avance_id) +{ + $this->verifyRole('viewAvance'); + + try { + $session = session(); + $users = $session->get('user'); + + $isCaissier = $this->isCaissier($users); + $isDirection = in_array($users['group_name'], ['Direction', 'Conseil']); + + if (!$isCaissier && !$isDirection) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Accès non autorisé' + ]); + } + + $Avance = new Avance(); + $Products = new Products(); + + $avance = $Avance->fetchSingleAvance($avance_id); + + if (!$avance) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Avance non trouvée' + ]); + } + + if (!$isDirection && $avance['store_id'] !== $users['store_id']) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Accès non autorisé à cette avance' + ]); + } + + // ✅ CORRECTION SIMPLIFIÉE: Récupérer le nom de la marque + if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) { + $productName = $avance['product_name']; + $productDetails = [ + 'marque' => $avance['product_name'], + 'numero_moteur' => '', + 'puissance' => '' + ]; + } else { + // Récupérer le produit + $product = $Products->find($avance['product_id']); + + if (!$product) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Produit non trouvé' + ]); + } + + $productName = $product['name'] ?? 'N/A'; + + // ✅ Récupérer le nom de la marque depuis la table brands + $brandName = 'N/A'; + if (!empty($product['marque'])) { + $db = \Config\Database::connect(); + $brandQuery = $db->table('brands') + ->select('name') + ->where('id', $product['marque']) + ->get(); + $brandResult = $brandQuery->getRowArray(); + if ($brandResult) { + $brandName = $brandResult['name']; + } + } + + $productDetails = [ + 'marque' => $brandName, // ✅ Nom de la marque au lieu de l'ID + 'numero_moteur' => $product['numero_de_moteur'] ?? '', + 'puissance' => $product['puissance'] ?? '' + ]; + } + + $html = $this->generateSimplifiedInvoiceForModal($avance, $productName, $productDetails); + + return $this->response->setJSON([ + 'success' => true, + 'html' => $html, + 'avance_id' => $avance_id, + 'can_print' => $isCaissier + ]); + + } catch (\Exception $e) { + log_message('error', "Erreur prévisualisation facture: " . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Erreur lors de la prévisualisation : ' . $e->getMessage() + ]); + } +} +/** + * Générer le HTML de la facture pour le modal (version simplifiée) + */ +/** + * Générer un aperçu simplifié de la facture pour le modal + */ +private function generateSimplifiedInvoiceForModal($avance, $productName, $productDetails) +{ + $avanceDate = date('d/m/Y', strtotime($avance['avance_date'])); + $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); + $customerName = strtoupper(esc($avance['customer_name'])); + $customerPhone = esc($avance['customer_phone']); + $customerCin = esc($avance['customer_cin']); + $grossAmount = number_format($avance['gross_amount'], 0, ',', ' '); + $avanceAmount = number_format($avance['avance_amount'], 0, ',', ' '); + $amountDue = number_format($avance['amount_due'], 0, ',', ' '); + $marque = esc($productDetails['marque']) ?: $productName; + + return << + .simplified-invoice { + font-family: Arial, sans-serif; + max-width: 100%; + background: white; + padding: 20px; + } + + .simplified-header { + display: flex; + justify-content: space-between; + border-bottom: 2px solid #333; + padding-bottom: 15px; + margin-bottom: 20px; + } + + .company-info h3 { + margin: 0 0 10px 0; + font-size: 20px; + font-weight: bold; + } + + .company-info p { + margin: 3px 0; + font-size: 11px; + } + + .invoice-info { + text-align: right; + } + + .invoice-info h2 { + margin: 0 0 10px 0; + font-size: 28px; + font-weight: bold; + color: #333; + } + + .invoice-info p { + margin: 3px 0; + font-size: 13px; + } + + .doit-badge { + display: inline-block; + background: #000; + color: #fff; + padding: 5px 20px; + font-weight: bold; + transform: skewX(-10deg); + margin-top: 10px; + } + + .customer-section { + background: #f8f8f8; + padding: 15px; + border: 1px solid #ddd; + margin: 20px 0; + } + + .customer-section h4 { + margin: 0 0 10px 0; + font-size: 14px; + font-weight: bold; + } + + .customer-details { + font-size: 13px; + line-height: 1.8; + } + + .customer-details strong { + display: inline-block; + width: 80px; + } + + .amounts-box { + border: 2px solid #333; + padding: 15px; + margin: 20px 0; + background: #fff; + } + + .amount-row { + display: flex; + justify-content: space-between; + padding: 8px 0; + font-size: 14px; + border-bottom: 1px solid #eee; + } + + .amount-row:last-child { + border-bottom: none; + font-weight: bold; + font-size: 16px; + color: #d32f2f; + } + + .amount-row strong { + min-width: 120px; + } + + .product-table { + width: 100%; + border-collapse: collapse; + margin: 20px 0; + } + + .product-table th, + .product-table td { + border: 1px solid #333; + padding: 10px; + text-align: left; + } + + .product-table th { + background: #f0f0f0; + font-weight: bold; + font-size: 13px; + } + + .product-table td { + font-size: 13px; + } + + +
+ +
+
+

KELY SCOOTERS

+

NIF: 401 840 5554

+

STAT: 46101 11 2024 00317

+

Contact: +261 34 27 946 35 / +261 34 07 079 69

+

Antsakaviro en face WWF

+
+ +
+

FACTURE

+

Date: {$avanceDate}

+

N°: {$avanceNumber}

+
DOIT ORIGINAL
+
+
+ + +
+

INFORMATIONS CLIENT

+
+
NOM: {$customerName}
+
Téléphone: {$customerPhone}
+
CIN: {$customerCin}
+
+
+ + +
+
+ PC (Prix Total): + {$grossAmount} Ar +
+
+ AVANCE: + {$avanceAmount} Ar +
+
+ RAP (Reste à payer): + {$amountDue} Ar +
+
+ + + + + + + + + + + + + + + + + + + +
MARQUEN°MOTEURPUISSANCERAP (Ariary)
{$marque}{$productDetails['numero_moteur']}{$productDetails['puissance']}{$amountDue}
+
+HTML; +} + +public function notifyPrintInvoice() +{ + $this->verifyRole('viewAvance'); + + try { + $session = session(); + $users = $session->get('user'); + + if (!$this->isCaissier($users)) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Accès non autorisé' + ]); + } + + $avance_id = $this->request->getPost('avance_id'); + + if (!$avance_id) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'ID avance manquant' + ]); + } + + $Avance = new Avance(); + $avance = $Avance->fetchSingleAvance($avance_id); + + if (!$avance) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Avance non trouvée' + ]); + } + + if ($avance['store_id'] !== $users['store_id']) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Accès non autorisé' + ]); + } + + // ✅ RETIRÉ : $Avance->markAsPrinted($avance_id); + + // Envoyer notification à la Direction + $Notification = new NotificationController(); + $customerName = $avance['customer_name']; + $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); + + $Notification->createNotification( + "La caissière a imprimé la facture N°{$avanceNumber} pour le client {$customerName}", + "Direction", + (int)$users['store_id'], + 'avances' + ); + + $Notification->createNotification( + "Il y a une avance N°{$avanceNumber} pour le client {$customerName}", + "DAF", + (int)$users['store_id'], + 'avances' + ); + + return $this->response->setJSON([ + 'success' => true, + 'messages' => 'Facture imprimée avec succès ! Notification envoyée à la Direction.' + ]); + + } catch (\Exception $e) { + log_message('error', "Erreur notification impression: " . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Erreur lors de l\'envoi de la notification' + ]); + } +} + +/** + * Récupérer le HTML complet de la facture pour impression + */ +public function getFullInvoiceForPrint($avance_id) +{ + $this->verifyRole('viewAvance'); + + try { + $session = session(); + $users = $session->get('user'); + + if (!$this->isCaissier($users)) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Seule la caissière peut imprimer les factures' + ]); + } + + $Avance = new Avance(); + $Products = new Products(); + + $avance = $Avance->fetchSingleAvance($avance_id); + + if (!$avance) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Avance non trouvée' + ]); + } + + if ($avance['store_id'] !== $users['store_id']) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Accès non autorisé' + ]); + } + + // ✅ CORRECTION SIMPLIFIÉE: Récupérer le nom de la marque + if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) { + $productName = $avance['product_name']; + $productDetails = [ + 'marque' => $avance['product_name'], + 'numero_moteur' => '', + 'puissance' => '' + ]; + } else { + $product = $Products->find($avance['product_id']); + + if (!$product) { + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Produit non trouvé' + ]); + } + + $productName = $product['name'] ?? 'N/A'; + + // ✅ Récupérer le nom de la marque depuis la table brands + $brandName = 'N/A'; + if (!empty($product['marque'])) { + $db = \Config\Database::connect(); + $brandQuery = $db->table('brands') + ->select('name') + ->where('id', $product['marque']) + ->get(); + $brandResult = $brandQuery->getRowArray(); + if ($brandResult) { + $brandName = $brandResult['name']; + } + } + + $productDetails = [ + 'marque' => $brandName, // ✅ Nom de la marque + 'numero_moteur' => $product['numero_de_moteur'] ?? '', + 'puissance' => $product['puissance'] ?? '' + ]; + } + + $html = $this->generatePrintableInvoiceHTML($avance, $productName, $productDetails); + + return $this->response->setJSON([ + 'success' => true, + 'html' => $html + ]); + + } catch (\Exception $e) { + log_message('error', "Erreur récupération facture impression: " . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Erreur lors de la récupération de la facture' + ]); + } +} + +/** + * Générer le HTML optimisé pour l'impression (version identique à printInvoice) + */ +private function generatePrintableInvoiceHTML($avance, $productName, $productDetails) +{ + $avanceDate = date('d/m/Y', strtotime($avance['avance_date'])); + $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); + $customerName = strtoupper(esc($avance['customer_name'])); + $customerPhone = esc($avance['customer_phone']); + $customerCin = esc($avance['customer_cin']); + $grossAmount = number_format($avance['gross_amount'], 0, ',', ' '); + $avanceAmount = number_format($avance['avance_amount'], 0, ',', ' '); + $amountDue = number_format($avance['amount_due'], 0, ',', ' '); + $marque = esc($productDetails['marque']) ?: $productName; + $numeroMoteur = esc($productDetails['numero_moteur']); + $puissance = esc($productDetails['puissance']); + return << + + + + + Facture Avance - KELY SCOOTERS + + + +
+ +
+

KELY SCOOTERS

+
+
+
NIF: 401 840 5554
+
STAT: 46101 11 2024 00317
+
+
+
Contact: +261 34 27 946 35 / +261 34 07 079 69
+
Antsakaviro en face WWF
+
+
+
+ + +
+
+

FACTURE

+
Date: {$avanceDate}
+
N°: {$avanceNumber}CI 2025
+
+
DOIT ORIGINAL
+
+ + +
+
NOM: {$customerName} ({$customerPhone})
+
CIN: {$customerCin}
+
PC: {$grossAmount} Ar
+
AVANCE: {$avanceAmount} Ar
+
RAP: {$amountDue} Ar
+
+ + + + + + + + + + + + + + + + + + + +
MARQUEN°MOTEURPUISSANCERAP (Ariary)
{$marque}{$numeroMoteur}{$puissance}{$amountDue}
+ + +
+

FIFANEKENA ARA-BAROTRA (Réservations)

+

Ry mpanjifa hajaina,

+

Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.

+ +
+ Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA +

Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).

+
+ +
+ Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE) +

Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.

+

Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.

+
+ +
+ Andininy faha-3: FAMERENANA VOLA +

Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.

+
+ +
+ Andininy faha-4: FEPETRA FANAMPINY +
    +
  • Tsy misafidy raha toa ka mamafa no ifanarahana.
  • +
  • Tsy azo atao ny mamerina ny entana efa nofandrahana.
  • +
  • Tsy azo atao ny manakalo ny entana efa nofandrahana.
  • +
+
+
+ + +
+
+

NY MPAMANDRIKA

+
Signature
+
+
+

NY MPIVAROTRA

+
+ KELY SCOOTERS
+ NIF: 401 840 5554 +
+
+
+
+ + +HTML; +} + +/** + * ✅ NOUVELLE MÉTHODE : Traiter manuellement les avances expirées + * URL: /avances/processExpiredAvances + * Accessible via bouton dans l'interface ou manuellement + */ + public function processExpiredAvances() + { + try { + log_message('info', "=== DÉBUT processExpiredAvances (manuel) ==="); + + $Avance = new Avance(); + $Products = new Products(); + + $today = date('Y-m-d'); + + // Récupérer les avances expirées et encore actives + $expiredAvances = $Avance + ->where('DATE(deadline) <', $today) + ->where('active', 1) + ->where('is_order', 0) + ->findAll(); + + if (empty($expiredAvances)) { + return $this->response->setJSON([ + 'success' => true, + 'messages' => 'Aucune avance expirée à traiter', + 'processed' => 0 + ]); + } + + $processedCount = 0; + $errorCount = 0; + $details = []; + + foreach ($expiredAvances as $avance) { + try { + // Désactiver l'avance + $Avance->update($avance['avance_id'], ['active' => 0]); + + $detail = [ + 'avance_id' => $avance['avance_id'], + 'customer' => $avance['customer_name'], + 'deadline' => $avance['deadline'], + 'product_freed' => false + ]; + + // Libérer le produit si c'est une avance "sur terre" + if ($avance['type_avance'] === 'terre' && !empty($avance['product_id'])) { + $Products->update($avance['product_id'], ['product_sold' => 0]); + $detail['product_freed'] = true; + $detail['product_id'] = $avance['product_id']; + } + + $details[] = $detail; + $processedCount++; + + } catch (\Exception $e) { + log_message('error', "Erreur traitement avance {$avance['avance_id']}: " . $e->getMessage()); + $errorCount++; + } + } + + log_message('info', "=== FIN processExpiredAvances - Traités: {$processedCount}, Erreurs: {$errorCount} ==="); + + return $this->response->setJSON([ + 'success' => true, + 'messages' => "Avances expirées traitées avec succès", + 'processed' => $processedCount, + 'errors' => $errorCount, + 'details' => $details + ]); + + } catch (\Exception $e) { + log_message('error', "Erreur processExpiredAvances: " . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Erreur lors du traitement des avances expirées: ' . $e->getMessage() + ]); + } + } + + /** + * ✅ NOUVELLE MÉTHODE : Vérifier les avances qui vont expirer dans X jours + * URL: /avances/checkExpiringAvances/{days} + * Utile pour la Direction/DAF pour anticiper + */ + public function checkExpiringAvances($days = 0) + { + try { + $Avance = new Avance(); + $Products = new Products(); + + $targetDate = date('Y-m-d', strtotime("+{$days} days")); + + $expiringAvances = $Avance + ->where('DATE(deadline)', $targetDate) + ->where('active', 1) + ->where('is_order', 0) + ->findAll(); + + $result = []; + + foreach ($expiringAvances as $avance) { + $productInfo = null; + + if ($avance['type_avance'] === 'terre' && !empty($avance['product_id'])) { + $product = $Products->find($avance['product_id']); + if ($product) { + $productInfo = [ + 'id' => $product['id'], + 'name' => $product['name'], + 'sku' => $product['sku'] + ]; + } + } + + $result[] = [ + 'avance_id' => $avance['avance_id'], + 'customer_name' => $avance['customer_name'], + 'customer_phone' => $avance['customer_phone'], + 'deadline' => $avance['deadline'], + 'amount_due' => $avance['amount_due'], + 'type_avance' => $avance['type_avance'], + 'product' => $productInfo + ]; + } + + return $this->response->setJSON([ + 'success' => true, + 'count' => count($result), + 'target_date' => $targetDate, + 'avances' => $result + ]); + + } catch (\Exception $e) { + log_message('error', "Erreur checkExpiringAvances: " . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Erreur lors de la vérification: ' . $e->getMessage() + ]); + } + } + +// Dans App\Controllers\AvanceController.php + +/** + * ✅ Fonction de paiement d'avance (à modifier ou créer) + */ +public function payAvance() +{ + $avance_id = $this->request->getPost('avance_id'); + $montant_paye = (float)$this->request->getPost('montant_paye'); + + $avanceModel = new \App\Models\Avance(); + $avance = $avanceModel->find($avance_id); + + if (!$avance) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Avance introuvable' + ]); + } + + // Calcul du nouveau montant dû + $amount_due = max(0, (float)$avance['amount_due'] - $montant_paye); + // ✅ Mise à jour de l'avance + $avanceModel->update($avance_id, [ + 'avance_amount' => (float)$avance['avance_amount'] + $montant_paye, + 'amount_due' => $amount_due, + ]); + + // ✅ NOUVEAU : Conversion automatique UNIQUEMENT pour avances TERRE + if ($amount_due <= 0) { + + if ($avance['type_avance'] === 'terre') { + // ✅ Avance TERRE complète → Conversion en commande + log_message('info', "💰 Avance TERRE {$avance_id} complétée ! Conversion en commande..."); + + $order_id = $avanceModel->convertToOrder($avance_id); + + if ($order_id) { + return $this->response->setJSON([ + 'success' => true, + 'message' => '✅ Paiement effectué ! L\'avance TERRE a été convertie en commande.', + 'converted' => true, + 'type' => 'terre', + 'order_id' => $order_id, + 'redirect_url' => site_url('orders/update/' . $order_id) + ]); + } else { + log_message('error', "Échec conversion avance TERRE {$avance_id} en commande"); + return $this->response->setJSON([ + 'success' => true, + 'message' => '⚠️ Paiement effectué mais erreur lors de la création de la commande.', + 'converted' => false + ]); + } + + } else { + // ✅ Avance MER complète → Reste dans la liste + log_message('info', "💰 Avance MER {$avance_id} complétée ! Elle reste dans la liste des avances."); + + return $this->response->setJSON([ + 'success' => true, + 'message' => '✅ Paiement effectué ! L\'avance MER est maintenant complète.', + 'converted' => false, + 'type' => 'mere', + 'status' => 'completed' + ]); + } + } + + // ✅ Paiement partiel + return $this->response->setJSON([ + 'success' => true, + 'message' => '✅ Paiement partiel enregistré avec succès', + 'amount_due_remaining' => $amount_due, + 'type' => $avance['type_avance'] + ]); +} +/** + * ✅ Conversion manuelle (optionnel - pour forcer la conversion) + */ +public function forceConvertToOrder($avance_id) +{ + $this->verifyRole('updateAvance'); // Adapter selon vos permissions + + $avanceModel = new \App\Models\Avance(); + $order_id = $avanceModel->convertToOrder($avance_id); + + if ($order_id) { + session()->setFlashdata('success', 'Avance convertie en commande avec succès !'); + return redirect()->to('orders/update/' . $order_id); + } else { + session()->setFlashdata('errors', 'Erreur lors de la conversion de l\'avance.'); + return redirect()->back(); + } +} + +/** + * ✅ Vérifier et convertir toutes les avances complètes + * URL: /avances/checkAndConvertCompleted + */ +public function checkAndConvertCompleted() +{ + try { + $Avance = new Avance(); + + // ✅ Récupérer uniquement les avances TERRE complètes non converties + $completedTerreAvances = $Avance->getCompletedNotConverted(); + + $convertedCount = 0; + $errorCount = 0; + $details = []; + + foreach ($completedTerreAvances as $avance) { + $order_id = $Avance->convertToOrder($avance['avance_id']); + + if ($order_id) { + $convertedCount++; + $details[] = [ + 'avance_id' => $avance['avance_id'], + 'customer' => $avance['customer_name'], + 'type' => 'terre', + 'order_id' => $order_id, + 'status' => 'success' + ]; + } else { + $errorCount++; + $details[] = [ + 'avance_id' => $avance['avance_id'], + 'customer' => $avance['customer_name'], + 'type' => 'terre', + 'status' => 'error' + ]; + } + } + + return $this->response->setJSON([ + 'success' => true, + 'message' => 'Vérification terminée', + 'converted' => $convertedCount, + 'errors' => $errorCount, + 'note' => 'Seules les avances TERRE sont converties. Les avances MER restent dans la liste.', + 'details' => $details + ]); + + } catch (\Exception $e) { + log_message('error', "Erreur checkAndConvertCompleted: " . $e->getMessage()); + return $this->response->setJSON([ + 'success' => false, + 'messages' => 'Erreur: ' . $e->getMessage() + ]); + } +} + } \ No newline at end of file diff --git a/app/Controllers/Dashboard.php b/app/Controllers/Dashboard.php index f7266b7d..8aa0d864 100644 --- a/app/Controllers/Dashboard.php +++ b/app/Controllers/Dashboard.php @@ -21,6 +21,10 @@ class Dashboard extends AdminController public function index() { + // === 🔥 Récupérer l'utilisateur en premier === + $session = session(); + $user_id = $session->get('user'); + $productModel = new Products(); $orderModel = new Orders(); $userModel = new Users(); @@ -65,49 +69,82 @@ class Dashboard extends AdminController $es_avances = isset($paymentDataAvance->total_espece) ? (float) $paymentDataAvance->total_espece : 0; $vb_avances = isset($paymentDataAvance->total_virement_bancaire) ? (float) $paymentDataAvance->total_virement_bancaire : 0; - // === COMBINAISON ORDERS + AVANCES === - $total_mvola = $mv1_orders + $mv2_orders + $mv_avances; - $total_espece = $es1_orders + $es2_orders + $es_avances; - $total_vb = $vb1_orders + $vb2_orders + $vb_avances; - $total = $total_orders + $total_avances; - - // === AJUSTEMENTS AVEC RECOUVREMENTS ET SORTIES (PAR MODE DE PAIEMENT) === - $total_mvola_final = $total_mvola - - $me - - $mb + - $bm - - $total_sortie_mvola; - - $total_espece_final = $total_espece + - $me + - $be - - $total_sortie_espece; - - $total_virement_bancaire_final = $total_vb - - $be - - $bm + - $mb - - $total_sortie_virement; + // === COMBINAISON ORDERS + AVANCES (BRUT = CE QUE LA CAISSIÈRE A ENCAISSÉ) === + $total_mvola_brut = $mv1_orders + $mv2_orders + $mv_avances; + $total_espece_brut = $es1_orders + $es2_orders + $es_avances; + $total_vb_brut = $vb1_orders + $vb2_orders + $vb_avances; + $total_brut = $total_orders + $total_avances; + + // === AJUSTEMENTS AVEC RECOUVREMENTS ET SORTIES === + $total_mvola_final = $total_mvola_brut - $me - $mb + $bm - $total_sortie_mvola; + $total_espece_final = $total_espece_brut + $me + $be - $total_sortie_espece; + $total_virement_bancaire_final = $total_vb_brut - $be - $bm + $mb - $total_sortie_virement; // === CALCUL DU TOTAL GÉNÉRAL === - // ✅ CORRECTION: Ne PAS additionner les recouvrements au total - // Les recouvrements sont des transferts internes, pas des entrées d'argent $total_sortie_global = $total_sortie_espece + $total_sortie_mvola + $total_sortie_virement; - $total_final = $total - $total_sortie_global; - - // check avance expired - $avance = new Avance(); - $avance->checkExpiredAvance(); - + $total_final = $total_brut - $total_sortie_global; + + // ✅ MODIFICATION : La caissière voit EXACTEMENT les mêmes montants que Direction/Conseil $data = [ + // === POUR DIRECTION/CONSEIL (AVEC TOUS LES AJUSTEMENTS) === 'total' => $total_final, 'total_mvola' => $total_mvola_final, 'total_espece' => $total_espece_final, 'total_virement_bancaire' => $total_virement_bancaire_final, - 'user_permission' => $this->permission, + + // === POUR CAISSIÈRE (MÊME CALCUL QUE DIRECTION) === + 'total_caisse' => $total_final, // ← Identique à 'total' + 'total_mvola_caisse' => $total_mvola_final, // ← Identique à 'total_mvola' + 'total_espece_caisse' => $total_espece_final, // ← Identique à 'total_espece' + 'total_vb_caisse' => $total_virement_bancaire_final, // ← Identique à 'total_virement_bancaire' + + // === DÉTAIL POUR LA CAISSIÈRE === + 'total_orders_only' => $total_orders, // Ventes complètes uniquement + 'total_avances' => $total_avances, // Avances uniquement + + // ✅ Détails des sorties + 'total_sorties' => $total_sortie_global, + 'total_sortie_espece' => $total_sortie_espece, + 'total_sortie_mvola' => $total_sortie_mvola, + 'total_sortie_virement' => $total_sortie_virement, + + // ✅ Détails des recouvrements + 'recouvrement_me' => $me, // Mvola → Espèce + 'recouvrement_be' => $be, // Banque → Espèce + 'recouvrement_bm' => $bm, // Banque → Mvola + 'recouvrement_mb' => $mb, // Mvola → Banque + 'total_recouvrements' => $me + $be + $bm + $mb, + + // Détail avances par mode de paiement + 'total_avances_mvola' => $mv_avances, + 'total_avances_espece' => $es_avances, + 'total_avances_virement' => $vb_avances, + + // Détail orders par mode de paiement + 'total_mvola_orders' => $mv1_orders + $mv2_orders, + 'total_espece_orders' => $es1_orders + $es2_orders, + 'total_vb_orders' => $vb1_orders + $vb2_orders, + + // ✅ Montants bruts (avant recouvrements et sorties) + 'total_brut' => $total_brut, + 'total_mvola_brut' => $total_mvola_brut, + 'total_espece_brut' => $total_espece_brut, + 'total_vb_brut' => $total_vb_brut, ]; - $data['total_products'] = $productModel->countTotalProducts(); + // === ✅ Compter les produits selon le store de l'utilisateur === + $data['total_products'] = $productModel->countProductsByUserStore(); + + // === ✅ Récupérer le nom du store pour l'affichage === + $isAdmin = in_array($user_id['group_name'], ['DAF', 'Direction']); + + if (!$isAdmin && !empty($user_id['store_id']) && $user_id['store_id'] != 0) { + $store = $storeModel->getStoresData($user_id['store_id']); + $data['store_name'] = $store['name'] ?? 'Votre magasin'; + } else { + $data['store_name'] = 'Tous les magasins'; + } + $data['total_paid_orders'] = $orderModel->countTotalPaidOrders(); $data['total_users'] = $userModel->countTotalUsers(); $data['total_stores'] = $storeModel->countTotalStores(); @@ -175,16 +212,14 @@ class Dashboard extends AdminController $data['count_id'] = $countId; - // Check if the user is an Conseil - $session = session(); - $user_id = $session->get('user'); $data['is_admin'] = false; $data['isCommercial'] = false; $data['isChef'] = false; $data['isCaissier'] = false; $data['isMecanicien'] = false; + $data['isSecurite'] = false; - if ($user_id['group_name'] == "Direction" || $user_id['group_name'] == "Conseil") { + if ($user_id['group_name'] == "Direction" || $user_id['group_name'] == "DAF") { $data['is_admin'] = true; } @@ -203,6 +238,9 @@ class Dashboard extends AdminController if ($user_id['group_name'] == "MECANICIEN") { $data['isMecanicien'] = true; } + if ($user_id['group_name'] == "Sécurité" || $user_id['group_name'] == "SECURITE") { + $data['isSecurite'] = true; + } $data['page_title'] = 'Dashboard'; $data['marques_total'] = json_encode($orderModel->getTotalProductvente()); diff --git a/app/Controllers/NotificationController.php b/app/Controllers/NotificationController.php index b271602b..cd06a01d 100644 --- a/app/Controllers/NotificationController.php +++ b/app/Controllers/NotificationController.php @@ -43,4 +43,25 @@ class NotificationController extends AdminController $Notification->insertNotification($data); } + + // Marquer toutes les notifications comme lues pour l'utilisateur connecté +public function markAllAsRead() +{ + $Notification = new Notification(); + $session = session(); + $users = $session->get('user'); + + // Mettre à jour toutes les notifications non lues pour ce store et ce groupe + $builder = $Notification->builder(); + $builder->where('store_id', $users['store_id']) + ->groupStart() + ->where('forgroup', $users['group_name']) + ->orWhere('forgroup', strtolower('TOUS')) + ->groupEnd() + ->where('is_read', 0) + ->set(['is_read' => 1]) + ->update(); + + return $this->response->setJSON(['status' => 'success']); +} } diff --git a/app/Controllers/OrderController.php b/app/Controllers/OrderController.php index b206a79c..1faa286d 100644 --- a/app/Controllers/OrderController.php +++ b/app/Controllers/OrderController.php @@ -41,23 +41,25 @@ class OrderController extends AdminController helper(['url', 'form']); $Orders = new Orders(); $result = ['data' => []]; - + $data = $Orders->getOrdersData(); $session = session(); $users = $session->get('user'); - + + // ======================================== + // POUR CAISSIÈRE + // ======================================== if ($users['group_name'] == "Caissière") { foreach ($data as $key => $value) { $date_time = date('d-m-Y h:i a', strtotime($value['date_time'])); - + $buttons = ''; - + // Bouton imprimer (sauf pour SECURITE) if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") { $buttons .= ''; } - - + // Bouton voir if (in_array('viewOrder', $this->permission)) { $buttons .= ' @@ -70,13 +72,15 @@ class OrderController extends AdminController '; } - - // Bouton modifier - if (in_array('updateOrder', $this->permission) && $users["store_id"] == $value['store_id']) { + + // ✅ Bouton modifier pour statuts 0 (Refusé) et 2 (En Attente) + if (in_array('updateOrder', $this->permission) + && $users["store_id"] == $value['store_id'] + && in_array($value['paid_status'], [0, 2])) { $buttons .= ' '; } - - // Statut de paiement + + // Statut de paiement if ($value['paid_status'] == 1) { $paid_status = 'Validé'; } elseif ($value['paid_status'] == 2) { @@ -86,13 +90,13 @@ class OrderController extends AdminController } else { $paid_status = 'Refusé'; } - - // Calcul délai (SANS notification ici) + + // Calcul délai $date1 = new DateTime($date_time); $date2 = new DateTime(); $interval = $date1->diff($date2); $daysPassed = $interval->days; - + $statuDate = ''; if ($value['paid_status'] == 2) { if ($daysPassed < 8) { @@ -103,10 +107,7 @@ class OrderController extends AdminController $statuDate = ' depuis ' . $daysPassed . ' Jours'; } } - - // $Orders_items = new OrderItems(); - // $sum_order_item = $Orders_items->getSumOrdersItemData($value['id']); - + $result['data'][$key] = [ $value['product_names'], $value['user_name'], @@ -119,31 +120,32 @@ class OrderController extends AdminController } return $this->response->setJSON($result); } - + + // ======================================== + // POUR DIRECTION OU DAF + // ======================================== elseif($users['group_name'] == "Direction" || $users['group_name'] == "DAF"){ foreach ($data as $key => $value) { $date_time = date('d-m-Y h:i a', strtotime($value['date_time'])); - + $buttons = ''; - - // Bouton imprimer (sauf pour SECURITE) - if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") { + + // Bouton imprimer + if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") { $buttons .= ''; } - - - if (in_array('updateOrder', $this->permission)) { + + // ✅ Bouton modifier pour statuts 0 (Refusé) et 2 (En Attente) + if (in_array('updateOrder', $this->permission) && in_array($value['paid_status'], [0, 2])) { $buttons .= ' '; } - - if (in_array('deleteOrder', $this->permission)) { + + // ✅ Bouton supprimer pour statuts 0 et 2 + if (in_array('deleteOrder', $this->permission) && in_array($value['paid_status'], [0, 2])) { $buttons .= ' '; } - - - - - // Statut de paiement + + // Statut de paiement if ($value['paid_status'] == 1) { $paid_status = 'Validé'; } elseif ($value['paid_status'] == 2) { @@ -153,13 +155,13 @@ class OrderController extends AdminController } else { $paid_status = 'Refusé'; } - - // Calcul délai (SANS notification) + + // Calcul délai $date1 = new DateTime($date_time); $date2 = new DateTime(); $interval = $date1->diff($date2); $daysPassed = $interval->days; - + $statuDate = ''; if ($value['paid_status'] == 2) { if ($daysPassed < 8) { @@ -170,10 +172,7 @@ class OrderController extends AdminController $statuDate = ' depuis ' . $daysPassed . ' Jours'; } } - - // $Orders_items= new OrderItems(); - // $sum_order_item = $Orders_items->getSumOrdersItemData($value['id']); - + $result['data'][$key] = [ $value['bill_no'], $value['customer_name'], @@ -187,23 +186,29 @@ class OrderController extends AdminController } return $this->response->setJSON($result); } + + // ======================================== + // POUR LES AUTRES UTILISATEURS (COMMERCIALE, SECURITE, etc.) + // ======================================== else { foreach ($data as $key => $value) { $date_time = date('d-m-Y h:i a', strtotime($value['date_time'])); - + $buttons = ''; - - // Bouton imprimer (sauf pour SECURITE) - if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") { + + // Bouton imprimer + if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") { $buttons .= ''; } - if (in_array('updateOrder', $this->permission) && $users["id"] == $value['user_id']) { - - $buttons .= ' '; - + // ✅ Bouton modifier pour statuts 0 et 2, ET si c'est l'utilisateur créateur + if (in_array('updateOrder', $this->permission) + && $users["id"] == $value['user_id'] + && in_array($value['paid_status'], [0, 2])) { + $buttons .= ' '; } - + + // Bouton voir if (in_array('viewOrder', $this->permission)) { $buttons .= ' '; } - - if (in_array('deleteOrder', $this->permission) && $users["id"] == $value['user_id']) { + + // ✅ Bouton supprimer pour statuts 0 et 2, ET si c'est l'utilisateur créateur + if (in_array('deleteOrder', $this->permission) + && $users["id"] == $value['user_id'] + && in_array($value['paid_status'], [0, 2])) { $buttons .= ' '; } - - - // Statut de paiement + + // Statut de paiement if ($value['paid_status'] == 1) { $paid_status = 'Validé'; } elseif ($value['paid_status'] == 2) { @@ -231,13 +238,13 @@ class OrderController extends AdminController } else { $paid_status = 'Refusé'; } - - // Calcul délai (SANS notification) + + // Calcul délai $date1 = new DateTime($date_time); $date2 = new DateTime(); $interval = $date1->diff($date2); $daysPassed = $interval->days; - + $statuDate = ''; if ($value['paid_status'] == 2) { if ($daysPassed < 8) { @@ -248,10 +255,7 @@ class OrderController extends AdminController $statuDate = ' depuis ' . $daysPassed . ' Jours'; } } - - // $Orders_items= new OrderItems(); - // $sum_order_item = $Orders_items->getSumOrdersItemData($value['id']); - + $result['data'][$key] = [ $value['product_names'], $value['user_name'], @@ -362,7 +366,7 @@ class OrderController extends AdminController return redirect()->back() ->withInput() ->with('errors', [ - "⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est inférieur au prix minimal autorisé de {$prixMinimalFormatted} Ar." + "⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé." ]); } } @@ -477,10 +481,8 @@ class OrderController extends AdminController // Redirection selon le rôle if ($users["group_name"] != "COMMERCIALE") { $this->checkProductisNull($posts, $users['store_id']); - return redirect()->to('orders/update/' . $order_id); - } else { - return redirect()->to('orders/'); } + return redirect()->to('orders/'); } else { session()->setFlashdata('errors', 'Error occurred!!'); @@ -626,6 +628,22 @@ public function markAsDelivered() $data['page_title'] = $this->pageTitle; $validation = \Config\Services::validation(); + // ✅ NOUVELLE VÉRIFICATION : Bloquer UNIQUEMENT si statut = Validé (1) ou Validé et Livré (3) + $Orders = new Orders(); + $current_order = $Orders->getOrdersData($id); + + if (!$current_order) { + session()->setFlashData('errors', 'Commande introuvable.'); + return redirect()->to('orders/'); + } + + // ✅ Bloquer UNIQUEMENT les statuts 1 (Validé) et 3 (Validé et Livré) + // Le statut 0 (Refusé) et 2 (En Attente) restent modifiables + if (in_array($current_order['paid_status'], [1, 3])) { + session()->setFlashData('errors', 'Cette commande ne peut plus être modifiée car elle est déjà validée ou livrée.'); + return redirect()->to('orders/'); + } + // Règles de validation $validation->setRules([ 'product' => 'required' @@ -635,7 +653,6 @@ public function markAsDelivered() 'product' => $this->request->getPost('product') ]; - $Orders = new Orders(); $Company = new Company(); $Products = new Products(); $OrderItems = new OrderItems(); @@ -646,12 +663,18 @@ public function markAsDelivered() if ($this->request->getMethod() === 'post' && $validation->run($validationData)) { - $current_order = $Orders->getOrdersData($id); + // ✅ DOUBLE VÉRIFICATION avant l'update + $current_order_check = $Orders->getOrdersData($id); + if (in_array($current_order_check['paid_status'], [1, 3])) { + session()->setFlashData('errors', 'Cette commande ne peut plus être modifiée car elle est déjà validée ou livrée.'); + return redirect()->to('orders/'); + } + $old_paid_status = $current_order['paid_status']; // ✅ Statut payé pour COMMERCIALE reste toujours "En attente" if ($role === 'COMMERCIALE') { - $paid_status = 2; // ← mettre la valeur correspondant à "En attente" dans votre table + $paid_status = 2; } else { $paid_status = $this->request->getPost('paid_status'); } @@ -745,7 +768,7 @@ public function markAsDelivered() } session()->setFlashData('success', 'Commande mise à jour avec succès.'); - return redirect()->to('orders/update/' . $id); + return redirect()->to('orders/'); } else { session()->setFlashData('errors', 'Une erreur est survenue lors de la mise à jour.'); return redirect()->to('orders/update/' . $id); @@ -760,6 +783,9 @@ public function markAsDelivered() $orders_data = $Orders->getOrdersData($id); + // ✅ Ajouter un flag pour désactiver le formulaire UNIQUEMENT pour les statuts 1 et 3 + $data['is_editable'] = !in_array($orders_data['paid_status'], [1, 3]); + // Montant tranches $orders_data['montant_tranches'] = (!empty($orders_data['discount']) && $orders_data['discount'] > 0) ? $orders_data['discount'] @@ -778,9 +804,8 @@ public function markAsDelivered() return $this->render_template('orders/edit', $data); } - - + public function lookOrder(int $id) { $this->verifyRole('viewOrder'); @@ -1546,10 +1571,7 @@ public function print5(int $id) // Modèles $Orders = new Orders(); $Company = new Company(); - $Products = new Products(); $OrderItems = new OrderItems(); - $Brand = new Brands(); - $Category = new Category(); // Récupération des données $order = $Orders->getOrdersData($id); @@ -1557,17 +1579,23 @@ public function print5(int $id) $company = $Company->getCompanyData(1); $today = date('d/m/Y'); - // ✅ LOGIQUE DE REMISE: Si discount existe, il devient le prix de vente + // ✅ Vérifier si c'est une avance "sur mer" + $isAvanceMere = false; + foreach ($items as $item) { + if (!empty($item['product_name']) && empty($item['product_id'])) { + $isAvanceMere = true; + break; + } + } + + // Calculs $discount = (float) $order['discount']; $grossAmount = (float) $order['gross_amount']; - - // Si remise existe, elle devient le montant TTC, sinon on prend gross_amount $totalTTC = ($discount > 0) ? $discount : $grossAmount; $totalHT = $totalTTC / 1.20; $tva = $totalTTC - $totalHT; $inWords = $this->numberToWords((int) round($totalTTC)); - // Statut paiement $paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé'; // Début du HTML @@ -1612,13 +1640,54 @@ public function print5(int $id)

CIN : '.esc($order['customer_cin']).'

Téléphone : '.esc($order['customer_phone'] ?? '').'

Antananarivo, le '.$today.'

- +'; + + // ✅ TABLEAU ADAPTÉ SELON LE TYPE + if ($isAvanceMere) { + // ======================================== + // TABLE SIMPLIFIÉE POUR AVANCE "SUR MER" + // ======================================== + $html .= ' + + + + + + + + '; + foreach ($items as $it) { + $details = $this->getOrderItemDetails($it); + + if (!$details) continue; + + $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + + $html .= ' + + + + '; + } + + } else { + // ======================================== + // TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE + // ======================================== + $html .= '
ProduitPrix (Ar)
'.esc($details['product_name']); + + // Afficher le commentaire s'il existe + if (!empty($details['commentaire'])) { + $html .= '
'.esc($details['commentaire']).''; + } + + $html .= '
'.number_format($prixAffiche, 0, '', ' ').'
- + @@ -1627,33 +1696,26 @@ public function print5(int $id) '; -foreach ($items as $it) { - $p = $Products->getProductData($it['product_id']); - - // ✅ Récupérer le nom de la marque correctement - $brandName = 'Non définie'; - if (!empty($p['marque'])) { - $brandData = $Brand->find($p['marque']); - if ($brandData && isset($brandData['name'])) { - $brandName = $brandData['name']; - } - } - - // ✅ LOGIQUE: Si discount existe pour la commande, on l'affiche comme prix - $prixAffiche = ($discount > 0) ? $discount : $p['prix_vente']; - - $html .= ' + foreach ($items as $it) { + $details = $this->getOrderItemDetails($it); + + if (!$details) continue; + + $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + + $html .= ' - - - - - + + + + + '; -} + } + } -$html .= ' + $html .= '
Désignation MARQUEDésignation N° Moteur N° Châssis Puissance (CC)
'.esc($p['name']).''.esc($brandName).''.esc($p['numero_de_moteur']).''.esc($p['chasis'] ?? '').''.esc($p['puissance']).''.esc($details['marque']).''.esc($details['product_name']).''.esc($details['numero_moteur']).''.esc($details['numero_chassis']).''.esc($details['puissance']).' '.number_format($prixAffiche, 0, '', ' ').'
@@ -1704,7 +1766,6 @@ $html .= ' return $this->response->setBody($html); } - /** * Convertit un nombre entier en texte (français, sans décimales). * Usage basique, pour Ariary. @@ -1773,24 +1834,100 @@ private function numberToWords(int $num): string return trim($words) . ' ariary'; } -// ==================================== -// PRINT7 - Bon de commande -// ==================================== +/** + * ✅ NOUVELLE MÉTHODE : Vérifier si une commande provient d'une avance "sur mer" + */ +private function isFromAvanceMere($order_id) +{ + $db = \Config\Database::connect(); + + // Vérifier s'il existe une avance "sur mer" liée à cette commande + $avance = $db->table('avances') + ->select('type_avance, product_name') + ->where('is_order', 1) + ->where('customer_name', + $db->table('orders') + ->select('customer_name') + ->where('id', $order_id) + ->get() + ->getRow()->customer_name ?? '' + ) + ->where('type_avance', 'mere') + ->get() + ->getRowArray(); + + return $avance !== null; +} + +/** + * ✅ NOUVELLE MÉTHODE : Récupérer les détails d'un item de commande + * Gère à la fois les produits normaux et ceux des avances "sur mer" + */ +private function getOrderItemDetails($item) +{ + $Products = new Products(); + $Brand = new Brands(); + + // Si l'item a un product_name (avance sur mer), on l'utilise directement + if (!empty($item['product_name']) && empty($item['product_id'])) { + return [ + 'type' => 'mere', + 'product_name' => $item['product_name'], + 'marque' => $item['marque'] ?? $item['product_name'], + 'numero_moteur' => $item['numero_moteur'] ?? '', + 'numero_chassis' => $item['numero_chassis'] ?? '', + 'puissance' => $item['puissance'] ?? '', + 'commentaire' => $item['commentaire'] ?? '', + 'prix' => (float)$item['amount'] + ]; + } + + // Sinon, récupérer depuis la table products (avance sur terre ou commande normale) + if (empty($item['product_id'])) { + return null; + } + + $product = $Products->getProductData($item['product_id']); + + if (!$product) { + return null; + } + + // Récupérer le nom de la marque + $brandName = 'N/A'; + if (!empty($product['marque'])) { + $brandData = $Brand->find($product['marque']); + if ($brandData && isset($brandData['name'])) { + $brandName = $brandData['name']; + } + } + + return [ + 'type' => 'terre', + 'product_name' => $product['name'], + 'marque' => $brandName, + 'numero_moteur' => $product['numero_de_moteur'] ?? '', + 'numero_chassis' => $product['chasis'] ?? $product['sku'] ?? '', + 'puissance' => $product['puissance'] ?? '', + 'commentaire' => '', + 'prix' => (float)$item['amount'] + ]; +} +/** + * ✅ PRINT7 - Bon de commande adapté pour avances "sur mer" et "sur terre" + */ public function print7(int $id) { $this->verifyRole('viewOrder'); - if (! $id) { + if (!$id) { throw new \CodeIgniter\Exceptions\PageNotFoundException(); } // Modèles $Orders = new Orders(); $Company = new Company(); - $Products = new Products(); $OrderItems = new OrderItems(); - $Brand = new Brands(); - $Category = new Category(); // Récupération des données $order = $Orders->getOrdersData($id); @@ -1798,8 +1935,19 @@ public function print7(int $id) $company = $Company->getCompanyData(1); $today = date('d/m/Y'); - // Calculs totaux - $totalTTC = (float) $order['net_amount']; + // ✅ VÉRIFIER SI C'EST UNE AVANCE "SUR MER" + $isAvanceMere = false; + foreach ($items as $item) { + if (!empty($item['product_name']) && empty($item['product_id'])) { + $isAvanceMere = true; + break; + } + } + + // ✅ LOGIQUE DE REMISE + $discount = (float) $order['discount']; + $grossAmount = (float) $order['gross_amount']; + $totalTTC = ($discount > 0) ? $discount : $grossAmount; $totalHT = $totalTTC / 1.20; $tva = $totalTTC - $totalHT; @@ -1827,6 +1975,7 @@ public function print7(int $id) .conditions { page-break-before: always; padding:20px; line-height:1.5; } @media print { @page { margin:1cm; } + * { -webkit-print-color-adjust: exact; print-color-adjust: exact; } } @@ -1851,8 +2000,51 @@ public function print7(int $id)

Téléphone : '.esc($order['customer_phone']).'

CIN : '.esc($order['customer_cin']).'

Antananarivo, le '.$today.'

- + '; + + // ======================================== + // ✅ TABLEAU ADAPTÉ SELON LE TYPE + // ======================================== + if ($isAvanceMere) { + // --- TABLE SIMPLIFIÉE POUR AVANCE "SUR MER" (2-3 COLONNES) --- + $html .= ' + + + + + + + + '; + + foreach ($items as $item) { + $details = $this->getOrderItemDetails($item); + + if (!$details) continue; + + $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + + $html .= ' + + + + '; + } + + $html .= ' + +
ProduitPrix Unitaire (Ar)
'.esc($details['product_name']); + + // Afficher le commentaire s'il existe + if (!empty($details['commentaire'])) { + $html .= '
Remarque : '.esc($details['commentaire']).''; + } + + $html .= '
'.number_format($prixAffiche, 0, '', ' ').'
'; + } else { + // --- TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE (7 COLONNES) --- + $html .= ' @@ -1867,42 +2059,50 @@ public function print7(int $id) '; - foreach ($items as $item) { - $p = $Products->getProductData($item['product_id']); - - // ✅ Récupérer le nom de la marque correctement - $brandName = 'Non définie'; - if (!empty($p['marque'])) { - $brandData = $Brand->find($p['marque']); - if ($brandData && isset($brandData['name'])) { - $brandName = $brandData['name']; - } - } - - // ✅ Récupérer le nom de la catégorie - $categoryName = 'Non définie'; - if (!empty($p['categorie_id'])) { - $categoryData = $Category->find($p['categorie_id']); - if ($categoryData && isset($categoryData['name'])) { - $categoryName = $categoryData['name']; + $Products = new Products(); + $Brand = new Brands(); + $Category = new Category(); + + foreach ($items as $item) { + $details = $this->getOrderItemDetails($item); + + if (!$details) continue; + + $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + + // Récupérer la catégorie si c'est un produit terre + $categoryName = 'Non définie'; + if ($details['type'] === 'terre' && !empty($item['product_id'])) { + $p = $Products->getProductData($item['product_id']); + if (!empty($p['categorie_id'])) { + $categoryData = $Category->find($p['categorie_id']); + if ($categoryData && isset($categoryData['name'])) { + $categoryName = $categoryData['name']; + } + } } + + $html .= ' + + + + + + + + + '; } - - $html .= ' - - - - - - - - '; - } - $html .= ' + $html .= ' -
'.esc($details['product_name']).''.esc($details['marque']).''.esc($categoryName).''.esc($details['numero_moteur']).''.esc($details['numero_chassis']).''.esc($details['puissance']).''.number_format($prixAffiche, 0, '', ' ').'
'.esc($p['name']).''.esc($brandName).''.esc($categoryName).''.esc($p['numero_de_moteur']).''.esc($p['chasis'] ?? '').''.esc($p['puissance']).''.number_format($p['prix_vente'], 0, '', ' ').'
+ '; + } + // ======================================== + // TABLEAU RÉCAPITULATIF (IDENTIQUE POUR TOUS) + // ======================================== + $html .= ' @@ -1921,13 +2121,30 @@ public function print7(int $id) '; - if (! empty($order['order_payment_mode'])) { - $html .= ' + if (!empty($order['order_payment_mode'])) { + $html .= ' + '; } + if (!empty($order['tranche_1'])) { + $html .= ' + + + + '; + } + + if (!empty($order['tranche_2'])) { + $html .= ' + + + + '; + } + $html .= '
Total HT :'.$paidLabel.'
Mode de paiement : '.esc($order['order_payment_mode']).'
Tranche 1 :'.number_format((float)$order['tranche_1'], 0, '', ' ').' Ar
Tranche 2 :'.number_format((float)$order['tranche_2'], 0, '', ' ').' Ar
@@ -1959,9 +2176,8 @@ public function print7(int $id) '; - echo $html; + return $this->response->setBody($html); } - // ==================================== // PRINT31 - Facture + Bon de commande (pages séparées) // ==================================== @@ -1969,7 +2185,7 @@ public function print31(int $id) { $this->verifyRole('viewOrder'); - if (! $id) { + if (!$id) { return $this->response->setStatusCode(400, 'ID manquant'); } @@ -1984,38 +2200,39 @@ public function print31(int $id) $items = $OrderItems->getOrdersItemData($id); $company_info = $Company->getCompanyData(1); + // ✅ Vérifier si c'est une avance "sur mer" + $isAvanceMere = false; + foreach ($items as $item) { + if (!empty($item['product_name']) && empty($item['product_id'])) { + $isAvanceMere = true; + break; + } + } + $paid_status = $order_data['paid_status'] === 1 ? "Validé" : "Refusé"; + // Calculs globaux + $discount = (float) $order_data['discount']; + $grossAmount = (float) $order_data['gross_amount']; + $totalTTC = ($discount > 0) ? $discount : $grossAmount; + $totalHT = $totalTTC / 1.20; + $tva = $totalTTC - $totalHT; + // --- FACTURES : Une par produit --- foreach ($items as $item) { - $p = $Products->getProductData($item['product_id']); - $unitPrice = (float) $item['amount']; + // ✅ Utiliser getOrderItemDetails au lieu de getProductData directement + $details = $this->getOrderItemDetails($item); + + if (!$details) continue; + + $unitPrice = $details['prix']; $quantity = isset($item['qty']) ? (int) $item['qty'] : 1; $subtotal = $unitPrice * $quantity; - $vatAmount = $subtotal * 0.2; - $discount = (float) $order_data['discount']; - $totalNet = $subtotal + $vatAmount - $discount; - $inWords = $this->numberToWords((int) round($subtotal)); - - // ✅ Récupérer le nom de la marque - $brandName = 'Non définie'; - if (!empty($p['marque'])) { - $brandData = $Brand->find($p['marque']); - if ($brandData && isset($brandData['name'])) { - $brandName = $brandData['name']; - } - } - // ✅ Récupérer le nom de la catégorie - $categoryName = 'Non définie'; - if (!empty($p['categorie_id'])) { - $categoryData = $Category->find($p['categorie_id']); - if ($categoryData && isset($categoryData['name'])) { - $categoryName = $categoryData['name']; - } - } + // ✅ Pour avance sur mer avec remise, utiliser la remise comme prix + $prixAffiche = ($discount > 0 && $isAvanceMere) ? $discount : $unitPrice; echo ''; echo ''; @@ -2034,9 +2251,11 @@ public function print31(int $id) .signature { display:flex; justify-content:space-between; margin-top:50px; } .signature div { text-align:center; } .page-break { page-break-after: always; } + .to-fill { border-bottom: 1px dotted #000; min-width: 150px; display: inline-block; } @media print { body { font-size: 14px; } @page { margin: 1cm; } + * { -webkit-print-color-adjust: exact; print-color-adjust: exact; } } "; echo ''; @@ -2057,34 +2276,203 @@ public function print31(int $id) echo ''; echo '
'; - echo '

Client : ' . esc($order_data['customer_name']) . '

'; + echo '

DOIT : ' . esc($order_data['customer_name']) . '

'; echo '

Adresse : ' . esc($order_data['customer_address']) . '

'; echo '

Téléphone : ' . esc($order_data['customer_phone']) . '

'; echo '

CIN : ' . esc($order_data['customer_cin']) . '

'; echo '
'; + // ✅ TABLEAU ADAPTÉ SELON LE TYPE + if ($isAvanceMere) { + // TABLE SIMPLIFIÉE POUR AVANCE "SUR MER" + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + // Afficher commentaire si existant + if (!empty($details['commentaire'])) { + echo ''; + } + + echo '
DésignationProduit à compléterPrix (Ar)
' . esc($details['product_name']) . '                    ' . number_format($prixAffiche, 0, '', ' ') . '
Remarque : ' . esc($details['commentaire']) . '
'; + } else { + // TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE + // Récupérer catégorie + $categoryName = 'Non définie'; + if ($details['type'] === 'terre' && !empty($item['product_id'])) { + $p = $Products->getProductData($item['product_id']); + if (!empty($p['categorie_id'])) { + $categoryData = $Category->find($p['categorie_id']); + if ($categoryData && isset($categoryData['name'])) { + $categoryName = $categoryData['name']; + } + } + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo '
NomMarqueCatégorieN° MoteurChâssisPuissance (CC)Prix Unit. (Ar)
' . esc($details['product_name']) . '' . esc($details['marque']) . '' . esc($categoryName) . '' . esc($details['numero_moteur']) . '' . esc($details['numero_chassis']) . '' . esc($details['puissance']) . '' . number_format($prixAffiche, 0, '', ' ') . '
'; + } + + // Récapitulatif pour cette facture + $itemHT = $prixAffiche / 1.20; + $itemTVA = $prixAffiche - $itemHT; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + if (!empty($order_data['order_payment_mode'])) { + echo ''; + } + + if (!empty($order_data['tranche_1'])) { + echo ''; + } + + if (!empty($order_data['tranche_2'])) { + echo ''; + } + + echo '
Total HT :' . number_format($itemHT, 0, '', ' ') . ' Ar
TVA (20%) :' . number_format($itemTVA, 0, '', ' ') . ' Ar
Total TTC :' . number_format($prixAffiche, 0, '', ' ') . ' Ar
Statut :' . $paid_status . '
Mode de paiement :' . esc($order_data['order_payment_mode']) . '
Tranche 1 :' . number_format((float)$order_data['tranche_1'], 0, '', ' ') . ' Ar
Tranche 2 :' . number_format((float)$order_data['tranche_2'], 0, '', ' ') . ' Ar
'; + + echo '
'; + echo '
L\'Acheteur

__________________
'; + echo '
Le Vendeur

__________________
'; + echo '
'; + + echo ''; // fin page-break + echo ''; + } + + // --- BON DE COMMANDE (UNE SEULE PAGE) --- + echo ''; + echo ''; + echo ''; + echo ''; + echo ""; + echo ''; + + echo '
'; + echo '
'; + echo '

' . esc($company_info['company_name']) . '

'; + echo '

NIF : ' . esc($company_info['NIF']) . '

'; + echo '

STAT : ' . esc($company_info['STAT']) . '

'; + echo '

Contact : ' . esc($company_info['phone']) . ' | ' . esc($company_info['phone2']) . '

'; + echo '
'; + echo '
'; + echo 'Logo'; + echo '

Bon de Commande N° ' . esc($order_data['bill_no']) . '

'; + echo '
'; + echo '
'; + + echo '
'; + echo '

Client : ' . esc($order_data['customer_name']) . '

'; + echo '

Adresse : ' . esc($order_data['customer_address']) . '

'; + echo '

Téléphone : ' . esc($order_data['customer_phone']) . '

'; + echo '

CIN : ' . esc($order_data['customer_cin']) . '

'; + echo '
'; + + // ✅ TABLEAU RÉCAPITULATIF SELON LE TYPE + if ($isAvanceMere) { + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + + foreach ($items as $item) { + $details = $this->getOrderItemDetails($item); + if (!$details) continue; + + $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
DésignationProduit à compléterPrix (Ar)
' . esc($details['product_name']) . '                    ' . number_format($prixAffiche, 0, '', ' ') . '
'; + } else { echo ''; - echo ''; + echo ''; echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; + + foreach ($items as $item) { + $details = $this->getOrderItemDetails($item); + if (!$details) continue; + + $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + + // Catégorie + $categoryName = 'Non définie'; + if ($details['type'] === 'terre' && !empty($item['product_id'])) { + $p = $Products->getProductData($item['product_id']); + if (!empty($p['categorie_id'])) { + $categoryData = $Category->find($p['categorie_id']); + if ($categoryData) $categoryName = $categoryData['name']; + } + } + + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + } + + echo '
NomMarqueCatégorieN° MoteurChâssisPuissance (CC)Prix Unit. (Ar)NomMarqueCatégorieN° MoteurChâssisPuissancePrix (Ar)
' . esc($p['name']) . '' . esc($brandName) . '' . esc($categoryName) . '' . esc($p['numero_de_moteur']) . '' . esc($p['chasis'] ?? '') . '' . esc($p['puissance']) . '' . number_format($amount, 0, '', ' ') . '
' . esc($details['product_name']) . '' . esc($details['marque']) . '' . esc($categoryName) . '' . esc($details['numero_moteur']) . '' . esc($details['numero_chassis']) . '' . esc($details['puissance']) . '' . number_format($prixAffiche, 0, '', ' ') . '
'; } - $tva = $total_ht * 0.2; - $total_ttc = $total_ht + $tva - (float) $order_data['discount']; - echo ''; echo ''; - echo ''; + echo ''; echo ''; - echo ''; - echo ''; + echo ''; + echo ''; echo '
Total HT :' . number_format($total_ht, 0, '', ' ') . ' Ar
Total HT :' . number_format($totalHT, 0, '', ' ') . ' Ar
TVA :' . number_format($tva, 0, '', ' ') . ' Ar
Réduction :' . number_format($order_data['discount'], 0, '', ' ') . ' Ar
Total TTC :' . number_format($total_ttc, 0, '', ' ') . ' Ar
Réduction :' . number_format($discount, 0, '', ' ') . ' Ar
Total TTC :' . number_format($totalTTC, 0, '', ' ') . ' Ar
'; echo '
'; @@ -2093,7 +2481,7 @@ public function print31(int $id) echo '
'; // --- CONDITIONS GÉNÉRALES --- - echo '
'; + echo '
'; echo '
'; echo '

Conditions Générales

'; echo 'Logo'; @@ -2108,7 +2496,6 @@ public function print31(int $id) echo '
L\'Acheteur
'; echo '
'; - echo '
'; echo ''; } diff --git a/app/Controllers/ProductCOntroller.php b/app/Controllers/ProductCOntroller.php index bf3d56db..3727e403 100644 --- a/app/Controllers/ProductCOntroller.php +++ b/app/Controllers/ProductCOntroller.php @@ -82,7 +82,15 @@ class ProductCOntroller extends AdminController return "$name"; } - $data = $Products->getProductData(); + // ✅ Utiliser la nouvelle méthode qui filtre automatiquement par rôle et store + $data = $Products->getProductDataByRole(); + + // ✅ Debug : Logs pour vérifier le filtrage + $session = session(); + $user = $session->get('user'); + log_message('debug', '=== fetchProductData ==='); + log_message('debug', 'User: ' . ($user['username'] ?? 'N/A') . ' (Group: ' . ($user['group_name'] ?? 'N/A') . ', Store: ' . ($user['store_id'] ?? 'N/A') . ')'); + log_message('debug', 'Products found: ' . count($data)); foreach ($data as $key => $value) { // Gestion du nom du magasin @@ -139,7 +147,7 @@ class ProductCOntroller extends AdminController '
Aucune image
'; $result['data'][$key] = [ - $imageHtml, // Correction : utiliser $imageHtml au lieu de $value['image'] + $imageHtml, convertString($value['sku']), $value['name'], $value['prix_vente'], diff --git a/app/Helpers/alerts_helper.php b/app/Helpers/alerts_helper.php index f8f87a2b..0c494223 100644 --- a/app/Helpers/alerts_helper.php +++ b/app/Helpers/alerts_helper.php @@ -4,13 +4,14 @@ use App\Models\Users; use App\Models\Avance; use App\Models\AlertMail; +/** + * Vérifier les deadlines et envoyer des alertes email + */ function checkDeadlineAlerts() { log_message('info', "=== DÉBUT checkDeadlineAlerts ==="); $cacheFile = WRITEPATH . 'cache/check_deadline_last_run.txt'; - - // On enlève la vérification de 24h pour s'assurer que le script tourne quotidiennement file_put_contents($cacheFile, time()); $avanceModel = new Avance(); @@ -20,15 +21,16 @@ function checkDeadlineAlerts() $today = date('Y-m-d'); log_message('info', "Date du jour: {$today}"); - // Modification pour vérifier les avances dans 0-3 jours + // Récupération des avances dans 0-3 jours $avances = $avanceModel - ->where('DATE(deadline) >=', $today) // Inclut le jour même + ->where('DATE(deadline) >=', $today) ->where('DATE(deadline) <=', date('Y-m-d', strtotime('+3 days'))) ->where('active', 1) ->findAll(); log_message('info', "Nombre d'avances trouvées (0-3 jours): " . count($avances)); + // Récupération des utilisateurs DAF $users = $usersModel->select('users.email, users.firstname, users.lastname') ->join('user_group', 'user_group.user_id = users.id') ->join('groups', 'groups.id = user_group.group_id') @@ -56,7 +58,7 @@ function checkDeadlineAlerts() log_message('info', "Avance ID: {$avance['avance_id']}, Deadline: {$deadline}, Jours restants: {$daysLeft}"); - // Modification des types d'alerte pour 0, 1, 2, 3 jours + // Détermination du type d'alerte $alertType = match($daysLeft) { 3 => 'deadline_3_days', 2 => 'deadline_2_days', @@ -83,31 +85,35 @@ function checkDeadlineAlerts() continue; } - // Message modifié pour inclure le cas du jour même + // Construction du message $urgencyText = $daysLeft === 0 ? "ÉCHÉANCE AUJOURD'HUI" : "{$daysLeft} jour(s) restant(s)"; $message = " -

⚠️ URGENT : Avance approchant de la deadline

-

ID Avance : {$avance['avance_id']}

-

Client : {$avance['customer_name']}

-

Montant avance : " . number_format($avance['avance_amount'], 0, ',', ' ') . " Ar

-

Montant dû : " . number_format($avance['amount_due'], 0, ',', ' ') . " Ar

-

Deadline : {$deadline}

-

Statut : {$urgencyText}

-

Téléphone client : {$avance['customer_phone']}

-

Adresse client : {$avance['customer_address']}

-
-

Cette avance " . ($daysLeft === 0 ? "arrive à échéance aujourd'hui" : "arrivera à échéance dans {$daysLeft} jour(s)") . ". Action requise immédiatement.

+ + +

⚠️ URGENT : Avance approchant de la deadline

+

ID Avance : {$avance['avance_id']}

+

Client : {$avance['customer_name']}

+

Montant avance : " . number_format($avance['avance_amount'], 0, ',', ' ') . " Ar

+

Montant dû : " . number_format($avance['amount_due'], 0, ',', ' ') . " Ar

+

Deadline : {$deadline}

+

Statut : {$urgencyText}

+

Téléphone client : {$avance['customer_phone']}

+

Adresse client : {$avance['customer_address']}

+
+

Cette avance " . ($daysLeft === 0 ? "arrive à échéance aujourd'hui" : "arrivera à échéance dans {$daysLeft} jour(s)") . ". Action requise immédiatement.

+ + "; $emailsSent = 0; + $subject = $daysLeft === 0 + ? "⚠️ AVANCE URGENTE - ÉCHÉANCE AUJOURD'HUI" + : "⚠️ AVANCE URGENTE - {$daysLeft} jour(s) restant(s)"; + foreach ($emails as $to) { log_message('info', "Tentative d'envoi email à: {$to}"); - $subject = $daysLeft === 0 - ? "⚠️ AVANCE URGENTE - ÉCHÉANCE AUJOURD'HUI" - : "⚠️ AVANCE URGENTE - {$daysLeft} jour(s) restant(s)"; - - if (sendEmailInBackground($to, $subject, $message)) { + if (sendEmailWithBrevo($to, $subject, $message)) { $emailsSent++; log_message('info', "Email envoyé avec succès à: {$to}"); } else { @@ -115,6 +121,7 @@ function checkDeadlineAlerts() } } + // Enregistrement de l'alerte si au moins un email a été envoyé if ($emailsSent > 0) { log_message('info', "Insertion alerte pour avance_id={$avance['avance_id']} avec type {$alertType}"); $alertMailModel->insert([ @@ -128,49 +135,182 @@ function checkDeadlineAlerts() log_message('error', "Aucun email envoyé pour avance_id={$avance['avance_id']} avec type {$alertType}"); } } - + checkAndConvertCompletedAvances(); + log_message('info', "=== FIN checkDeadlineAlerts ==="); + + // ✅ NOUVELLE FONCTIONNALITÉ : Gérer les avances expirées + handleExpiredAvances(); +} + +/** + * ✅ NOUVELLE FONCTION : Gérer automatiquement les avances expirées + * - Libérer les produits (remettre en stock) + * - Désactiver les avances + */ +function handleExpiredAvances() +{ + log_message('info', "=== DÉBUT handleExpiredAvances ==="); + + $avanceModel = new Avance(); + $productsModel = new \App\Models\Products(); + + $today = date('Y-m-d'); + log_message('info', "Vérification des avances expirées au: {$today}"); + + // Récupérer les avances expirées et encore actives + $expiredAvances = $avanceModel + ->where('DATE(deadline) <', $today) + ->where('active', 1) + ->where('is_order', 0) + ->findAll(); + + log_message('info', "Nombre d'avances expirées trouvées: " . count($expiredAvances)); + + if (empty($expiredAvances)) { + log_message('info', "Aucune avance expirée à traiter"); + log_message('info', "=== FIN handleExpiredAvances ==="); + return; + } + + $processedCount = 0; + $errorCount = 0; + + foreach ($expiredAvances as $avance) { + try { + log_message('info', "Traitement avance expirée ID: {$avance['avance_id']}, Client: {$avance['customer_name']}, Deadline: {$avance['deadline']}"); + + // ✅ Désactiver l'avance + $updateResult = $avanceModel->update($avance['avance_id'], ['active' => 0]); + + if (!$updateResult) { + log_message('error', "Échec désactivation avance ID: {$avance['avance_id']}"); + $errorCount++; + continue; + } + + log_message('info', "Avance ID {$avance['avance_id']} désactivée avec succès"); + + // ✅ Libérer le produit UNIQUEMENT pour les avances "sur terre" avec product_id + if ($avance['type_avance'] === 'terre' && !empty($avance['product_id'])) { + $productUpdateResult = $productsModel->update($avance['product_id'], ['product_sold' => 0]); + + if ($productUpdateResult) { + log_message('info', "Produit ID {$avance['product_id']} libéré (remis en stock)"); + } else { + log_message('warning', "Échec libération produit ID: {$avance['product_id']}"); + } + } elseif ($avance['type_avance'] === 'mere') { + log_message('info', "Avance 'sur mer' - Pas de produit à libérer (product_name: {$avance['product_name']})"); + } else { + log_message('info', "Pas de product_id à libérer pour avance ID: {$avance['avance_id']}"); + } + + $processedCount++; + + } catch (\Exception $e) { + log_message('error', "Erreur traitement avance expirée ID {$avance['avance_id']}: " . $e->getMessage()); + $errorCount++; + } + } + + log_message('info', "Avances expirées traitées: {$processedCount}, Erreurs: {$errorCount}"); + log_message('info', "=== FIN handleExpiredAvances ==="); +} + +/** + * ✅ Vérifier et convertir automatiquement les avances complètes en commandes + * À appeler dans checkDeadlineAlerts() ou via un cron job + */ +function checkAndConvertCompletedAvances() +{ + log_message('info', "=== DÉBUT checkAndConvertCompletedAvances ==="); + + $avanceModel = new \App\Models\Avance(); + + // Récupérer toutes les avances complètes non encore converties + $completedAvances = $avanceModel + ->where('amount_due', 0) + ->where('is_order', 0) // Pas encore converties + ->where('active', 1) // Encore actives + ->findAll(); + + log_message('info', "Avances complètes trouvées : " . count($completedAvances)); + + $convertedCount = 0; + $errorCount = 0; + + foreach ($completedAvances as $avance) { + log_message('info', "Traitement avance complète ID: {$avance['avance_id']}, Client: {$avance['customer_name']}"); + + $order_id = $avanceModel->convertToOrder($avance['avance_id']); + + if ($order_id) { + $convertedCount++; + log_message('info', "✅ Avance {$avance['avance_id']} convertie en commande {$order_id}"); + } else { + $errorCount++; + log_message('error', "❌ Échec conversion avance {$avance['avance_id']}"); + } + } + + log_message('info', "Avances converties : {$convertedCount}, Erreurs : {$errorCount}"); + log_message('info', "=== FIN checkAndConvertCompletedAvances ==="); } -function sendEmailInBackground($to, $subject, $message) + +/** + * Envoyer un email via Brevo + */ +function sendEmailWithBrevo($to, $subject, $message) { try { - log_message('info', "Préparation envoi email à: {$to}"); + log_message('info', "Préparation envoi email via Brevo à: {$to}"); $email = \Config\Services::email(); + // Configuration Brevo depuis le fichier .env $config = [ - 'protocol' => 'smtp', - 'SMTPHost' => 'smtp.gmail.com', - 'SMTPUser' => 'rey342505@gmail.com', - 'SMTPPass' => 'loirqovmfuxnasrm', - 'SMTPPort' => 587, - 'SMTPCrypto' => 'tls', - 'mailType' => 'html', - 'charset' => 'utf-8', - 'newline' => "\r\n" + 'protocol' => env('email.protocol', 'smtp'), + 'SMTPHost' => env('email.SMTPHost', 'smtp-relay.brevo.com'), + 'SMTPUser' => env('email.SMTPUser'), + 'SMTPPass' => env('email.SMTPPass'), + 'SMTPPort' => env('email.SMTPPort', 587), + 'SMTPCrypto' => env('email.SMTPCrypto', 'tls'), + 'mailType' => 'html', + 'charset' => 'utf-8', + 'newline' => "\r\n", + 'wordWrap' => true, + 'validation' => true ]; + log_message('info', "Configuration Brevo - Host: {$config['SMTPHost']}, Port: {$config['SMTPPort']}, User: {$config['SMTPUser']}"); + $email->initialize($config); - $email->setFrom('rey342505@gmail.com', 'Système Motorbike - Alertes Avances'); + // Utilisation de l'email configuré dans .env + $fromEmail = env('email.fromEmail', 'noreply@motorbike.mg'); + $fromName = env('email.fromName', 'Système Motorbike - Alertes'); + + $email->setFrom($fromEmail, $fromName); $email->setTo($to); $email->setSubject($subject); $email->setMessage($message); - log_message('info', "Configuration email terminée, tentative d'envoi..."); + log_message('info', "Configuration email Brevo terminée, tentative d'envoi..."); if (!$email->send()) { - $debugInfo = $email->printDebugger(['headers']); - log_message('error', "Erreur email à {$to}: " . print_r($debugInfo, true)); + $debugInfo = $email->printDebugger(['headers', 'subject', 'body']); + log_message('error', "Erreur email Brevo à {$to}: " . print_r($debugInfo, true)); return false; } - log_message('info', "Email envoyé avec succès à: {$to}"); + log_message('info', "Email envoyé avec succès via Brevo à: {$to}"); return true; } catch (\Exception $e) { - log_message('error', "Exception email à {$to}: " . $e->getMessage()); + log_message('error', "Exception email Brevo à {$to}: " . $e->getMessage()); + log_message('error', "Stack trace: " . $e->getTraceAsString()); return false; } } \ No newline at end of file diff --git a/app/Models/Avance.php b/app/Models/Avance.php index c842b481..fc848d54 100644 --- a/app/Models/Avance.php +++ b/app/Models/Avance.php @@ -11,17 +11,15 @@ class Avance extends Model { 'avance_amount', 'avance_date','user_id', 'customer_name', 'customer_address', 'customer_phone', 'customer_cin', 'gross_amount','amount_due','product_id','is_order','active','store_id', - 'type_avance','type_payment', 'deadline','commentaire','product_name' // Ajout du champ type et deadline + 'type_avance','type_payment', 'deadline','commentaire','product_name' ]; public function createAvance(array $data) { try { - // Si la date de création n'est pas définie, on prend aujourd'hui if (empty($data['avance_date'])) { $data['avance_date'] = date('Y-m-d'); } - // Calcul de la deadline en fonction du type if (!empty($data['type'])) { if (strtolower($data['type']) === 'avance sur terre') { $data['deadline'] = date('Y-m-d', strtotime($data['avance_date'] . ' +15 days')); @@ -43,7 +41,6 @@ class Avance extends Model { return false; } try { - // Recalcul de la deadline si le type change if (!empty($data['type']) && !empty($data['avance_date'])) { if (strtolower($data['type']) === 'avance sur terre') { $data['deadline'] = date('Y-m-d', strtotime($data['avance_date'] . ' +15 days')); @@ -58,7 +55,6 @@ class Avance extends Model { } } - // 📌 Le reste de tes fonctions restent inchangées public function getAllAvanceData(int $id=null) { $session = session(); $users = $session->get('user'); @@ -115,48 +111,47 @@ class Avance extends Model { } public function fetchSingleAvance(int $avance_id){ - return $this->where('avance_id',$avance_id)->first(); + return $this->select('avances.*, products.name as product_name_db, products.prix_vente as product_price') + ->join('products', 'products.id = avances.product_id', 'left') + ->where('avances.avance_id', $avance_id) + ->first(); } public function removeAvance(int $avance_id){ return $this->delete($avance_id); } + // ✅ CORRECTION : getTotalAvance pour la caissière public function getTotalAvance() { $session = session(); - $users = $session->get('user'); + $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); - if($isAdmin) { - try { - return $this->select('SUM(avance_amount) AS ta') - ->where('is_order', 0) - ->get() - ->getRowObject(); - } catch (\Exception $e) { - log_message('error', 'Erreur lors du total du montant des avances : ' . $e->getMessage()); - return false; - } - } else { - try { - return $this->select('SUM(avance_amount) AS ta') - ->where('is_order', 0) - ->where('store_id',$users['store_id']) - ->get() - ->getRowObject(); - } catch (\Exception $e) { - log_message('error', 'Erreur lors du total du montant des avances : ' . $e->getMessage()); - return false; + + try { + $builder = $this->select('SUM(avance_amount) AS ta') + ->where('is_order', 0) + ->where('active', 1); // ✅ Ajout du filtre active + + if (!$isAdmin) { + $builder->where('store_id', $users['store_id']); // ✅ Filtre par store pour caissière } + + return $builder->get()->getRowObject(); + } catch (\Exception $e) { + log_message('error', 'Erreur lors du total du montant des avances : ' . $e->getMessage()); + return (object) ['ta' => 0]; // ✅ Retourner un objet avec ta = 0 en cas d'erreur } } + + // ✅ CORRECTION PRINCIPALE : getPaymentModesAvance pour la caissière public function getPaymentModesAvance() { $session = session(); $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); - if ($isAdmin) { - return $this->db->table('avances') + try { + $builder = $this->db->table('avances') ->select(' SUM(avance_amount) AS total, SUM(CASE WHEN LOWER(type_payment) = "mvola" THEN avance_amount ELSE 0 END) AS total_mvola, @@ -164,9 +159,29 @@ class Avance extends Model { SUM(CASE WHEN LOWER(type_payment) = "virement bancaire" THEN avance_amount ELSE 0 END) AS total_virement_bancaire ') ->where('active', 1) - ->get() - ->getRowObject(); - } else { + ->where('is_order', 0); // ✅ Exclure les avances devenues orders + + // ✅ CORRECTION : Ajouter le filtre store_id pour la caissière + if (!$isAdmin) { + $builder->where('store_id', $users['store_id']); + } + + $result = $builder->get()->getRowObject(); + + // ✅ Gérer le cas où il n'y a pas de résultats + if (!$result) { + return (object) [ + 'total' => 0, + 'total_mvola' => 0, + 'total_espece' => 0, + 'total_virement_bancaire' => 0 + ]; + } + + return $result; + + } catch (\Exception $e) { + log_message('error', 'Erreur getPaymentModesAvance: ' . $e->getMessage()); return (object) [ 'total' => 0, 'total_mvola' => 0, @@ -176,8 +191,6 @@ class Avance extends Model { } } - - public function getAllAvanceData1(int $id=null) { $session = session(); $users = $session->get('user'); @@ -221,7 +234,9 @@ class Avance extends Model { } try { return $this - ->where('is_order',0) + ->where('is_order',1) // ✅ Correction: devrait être 1, pas 0 + ->where('active',1) // ✅ Ajout du filtre active + ->where('store_id',$users['store_id']) // ✅ Ajout du filtre store ->orderBy('avance_date', 'DESC') ->findAll(); } catch (\Exception $e) { @@ -297,70 +312,343 @@ class Avance extends Model { foreach ($avances as $avance) { $this->update($avance['avance_id'], ['active' => '0']); - $productModel->update($avance['product_id'], ['product_sold' => 0]); + if (!empty($avance['product_id'])) { // ✅ Vérifier que product_id existe + $productModel->update($avance['product_id'], ['product_sold' => 0]); + } } } } - /** - * Récupérer les avances qui arrivent à échéance dans X jours - */ -public function getAvancesNearDeadline($days = 3) -{ - $alertDate = date('Y-m-d', strtotime("+{$days} days")); - - return $this->select('avances.*, users.store_id') - ->join('users', 'users.id = avances.user_id') - ->where('avances.is_order', 0) - ->where('avances.active', 1) - ->where('avances.amount_due >', 0) - ->where('DATE(avances.deadline)', $alertDate) - ->findAll(); -} -// Avances incomplètes (reste à payer > 0 et non transformées en commande) -public function getIncompleteAvances(int $id = null) -{ - $session = session(); - $users = $session->get('user'); - $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); - $builder = $this->where('is_order', 0) - ->where('active', 1) - ->where('amount_due >', 0); + public function getAvancesNearDeadline($days = 3) + { + $alertDate = date('Y-m-d', strtotime("+{$days} days")); + + return $this->select('avances.*, users.store_id') + ->join('users', 'users.id = avances.user_id') + ->where('avances.is_order', 0) + ->where('avances.active', 1) + ->where('avances.amount_due >', 0) + ->where('DATE(avances.deadline)', $alertDate) + ->findAll(); + } - if (!$isAdmin) { - $builder->where('store_id', $users['store_id']); + public function getIncompleteAvances(int $id = null) + { + $session = session(); + $users = $session->get('user'); + $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); + + $builder = $this->where('is_order', 0) + ->where('active', 1) + ->where('amount_due >', 0); + + if (!$isAdmin) { + $builder->where('store_id', $users['store_id']); + } + + if ($id) { + $builder->where('user_id', $id); + } + + return $builder->orderBy('avance_date', 'DESC')->findAll(); } - if ($id) { - $builder->where('user_id', $id); + public function getCompletedAvances(int $id = null) + { + $session = session(); + $users = $session->get('user'); + $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); + + $builder = $this->where('is_order', 0) + ->where('active', 1) + ->where('amount_due', 0); + + if (!$isAdmin) { + $builder->where('store_id', $users['store_id']); + } + + if ($id) { + $builder->where('user_id', $id); + } + + return $builder->orderBy('avance_date', 'DESC')->findAll(); } - return $builder->orderBy('avance_date', 'DESC')->findAll(); +public function markAsPrinted($avance_id) +{ + try { + return $this->update($avance_id, [ + 'is_printed' => 1 + ]); + } catch (\Exception $e) { + log_message('error', 'Erreur markAsPrinted: ' . $e->getMessage()); + return false; + } } -// Avances complètes (reste à payer = 0 et non transformées en commande) -public function getCompletedAvances(int $id = null) +/** + * Marquer une avance comme non imprimée (quand elle est modifiée) + */ +public function markAsNotPrinted($avance_id) { - $session = session(); - $users = $session->get('user'); - $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); - - $builder = $this->where('is_order', 0) - ->where('active', 1) - ->where('amount_due', 0); + try { + return $this->update($avance_id, [ + 'is_printed' => 0, + 'last_modified_at' => date('Y-m-d H:i:s') + ]); + } catch (\Exception $e) { + log_message('error', 'Erreur markAsNotPrinted: ' . $e->getMessage()); + return false; + } +} - if (!$isAdmin) { - $builder->where('store_id', $users['store_id']); +/** + * Vérifier si une avance a déjà été imprimée + */ +public function isPrinted($avance_id) +{ + try { + $avance = $this->find($avance_id); + return $avance ? (bool)$avance['is_printed'] : false; + } catch (\Exception $e) { + log_message('error', 'Erreur isPrinted: ' . $e->getMessage()); + return false; } +} +/** + * Récupérer un produit avec le nom de sa marque + * @param int $product_id + * @return array|null + */ +public function getProductWithBrand($product_id) +{ + try { + return $this->select('products.*, brands.name as brand_name') + ->join('brands', 'brands.id = products.marque', 'left') + ->where('products.id', $product_id) + ->first(); + } catch (\Exception $e) { + log_message('error', 'Erreur getProductWithBrand: ' . $e->getMessage()); + return null; + } +} +// À ajouter dans App\Models\Avance.php - if ($id) { - $builder->where('user_id', $id); +/** + * ✅ Convertir une avance complète en commande + * Appelé automatiquement quand amount_due atteint 0 + * + * @param int $avance_id + * @return int|false ID de la commande créée ou false + */ +public function convertToOrder(int $avance_id) +{ + try { + $avance = $this->find($avance_id); + + if (!$avance) { + log_message('error', "Avance introuvable : {$avance_id}"); + return false; + } + + // ✅ MODIFICATION PRINCIPALE : Vérifier que c'est bien une avance sur TERRE + if ($avance['type_avance'] !== 'terre') { + log_message('info', "Avance {$avance_id} de type '{$avance['type_avance']}' - Conversion ignorée (seules les avances TERRE sont converties)"); + return false; + } + + // ✅ Vérifier que l'avance est bien complète + if ((float)$avance['amount_due'] > 0) { + log_message('warning', "Avance TERRE {$avance_id} non complète (amount_due={$avance['amount_due']})"); + return false; + } + + // ✅ Vérifier qu'elle n'a pas déjà été convertie + if ((int)$avance['is_order'] === 1) { + log_message('info', "Avance TERRE {$avance_id} déjà convertie en commande"); + return false; + } + + // ✅ Vérifier que le produit existe (obligatoire pour avance TERRE) + if (empty($avance['product_id'])) { + log_message('error', "Avance TERRE {$avance_id} sans product_id - Impossible de convertir"); + return false; + } + + $db = \Config\Database::connect(); + $db->transStart(); + + // ✅ 1. Créer la commande + $orderModel = new \App\Models\Orders(); + $bill_no = 'BILAV-' . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4)); + + $orderData = [ + 'bill_no' => $bill_no, + 'customer_name' => $avance['customer_name'], + 'customer_address' => $avance['customer_address'], + 'customer_phone' => $avance['customer_phone'], + 'customer_cin' => $avance['customer_cin'], + 'date_time' => date('Y-m-d H:i:s'), + 'gross_amount' => $avance['gross_amount'], + 'net_amount' => $avance['gross_amount'], + 'discount' => 0, + 'paid_status' => 2, // En attente validation caissière + 'user_id' => $avance['user_id'], + 'store_id' => $avance['store_id'], + 'service_charge_rate' => 0, + 'vat_charge_rate' => 0, + 'vat_charge' => 0, + 'tranche_1' => $avance['avance_amount'], + 'tranche_2' => null, + 'order_payment_mode' => $avance['type_payment'] ?? 'En espèce', + 'order_payment_mode_1' => null, + ]; + + $order_id = $orderModel->insert($orderData); + + if (!$order_id) { + throw new \Exception("Échec création commande pour avance TERRE {$avance_id}"); + } + + log_message('info', "✅ Commande {$bill_no} créée depuis avance TERRE {$avance_id}"); + + // ✅ 2. Créer les items de commande + $orderItemModel = new \App\Models\OrderItems(); + $productModel = new \App\Models\Products(); + + $product = $productModel->find($avance['product_id']); + + if ($product) { + $orderItemModel->insert([ + 'order_id' => $order_id, + 'product_id' => $avance['product_id'], + 'rate' => $avance['gross_amount'], + 'qty' => 1, + 'amount' => $avance['gross_amount'], + ]); + + log_message('info', "Item ajouté : produit {$avance['product_id']} (TERRE)"); + + // ✅ Le produit reste marqué comme vendu (product_sold = 1) + // Il sera géré dans la commande maintenant + } else { + log_message('warning', "Produit {$avance['product_id']} introuvable pour avance TERRE {$avance_id}"); + } + + // ✅ 3. Marquer l'avance comme convertie + $this->update($avance_id, [ + 'is_order' => 1, + 'active' => 0, // Désactiver l'avance (elle devient commande) + ]); + + $db->transComplete(); + + if ($db->transStatus() === false) { + log_message('error', "Transaction échouée pour avance TERRE {$avance_id}"); + return false; + } + + // ✅ 4. Notification à la caissière + $notificationController = new \App\Controllers\NotificationController(); + $notificationController->createNotification( + "Nouvelle commande issue d'une avance TERRE complète - {$bill_no}", + "Caissière", + (int)$avance['store_id'], + "orders" + ); + + log_message('info', "✅ Avance TERRE {$avance_id} convertie en commande {$order_id} ({$bill_no})"); + + return $order_id; + + } catch (\Exception $e) { + log_message('error', "Erreur conversion avance TERRE→commande : " . $e->getMessage()); + return false; } +} - return $builder->orderBy('avance_date', 'DESC')->findAll(); +/** + * ✅ Hook appelé automatiquement lors du paiement d'une avance + * Intégrer ceci dans votre fonction de paiement existante + */ +public function afterPayment(int $avance_id) +{ + $avance = $this->find($avance_id); + + if (!$avance) { + return false; + } + + // ✅ Si l'avance est maintenant complète ET que c'est une avance TERRE + if ((float)$avance['amount_due'] <= 0 && $avance['type_avance'] === 'terre') { + log_message('info', "💰 Avance TERRE {$avance_id} complète ! Conversion automatique en commande..."); + return $this->convertToOrder($avance_id); + } + + // ✅ Si c'est une avance MER complète, on ne fait rien (elle reste dans la liste) + if ((float)$avance['amount_due'] <= 0 && $avance['type_avance'] === 'mere') { + log_message('info', "💰 Avance MER {$avance_id} complète ! Elle reste dans la liste des avances."); + } + + return true; } + /** + * ✅ Générer un numéro de facture unique + */ + private function generateBillNumber($store_id) + { + $db = \Config\Database::connect(); + + // Récupérer le dernier numéro pour ce store + $query = $db->query( + "SELECT bill_no FROM orders WHERE store_id = ? ORDER BY id DESC LIMIT 1", + [$store_id] + ); + + $result = $query->getRow(); + + if ($result && preg_match('/(\d+)$/', $result->bill_no, $matches)) { + $lastNumber = intval($matches[1]); + $newNumber = $lastNumber + 1; + } else { + $newNumber = 1; + } + + // Format: BILL-STORE{store_id}-{number} + return 'BILL-STORE' . $store_id . '-' . str_pad($newNumber, 5, '0', STR_PAD_LEFT); + } + /** + * ✅ Récupérer toutes les avances complètes non converties + */ + public function getCompletedNotConverted() + { + return $this->where('amount_due', 0) + ->where('is_order', 0) + ->where('active', 1) + ->where('type_avance', 'terre') // ✅ Uniquement TERRE à convertir + ->findAll(); + } -} + /** + * ✅ NOUVELLE MÉTHODE : Récupérer les avances MER complètes (pour statistiques) + */ + public function getCompletedMerAvances() + { + $session = session(); + $users = $session->get('user'); + $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); + + $builder = $this->where('amount_due', 0) + ->where('active', 1) + ->where('type_avance', 'mere'); + + if (!$isAdmin) { + $builder->where('store_id', $users['store_id']); + } + + return $builder->orderBy('avance_date', 'DESC')->findAll(); + } +} \ No newline at end of file diff --git a/app/Models/Products.php b/app/Models/Products.php index 05380e5f..0e1f3380 100644 --- a/app/Models/Products.php +++ b/app/Models/Products.php @@ -6,14 +6,46 @@ use CodeIgniter\Model; class Products extends Model { - /** - * table products - * @var string - */ protected $table = 'products'; protected $primaryKey = 'id'; protected $allowedFields = ['name', 'sku', 'price', 'product_sold', 'qty', 'image', 'description', 'numero_de_moteur', 'marque', 'chasis', 'store_id', 'availability', 'is_piece', 'prix_vente', 'date_arivage', 'puissance', 'cler', 'categorie_id', 'etats','type', 'infoManquekit', 'info', 'infoManque']; + /** + * ✅ NOUVELLE MÉTHODE : Récupérer les produits selon le rôle et le store de l'utilisateur + * @param int|null $id + * @return array|object|null + */ + public function getProductDataByRole(int $id = null) + { + $session = session(); + $user = $session->get('user'); + + // Vérifier si l'utilisateur est admin (Conseil ou Direction) + $isAdmin = in_array($user['group_name'], ['Conseil', 'Direction']); + + $builder = $this->where('is_piece', 0) + ->where('product_sold', 0); + + // ✅ Si pas admin ET a un store_id valide, filtrer par son magasin + if (!$isAdmin) { + // ✅ Si l'utilisateur n'a pas de store_id (NULL ou 0), ne retourner aucun produit + if (empty($user['store_id']) || $user['store_id'] == 0) { + // Retourner une requête impossible pour avoir 0 résultats + $builder->where('id', -1); + } else { + // Filtrer par le store_id de l'utilisateur + $builder->where('store_id', $user['store_id']); + } + } + + // Si un ID spécifique est demandé + if ($id) { + return $builder->where('id', $id)->first(); + } + + return $builder->orderBy('id', 'DESC')->findAll(); + } + /** * get the brand data * @param int $id @@ -22,7 +54,6 @@ class Products extends Model public function getProductData(int $id = null) { if ($id) { - return $this->where('id', $id)->first(); } @@ -32,17 +63,12 @@ class Products extends Model ])->orderBy('id', 'DESC')->findAll(); } - public function getProductData2(int $id) { $builder = $this->where('is_piece', 0) ->where('product_sold', 0) ->where('store_id', $id); - // if ($id != 0) { - // $builder = $builder->where('store_id', $id); - // } - return $builder->join('brands', 'brands.id = products.marque') ->orderBy('products.id', 'DESC') ->select('brands.name as brand_name,COUNT( products.id) as total_product, products.store_id as store_id,products.*') @@ -50,15 +76,12 @@ class Products extends Model ->findAll(); } - - public function getProductData3(int $id) { if ($id == 0) { return $this->where('is_piece', 0)->orderBy('id', 'DESC')->findAll(); } - // Fetch all products, ordered by ID descending return $this->where('is_piece', 0)->where('product_sold', 0)->where('store_id', $id)->orderBy('id', 'DESC')->findAll(); } @@ -80,37 +103,21 @@ class Products extends Model $builder->where("id NOT IN ($subQuery)", null, false); } - // Si on modifie et qu'on veut inclure le produit actuel dans la liste if ($currentProductId) { $builder->orWhere('id', $currentProductId); } return $builder->orderBy('id', 'DESC')->findAll(); } - - - /** - * Get active products (availability = 1) - * @return array - */ public function getActiveProductData() { return $this->where('is_piece', 0)->orderBy('id', 'DESC')->findAll(); } - /** - * Assigner un utilisateur à un magasin - * - * @param int|null $productid ID de l'utilisateur - * @param int|null $storeid ID du magasin - * @return bool Résultat de l'opération (true si success, false sinon) - */ public function assignToStore($productid = null, $storeid = null) { - // Vérifie si l'utilisateur et le magasin sont fournis if (!is_null($productid) && !is_null($storeid)) { - // Mise à jour du champ store_id pour l'utilisateur spécifié $this->db->table('products') ->where('id', $productid) ->update(['store_id' => $storeid]); @@ -118,36 +125,19 @@ class Products extends Model return true; } - // Si $userid ou $storeid est null, l'opération échoue return false; } - /** - * create new product - * @param array $data - * @return bool - */ public function create(array $data) { return $this->insert($data) ? true : false; } - /** - * update existing product - * @param array $data - * @param int $id - * @return bool - */ public function updateProduct(array $data, int $id) { return $this->update($id, $data) ? true : false; } - /** - * remove existing product - * @param int $id - * @return bool - */ public function remove(int $id) { return $this->delete($id) ? true : false; @@ -157,23 +147,18 @@ class Products extends Model { $db = \Config\Database::connect(); - // Sous-requête pour obtenir les product_id dans avances actives $subQuery = $db->table('avances') ->select('product_id') ->where('active', 1) ->where('is_order', 0) ->getCompiledSelect(); - // Compter les produits disponibles return $this->where('is_piece', 0) ->where('product_sold', 0) ->where("id NOT IN ($subQuery)", null, false) ->countAllResults(); } - - /** - * count all products including sold and reserved (méthode originale si besoin) - */ + public function countAllProductsIncludingSold() { return $this->countAll(); @@ -185,7 +170,6 @@ class Products extends Model $total = 0.0; foreach ($productIds as $id) { - // Récupère le prix du produit courant $row = $this->select('price') ->where('id', $id) ->first(); @@ -197,7 +181,6 @@ class Products extends Model return $total; } catch (\Throwable $th) { - // Loger l’erreur ici si besoin : log_message('error', $th->getMessage()); return false; } } @@ -207,12 +190,45 @@ class Products extends Model $product = $this->where('id', $id)->first(); if ($product && isset($product['name'])) { - return $product['name']; // ou un autre champ selon le vrai nom + return $product['name']; } return null; } - - + /** + * Compter les produits par store selon le rôle de l'utilisateur + * @return int + */ +public function countProductsByUserStore() +{ + $session = session(); + $user = $session->get('user'); + + // Vérifier si l'utilisateur est admin + $isAdmin = in_array($user['group_name'], ['DAF', 'Direction']); + + $db = \Config\Database::connect(); + + // Sous-requête pour exclure les produits en avance + $subQuery = $db->table('avances') + ->select('product_id') + ->where('active', 1) + ->where('is_order', 0) + ->getCompiledSelect(); + + $builder = $this->where('is_piece', 0) + ->where('product_sold', 0) + ->where("id NOT IN ($subQuery)", null, false); + + // Si pas admin ET a un store_id valide, filtrer par son magasin + if (!$isAdmin && !empty($user['store_id']) && $user['store_id'] != 0) { + $builder->where('store_id', $user['store_id']); + } elseif (!$isAdmin) { + // Utilisateur sans store = 0 produits + return 0; + } + + return $builder->countAllResults(); +} } \ No newline at end of file diff --git a/app/Views/avances/avance.php b/app/Views/avances/avance.php index 77f0702d..4120a9f5 100644 --- a/app/Views/avances/avance.php +++ b/app/Views/avances/avance.php @@ -40,7 +40,29 @@

- + +get('user'); +$isAdmin = isset($users['group_name']) && in_array($users['group_name'], ['Conseil', 'Direction']); +$isCommerciale = isset($users['group_name']) && in_array($users['group_name'], ['COMMERCIALE']); +$isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Caissière']); +?> + + + +
+
+ +
+
+

Liste des avances

@@ -50,7 +72,7 @@ $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); $isCommerciale = in_array($users['group_name'], ['COMMERCIALE']); - $isCaissier = in_array($users['group_name'], ['Caissier']); + $isCaissier = in_array($users['group_name'], ['Caissière']); if ($isAdmin): ?> Client @@ -70,7 +92,7 @@ $users = $session->get('user'); $isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']); $isCommerciale = in_array($users['group_name'], ['COMMERCIALE']); - $isCaissier = in_array($users['group_name'], ['Caissier']); + $isCaissier = in_array($users['group_name'], ['Caissière']); if ($isCommerciale || $isCaissier): ?> # @@ -173,7 +195,7 @@
- +
@@ -181,7 +203,7 @@
- +
@@ -295,7 +317,7 @@
- +
@@ -303,7 +325,7 @@
- +
@@ -332,6 +354,37 @@ + + + + + + - - - - - - + + + diff --git a/app/Views/orders/createbyid.php b/app/Views/orders/createbyid.php index 1ae8db63..3b76e42c 100644 --- a/app/Views/orders/createbyid.php +++ b/app/Views/orders/createbyid.php @@ -1,4 +1,10 @@ +

@@ -27,21 +33,21 @@

getFlashdata('errors')): ?> - - + +

Ajouter une commande

@@ -53,7 +59,6 @@ listErrors() ?>
-
@@ -99,10 +104,10 @@
-
-
@@ -113,11 +118,8 @@ Produit - Prix unitaire Montant - @@ -134,15 +136,14 @@ + autocomplete="off" value="" min="0"> - + - + disabled autocomplete="off" min="0"> @@ -151,47 +152,40 @@ -

- +
+ disabled autocomplete="off" min="0">
- +
- -
+ +
+
- +
+ disabled autocomplete="off" min="0">
-
+ \ No newline at end of file diff --git a/app/Views/orders/edit.php b/app/Views/orders/edit.php index 7f4763d0..5a6906eb 100644 --- a/app/Views/orders/edit.php +++ b/app/Views/orders/edit.php @@ -22,26 +22,70 @@ getFlashdata('success')): ?> getFlashdata('error')): ?> -