From e9ed02267ce878025b071dfb795e88e42cf01e5c Mon Sep 17 00:00:00 2001 From: Sarobidy22 Date: Thu, 11 Sep 2025 16:45:26 +0300 Subject: [PATCH] 11092025 --- app/Config/Routes.php | 10 + app/Controllers/HistoriqueController.php | 315 ++++++++++++ app/Controllers/ProductCOntroller.php | 562 ++++++++++++---------- app/Models/Historique.php | 183 +++++++ app/Views/avances/avance.php | 1 + app/Views/commercial/addImage.php | 2 +- app/Views/dashboard.php | 6 +- app/Views/groups/edit.php | 17 +- app/Views/historique/index.php | 433 +++++++++++++++++ app/Views/products/index.php | 584 ++++++++++++----------- app/Views/templates/header.php | 2 + app/Views/templates/side_menubar.php | 25 + app/Views/users/index.php | 51 +- 13 files changed, 1613 insertions(+), 578 deletions(-) create mode 100644 app/Controllers/HistoriqueController.php create mode 100644 app/Models/Historique.php create mode 100644 app/Views/historique/index.php diff --git a/app/Config/Routes.php b/app/Config/Routes.php index d72f6c24..963d73fd 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -293,4 +293,14 @@ $routes->group('', ['filter' => 'auth'], function ($routes) { $routes->post('deleteAvance', [AvanceController::class, 'removeAvance']); $routes->post('updateAvance/(:num)', [AvanceController::class, 'updateAvance']); }); + // historique + $routes->group('historique', ['filter' => 'auth'], static function ($routes) { + $routes->get('/', 'HistoriqueController::index'); + $routes->get('fetchHistoriqueData', 'HistoriqueController::fetchHistoriqueData'); + $routes->get('export', 'HistoriqueController::export'); + $routes->get('stats', 'HistoriqueController::stats'); // <-- ici + $routes->get('getStats', 'HistoriqueController::getStats'); // reste pour AJAX + $routes->post('clean', 'HistoriqueController::clean'); + }); + }); diff --git a/app/Controllers/HistoriqueController.php b/app/Controllers/HistoriqueController.php new file mode 100644 index 00000000..0a65604d --- /dev/null +++ b/app/Controllers/HistoriqueController.php @@ -0,0 +1,315 @@ +verifyRole('viewCom'); + + $storesModel = new Stores(); + + $data['page_title'] = $this->pageTitle; + $data['stores'] = $storesModel->getActiveStore(); + + return $this->render_template('historique/index', $data); + } + + /** + * Récupérer les données pour DataTables + */ + public function fetchHistoriqueData() + { + $historiqueModel = new Historique(); + + // Récupération des paramètres envoyés par DataTables + $draw = intval($this->request->getGet('draw')); + $start = intval($this->request->getGet('start')); + $length = intval($this->request->getGet('length')); + + // Filtres personnalisés + $filters = [ + 'action' => $this->request->getGet('action'), + 'store_name' => $this->request->getGet('store_name'), + 'product_name'=> $this->request->getGet('product'), + 'sku' => $this->request->getGet('sku'), + 'date_from' => $this->request->getGet('date_from'), + 'date_to' => $this->request->getGet('date_to') + ]; + + // 1️⃣ Nombre total de lignes (sans filtre) + $recordsTotal = $historiqueModel->countAll(); + + // 2️⃣ Récupération des données filtrées + $allDataFiltered = $historiqueModel->getHistoriqueWithFilters($filters); + $recordsFiltered = count($allDataFiltered); + + // 3️⃣ Pagination + $dataPaginated = array_slice($allDataFiltered, $start, $length); + + // 4️⃣ Formatage pour DataTables + $data = []; + foreach ($dataPaginated as $row) { + $data[] = [ + date('d/m/Y H:i:s', strtotime($row['created_at'])), + $row['product_name'] ?? 'N/A', + $row['sku'] ?? 'N/A', + $row['store_name'] ?? 'N/A', + $this->getActionBadge($row['action']), + $row['description'] ?? '' + ]; + } + + // 5️⃣ Retour JSON + return $this->response->setJSON([ + 'draw' => $draw, + 'recordsTotal' => $recordsTotal, + 'recordsFiltered' => $recordsFiltered, + 'data' => $data + ]); + } + + + /** + * Historique spécifique d'un produit + */ + public function product($productId) + { + $this->verifyRole('viewCom'); + + $historiqueModel = new Historique(); + $productsModel = new Products(); + + $product = $productsModel->find($productId); + if (!$product) { + session()->setFlashdata('error', 'Produit introuvable'); + return redirect()->to('/historique'); + } + + $data['page_title'] = 'Historique - ' . $product['name']; + $data['product'] = $product; + $data['historique'] = $historiqueModel->getHistoriqueByProduct($productId); + + return $this->render_template('historique/product', $data); + } + + /** + * Enregistrer un mouvement d'entrée + */ + public function entrer() + { + if (!$this->request->isAJAX()) { + return $this->response->setStatusCode(404); + } + + $data = $this->request->getJSON(true); + + if (!isset($data['product_id']) || !isset($data['store_id'])) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Paramètres manquants.' + ]); + } + + $productsModel = new Products(); + $storesModel = new Stores(); + $historiqueModel = new Historique(); + + $product = $productsModel->find($data['product_id']); + $store = $storesModel->find($data['store_id']); + + if (!$product || !$store) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Produit ou magasin introuvable.' + ]); + } + + // Mettre à jour le produit + $updateData = [ + 'store_id' => $data['store_id'], + 'availability' => 1 + ]; + + if ($productsModel->update($data['product_id'], $updateData)) { + // Enregistrer dans l'historique + $description = "Produit ajouté au magasin " . $store['name'] . " depuis TOUS"; + $historiqueModel->logMovement( + 'products', + 'ENTRER', + $product['id'], + $product['name'], + $product['sku'], + $store['name'], + $description + ); + + return $this->response->setJSON(['success' => true]); + } + + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Erreur lors de la mise à jour.' + ]); + } + + /** + * Enregistrer un mouvement de sortie + */ + public function sortie() + { + if (!$this->request->isAJAX()) { + return $this->response->setStatusCode(404); + } + + $data = $this->request->getJSON(true); + + if (!isset($data['product_id'])) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'ID produit manquant.' + ]); + } + + $productsModel = new Products(); + $storesModel = new Stores(); + $historiqueModel = new Historique(); + + $product = $productsModel->find($data['product_id']); + + if (!$product) { + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Produit introuvable.' + ]); + } + + $currentStore = $storesModel->find($product['store_id']); + $currentStoreName = $currentStore ? $currentStore['name'] : 'TOUS'; + + // Mettre à jour le produit (retirer du magasin) + $updateData = [ + 'store_id' => 0, // TOUS + 'availability' => 0 // Non disponible + ]; + + if ($productsModel->update($data['product_id'], $updateData)) { + // Enregistrer dans l'historique + $description = "Produit retiré du magasin " . $currentStoreName . " vers TOUS"; + $historiqueModel->logMovement( + 'products', + 'SORTIE', + $product['id'], + $product['name'], + $product['sku'], + 'TOUS', + $description + ); + + return $this->response->setJSON(['success' => true]); + } + + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Erreur lors de la mise à jour.' + ]); + } + + /** + * Exporter l'historique + */ + public function export() + { + $this->verifyRole('viewCom'); + + $historiqueModel = new Historique(); + + $filters = [ + 'action' => $this->request->getGet('action'), + 'store_name' => $this->request->getGet('store_name'), // Utilise le nom du magasin + 'product_name' => $this->request->getGet('product'), + 'sku' => $this->request->getGet('sku'), + 'date_from' => $this->request->getGet('date_from'), + 'date_to' => $this->request->getGet('date_to') + ]; + + $csvData = $historiqueModel->exportHistorique($filters); + + $filename = 'historique_' . date('Y-m-d_H-i-s') . '.csv'; + + return $this->response + ->setHeader('Content-Type', 'text/csv') + ->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"') + ->setBody($csvData); + } + + /** + * Nettoyer l'historique ancien + */ + public function clean() + { + $this->verifyRole('updateCom'); + + $days = $this->request->getPost('days') ?? 365; + $historiqueModel = new Historique(); + + $deleted = $historiqueModel->cleanOldHistory($days); + + if ($deleted) { + session()->setFlashdata('success', "Historique nettoyé ($deleted entrées supprimées)"); + } else { + session()->setFlashdata('info', 'Aucune entrée à supprimer'); + } + + return redirect()->to('/historique'); + } + + /** + * Obtenir le badge HTML pour une action + */ + private function getActionBadge($action) + { + $badges = [ + 'CREATE' => 'Création', + 'UPDATE' => 'Modification', + 'DELETE' => 'Suppression', + 'ASSIGN_STORE' => 'Assignation', + 'ENTRER' => 'Entrée', + 'SORTIE' => 'Sortie', + 'IMPORT' => ' Import' + ]; + + return $badges[$action] ?? '' . $action . ''; + } + + /** + * API pour obtenir les statistiques + */ + public function getStats() + { + if (!$this->request->isAJAX()) { + return $this->response->setStatusCode(404); + } + + $historiqueModel = new Historique(); + $stats = $historiqueModel->getHistoriqueStats(); + + return $this->response->setJSON($stats); + } +} diff --git a/app/Controllers/ProductCOntroller.php b/app/Controllers/ProductCOntroller.php index 0f52a3e3..be9c2595 100644 --- a/app/Controllers/ProductCOntroller.php +++ b/app/Controllers/ProductCOntroller.php @@ -18,11 +18,9 @@ class ProductCOntroller extends AdminController public function __construct() { parent::__construct(); - // Assuming permission is being set from a session helper(['form', 'url']); } - private $pageTitle = 'Produits'; public function index() @@ -38,16 +36,13 @@ class ProductCOntroller extends AdminController public function assign_store() { - // Vérifie que la requête est bien une requête AJAX if (!$this->request->isAJAX()) { $response = Services::response(); $response->setStatusCode(404, 'Page Not Found')->send(); exit; } - // Récupère les données POST sous format JSON - $data = $this->request->getJSON(true); // Décodage en tableau associatif - + $data = $this->request->getJSON(true); if (!isset($data['product_id']) || !isset($data['store_id'])) { return $this->response->setJSON([ @@ -56,19 +51,23 @@ class ProductCOntroller extends AdminController ])->setStatusCode(400); } - $product_id = $data['product_id']; - $store_id = $data['store_id']; + $product_id = (int)$data['product_id']; + $store_id = (int)$data['store_id']; $productsModel = new Products(); - // Appeler la méthode assignToStore pour mettre à jour la base de données $result = $productsModel->assignToStore($product_id, $store_id); - // Répondre en JSON avec le résultat if ($result) { - return $this->response->setJSON(['success' => true]); + return $this->response->setJSON([ + 'success' => true, + 'message' => 'Produit assigné avec succès.' + ]); } else { - return $this->response->setJSON(['success' => false, 'message' => 'Échec de la mise à jour.']); + return $this->response->setJSON([ + 'success' => false, + 'message' => 'Échec de la mise à jour.' + ]); } } @@ -94,18 +93,17 @@ class ProductCOntroller extends AdminController $store_name = $store_info && isset($store_info['name']) ? $store_info['name'] : "Inconnu"; } - // CORRECTION: Disponibilité basée sur qty ET availability (avec conversion explicite) + // Disponibilité basée sur qty ET availability $isInStock = ((int)$value['qty'] > 0); - $isAvailable = ((int)$value['availability'] === 1); // Conversion explicite en entier et comparaison stricte + $isAvailable = ((int)$value['availability'] === 1); - // Un produit est disponible s'il est en stock ET marqué comme disponible $isProductAvailable = $isInStock && $isAvailable; $availability = $isProductAvailable ? 'En stock' : 'Rupture'; - // Construction des boutons (inchangé) + // Construction des boutons $buttons = ''; if (in_array('updateProduct', $this->permission ?? [])) { $buttons .= ''; @@ -128,10 +126,11 @@ class ProductCOntroller extends AdminController } if (in_array('assignStore', $this->permission ?? [])) { - $buttons .= - ''; + $buttons .= sprintf( + ' ', + htmlspecialchars($store_name, ENT_QUOTES), + (int)$value["id"] + ); } $imagePath = 'assets/images/product_image/' . $value['image']; @@ -139,9 +138,8 @@ class ProductCOntroller extends AdminController '' : '
Aucune image
'; - // Préparer les données pour DataTables $result['data'][$key] = [ - $value['image'], + $imageHtml, // Correction : utiliser $imageHtml au lieu de $value['image'] convertString($value['sku']), $value['name'], $value['prix_vente'], @@ -153,192 +151,203 @@ class ProductCOntroller extends AdminController return $this->response->setJSON($result); } - - -public function create() -{ - $Products = new Products(); - $Brands = new Brands(); - $Category = new Category(); - $Stores = new Stores(); - $Notification = new NotificationController(); - $this->verifyRole('createProduct'); - $data['page_title'] = $this->pageTitle; - - // Validate form inputs - $validation = \Config\Services::validation(); - $validation->setRules([ - 'nom_de_produit' => 'required', - 'marque' => 'required', - 'type' => 'required', - 'numero_de_moteur' => 'required', - 'prix' => 'required|numeric', - 'price_vente' => 'required|numeric', - 'puissance' => 'required', - 'store' => 'required', - 'availability' => 'required', - 'price_min' => 'required|numeric', - ]); - if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) { - // Conversion de la disponibilité : 1 = disponible, 2 ou autre = 0 (non disponible) - $availabilityValue = (int)$this->request->getPost('availability'); - $availability = ($availabilityValue === 1) ? 1 : 0; + public function create() + { + $Products = new Products(); + $Brands = new Brands(); + $Category = new Category(); + $Stores = new Stores(); + $Notification = new NotificationController(); + $this->verifyRole('createProduct'); + $data['page_title'] = $this->pageTitle; - $data = [ - 'name' => $this->request->getPost('nom_de_produit'), - 'sku' => $this->request->getPost('numero_de_serie'), - 'price' => $this->request->getPost('prix'), - 'qty' => 1, - 'image' => $upload_image, - 'description' => $this->request->getPost('description'), - 'numero_de_moteur' => $this->request->getPost('numero_de_moteur'), - 'marque' => $this->request->getPost('marque'), - 'chasis' => $this->request->getPost('chasis'), - 'store_id' => (int)$this->request->getPost('store'), - 'availability' => $availability, // Utilise la valeur convertie - 'prix_vente' => $this->request->getPost('price_vente'), - 'date_arivage' => $this->request->getPost('datea'), - 'puissance' => $this->request->getPost('puissance'), - 'cler' => $this->request->getPost('cler'), - 'categorie_id' => json_encode($this->request->getPost('categorie[]')), - 'etats' => $this->request->getPost('etats'), - 'infoManquekit' => $this->request->getPost('infoManquekit'), - 'info' => $this->request->getPost('info'), - 'infoManque' => $this->request->getPost('infoManque'), - 'product_sold' => $product_sold, - 'type'=> $this->request->getPost('type') - ]; - - $store_id1 = (int)$this->request->getPost('store'); - - // Insert data into the database - if ($Products->create($data)) { + $validation = \Config\Services::validation(); + $validation->setRules([ + 'nom_de_produit' => 'required', + 'marque' => 'required', + 'type' => 'required', + 'numero_de_moteur' => 'required', + 'prix' => 'required|numeric', + 'price_vente' => 'required|numeric', + 'puissance' => 'required', + 'store' => 'required', + 'availability' => 'required', + 'price_min' => 'required|numeric', + ]); + + if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) { + + $upload_image = ''; + + $file = $this->request->getFile('product_image'); + if ($file && $file->isValid() && !$file->hasMoved()) { + $uploadResult = $this->uploadImage(); + if ($uploadResult && !strpos($uploadResult, 'Error') && !strpos($uploadResult, 'No file')) { + $upload_image = $uploadResult; + } + } + + $product_sold = 0; + $availabilityValue = (int)$this->request->getPost('availability'); + $availability = ($availabilityValue === 1) ? 1 : 0; + $data = [ - 'product_id' => $Products->insertID(), - 'prix_minimal' => $this->request->getPost('price_min'), + 'name' => $this->request->getPost('nom_de_produit'), + 'sku' => $this->request->getPost('numero_de_serie'), + 'price' => $this->request->getPost('prix'), + 'qty' => 1, + 'image' => $upload_image, + 'description' => $this->request->getPost('description'), + 'numero_de_moteur' => $this->request->getPost('numero_de_moteur'), + 'marque' => $this->request->getPost('marque'), + 'chasis' => $this->request->getPost('chasis'), + 'store_id' => (int)$this->request->getPost('store'), + 'availability' => $availability, + 'prix_vente' => $this->request->getPost('price_vente'), + 'date_arivage' => $this->request->getPost('datea'), + 'puissance' => $this->request->getPost('puissance'), + 'cler' => $this->request->getPost('cler'), + 'categorie_id' => json_encode($this->request->getPost('categorie[]')), + 'etats' => $this->request->getPost('etats'), + 'infoManquekit' => $this->request->getPost('infoManquekit'), + 'info' => $this->request->getPost('info'), + 'infoManque' => $this->request->getPost('infoManque'), + 'product_sold' => $product_sold, + 'type'=> $this->request->getPost('type') ]; - $Fourchette = new FourchettePrix(); - - $Fourchette->createFourchettePrix($data); - session()->setFlashdata('success', 'Créé avec succès'); - $Notification->createNotification("Un nouveau Produit a été crée", "COMMERCIALE",$store_id1,'product/'); - return redirect()->to('/products'); + + $store_id1 = (int)$this->request->getPost('store'); + + $productId = $Products->insert($data); + if ($productId) { + $data_fourchette = [ + 'product_id' => $productId, + 'prix_minimal' => $this->request->getPost('price_min'), + ]; + $Fourchette = new FourchettePrix(); + + $Fourchette->createFourchettePrix($data_fourchette); + session()->setFlashdata('success', 'Créé avec succès'); + $Notification->createNotification("Un nouveau Produit a été crée", "COMMERCIALE",$store_id1,'product/'); + return redirect()->to('/products'); + } else { + session()->setFlashdata('errors', 'Error occurred while creating the product'); + return redirect()->to('products/create'); + } } else { - session()->setFlashdata('errors', 'Error occurred while creating the product'); - return redirect()->to('products/create'); + $data = [ + 'stores' => $Stores->getActiveStore(), + 'validation' => $validation, + 'page_title' => $this->pageTitle, + 'marque' => $Brands->getActiveBrands(), + 'categorie' => $Category->getActiveCategory(), + ]; + + return $this->render_template('products/create', $data); } - } else { - $data = [ - 'stores' => $Stores->getActiveStore(), - 'validation' => $validation, - 'page_title' => $this->pageTitle, - 'marque' => $Brands->getActiveBrands(), - 'categorie' => $Category->getActiveCategory(), - ]; - - return $this->render_template('products/create', $data); } -} + private function uploadImage() { - // Define the upload directory $uploadPath = 'assets/images/product_image'; - - // Ensure the directory exists + if (!is_dir($uploadPath)) { mkdir($uploadPath, 0777, true); } - - // Check if the file is uploaded via the form + $file = $this->request->getFile('product_image'); if ($file && $file->isValid() && !$file->hasMoved()) { - // Generate a unique file name + $allowedTypes = ['jpg', 'jpeg', 'png', 'gif', 'webp']; + $fileExtension = strtolower($file->getExtension()); + + if (!in_array($fileExtension, $allowedTypes)) { + log_message('error', 'Type de fichier non autorisé: ' . $fileExtension); + return ''; + } + $newName = uniqid() . '.' . $file->getExtension(); - - // Move the file to the target directory - $file->move($uploadPath, $newName); - - // Return the actual file name - return $newName; + + if ($file->move($uploadPath, $newName)) { + return $newName; + } else { + log_message('error', 'Erreur lors du déplacement du fichier'); + return ''; + } } - - // If an error occurs, return the error message - return $file ? $file->getErrorString() : 'No file was uploaded.'; + + return ''; } public function update(int $id) -{ - $Products = new Products(); - $Stores = new Stores(); - $Category = new Category(); - $this->verifyRole('updateProduct'); - $data['page_title'] = $this->pageTitle; - $Brands = new Brands(); + { + $Products = new Products(); + $Stores = new Stores(); + $Category = new Category(); + $this->verifyRole('updateProduct'); + $data['page_title'] = $this->pageTitle; + $Brands = new Brands(); - // Validate form inputs - $validation = \Config\Services::validation(); - $validation->setRules([ - 'nom_de_produit' => 'required', - 'marque' => 'required', - ]); + $validation = \Config\Services::validation(); + $validation->setRules([ + 'nom_de_produit' => 'required', + 'marque' => 'required', + ]); - if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) { - // Conversion de la disponibilité : 1 = disponible, 2 ou autre = 0 (non disponible) - $availabilityValue = (int)$this->request->getPost('availability'); - $availability = ($availabilityValue === 1) ? 1 : 0; + if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) { + $availabilityValue = (int)$this->request->getPost('availability'); + $availability = ($availabilityValue === 1) ? 1 : 0; - $data = [ - 'name' => $this->request->getPost('nom_de_produit'), - 'sku' => $this->request->getPost('numero_de_serie'), - 'price' => $this->request->getPost('price'), - 'qty' => 1, - 'description' => $this->request->getPost('description'), - 'numero_de_moteur' => $this->request->getPost('numero_de_moteur'), - 'marque' => $this->request->getPost('marque'), - 'chasis' => $this->request->getPost('chasis'), - 'store_id' => (int)$this->request->getPost('store'), - 'availability' => $availability, // Utilise la valeur convertie - 'prix_vente' => $this->request->getPost('price_vente'), - 'date_arivage' => $this->request->getPost('datea'), - 'puissance' => $this->request->getPost('puissance'), - 'cler' => $this->request->getPost('cler'), - 'categorie_id' => json_encode($this->request->getPost('categorie[]')), - 'etats' => $this->request->getPost('etats'), - 'infoManquekit' => $this->request->getPost('infoManquekit'), - 'info' => $this->request->getPost('info'), - 'infoManque' => $this->request->getPost('infoManque'), - 'type'=> $this->request->getPost('type'), - ]; + $data = [ + 'name' => $this->request->getPost('nom_de_produit'), + 'sku' => $this->request->getPost('numero_de_serie'), + 'price' => $this->request->getPost('price'), + 'qty' => 1, + 'description' => $this->request->getPost('description'), + 'numero_de_moteur' => $this->request->getPost('numero_de_moteur'), + 'marque' => $this->request->getPost('marque'), + 'chasis' => $this->request->getPost('chasis'), + 'store_id' => (int)$this->request->getPost('store'), + 'availability' => $availability, + 'prix_vente' => $this->request->getPost('price_vente'), + 'date_arivage' => $this->request->getPost('datea'), + 'puissance' => $this->request->getPost('puissance'), + 'cler' => $this->request->getPost('cler'), + 'categorie_id' => json_encode($this->request->getPost('categorie[]')), + 'etats' => $this->request->getPost('etats'), + 'infoManquekit' => $this->request->getPost('infoManquekit'), + 'info' => $this->request->getPost('info'), + 'infoManque' => $this->request->getPost('infoManque'), + 'type'=> $this->request->getPost('type'), + ]; - - // Check if a product image is uploaded - if ($this->request->getFile('product_image')->isValid()) { - $uploadImage = $this->uploadImage(); - $uploadData = ['image' => $uploadImage]; - $Products->update($id, $uploadData); - } + if ($this->request->getFile('product_image')->isValid()) { + $uploadImage = $this->uploadImage(); + $uploadData = ['image' => $uploadImage]; + $Products->update($id, $uploadData); + } - if ($Products->updateProduct($data, $id)) { - session()->setFlashdata('success', 'Successfully updated'); - return redirect()->to('/products'); + if ($Products->updateProduct($data, $id)) { + session()->setFlashdata('success', 'Successfully updated'); + return redirect()->to('/products'); + } else { + session()->setFlashdata('errors', 'Error occurred!!'); + return redirect()->to('/products/update/' . $id); + } } else { - session()->setFlashdata('errors', 'Error occurred!!'); - return redirect()->to('/produtcs/update/' . $id); - } - } else { - $data = [ - 'stores' => $Stores->getActiveStore(), - 'validation' => $validation, - 'page_title' => $this->pageTitle, - 'product_data' => $Products->getProductData($id), - 'categorie' => $Category->getActiveCategory(), - 'marque' => $Brands->getActiveBrands() - ]; + $data = [ + 'stores' => $Stores->getActiveStore(), + 'validation' => $validation, + 'page_title' => $this->pageTitle, + 'product_data' => $Products->getProductData($id), + 'categorie' => $Category->getActiveCategory(), + 'marque' => $Brands->getActiveBrands() + ]; - return $this->render_template('products/editbackup', $data); + return $this->render_template('products/editbackup', $data); + } } -} + public function remove() { $this->verifyRole('deleteProduct'); @@ -358,14 +367,13 @@ public function create() $response['success'] = false; $response['messages'] = "Refersh the page again!!"; } - // Return JSON response return $this->response->setJSON($response); } public function createByExcel() { $this->verifyRole("createProduct"); - + try { $file = $this->request->getFile('excel_product'); if (!$file || !$file->isValid()) { @@ -394,11 +402,9 @@ public function create() ]); } - // Récupérer les en-têtes $headers = array_shift($rows); $headers = array_map('strtolower', $headers); - // Mapping des colonnes Excel vers les champs de la base $columnMapping = [ 'n° série' => 'sku', 'marque' => 'marque', @@ -410,7 +416,7 @@ public function create() 'puissance' => 'puissance', 'clé' => 'cler', 'prix d\'achat' => 'price', - 'prix ar' => 'prix_vente', // Correction du mapping + 'prix ar' => 'prix_vente', 'catégories' => 'categorie_id', 'magasin' => 'store_id', 'disponibilité' => 'availability', @@ -424,99 +430,139 @@ public function create() $StoresModel = new Stores(); $CategoryModel = new Category(); + $db = \Config\Database::connect(); + $db->query("SET @IMPORT_MODE = 1"); + $countInserted = 0; + $errors = []; - foreach ($rows as $row) { - if (empty(array_filter($row))) continue; // Ignore les lignes vides + $db->transStart(); - $data = [ - 'is_piece' => 0, - 'product_sold' => 0, - 'qty' => 1 - ]; + try { + foreach ($rows as $rowIndex => $row) { + if (empty(array_filter($row))) continue; + + $data = [ + 'is_piece' => 0, + 'product_sold' => 0, + 'qty' => 1 + ]; - // Mapper chaque colonne - foreach ($headers as $index => $header) { - $header = trim($header); - if (isset($columnMapping[$header]) && isset($row[$index])) { - $field = $columnMapping[$header]; - $value = trim($row[$index]); - - // Traitements spécifiques pour certains champs - switch ($field) { - case 'marque': - // Chercher ou créer la marque - $brand = $BrandsModel->where('name', $value)->first(); - if (!$brand) { - $brandId = $BrandsModel->insert(['name' => $value, 'active' => 1]); - } else { - $brandId = $brand['id']; - } - $data[$field] = $brandId; - break; - - case 'store_id': - // Gestion du magasin - if ($value == 'TOUS') { - $data[$field] = 0; - } else { - $store = $StoresModel->where('name', $value)->first(); - $data[$field] = $store ? $store['id'] : 0; - } - break; - - case 'date_arivage': - // Convertir la date Excel en format MySQL - if (is_numeric($value)) { - $data[$field] = date('Y-m-d', \PhpOffice\PhpSpreadsheet\Shared\Date::excelToTimestamp($value)); - } else { - $data[$field] = date('Y-m-d', strtotime($value)); - } - break; - - case 'price': - case 'prix_vente': // Ajout de la gestion pour prix_vente - $cleanedValue = str_replace(['Ar', ' ', ','], '', $value); - $data[$field] = (float)$cleanedValue; - break; - - case 'categorie_id': - // Gestion des catégories si nécessaire - if (!empty($value)) { - $category = $CategoryModel->where('name', $value)->first(); - $data[$field] = $category ? $category['id'] : null; - } - break; - - case 'availability': - // Convertir la disponibilité en booléen - $data[$field] = (strtolower($value) == 'oui' || $value == '1') ? 1 : 0; - break; - - default: - $data[$field] = $value; + foreach ($headers as $index => $header) { + $header = trim($header); + if (isset($columnMapping[$header]) && isset($row[$index])) { + $field = $columnMapping[$header]; + $value = trim($row[$index]); + + switch ($field) { + case 'marque': + if (!empty($value)) { + $brand = $BrandsModel->where('name', $value)->first(); + if (!$brand) { + $brandId = $BrandsModel->insert(['name' => $value, 'active' => 1]); + } else { + $brandId = $brand['id']; + } + $data[$field] = $brandId; + } + break; + + case 'store_id': + if ($value == 'TOUS' || empty($value)) { + $data[$field] = 0; + } else { + $store = $StoresModel->where('name', $value)->first(); + $data[$field] = $store ? $store['id'] : 0; + } + break; + + case 'date_arivage': + if (!empty($value)) { + try { + if (is_numeric($value)) { + $data[$field] = date('Y-m-d', \PhpOffice\PhpSpreadsheet\Shared\Date::excelToTimestamp($value)); + } else { + $data[$field] = date('Y-m-d', strtotime($value)); + } + } catch (\Exception $e) { + $data[$field] = date('Y-m-d'); + } + } + break; + + case 'price': + case 'prix_vente': + if (!empty($value)) { + $cleanedValue = str_replace(['Ar', ' ', ','], '', $value); + $data[$field] = (float)$cleanedValue; + } + break; + + case 'categorie_id': + if (!empty($value)) { + $category = $CategoryModel->where('name', $value)->first(); + $data[$field] = $category ? $category['id'] : null; + } + break; + + case 'availability': + $data[$field] = (strtolower($value) == 'oui' || $value == '1') ? 1 : 0; + break; + + default: + $data[$field] = $value; + } } } - } - // Insertion - if (!empty($data['name'])) { + if (empty($data['name'])) { + $errors[] = "Ligne " . ($rowIndex + 2) . ": Nom du produit manquant"; + continue; + } + if ($ProductsModel->insert($data)) { $countInserted++; + } else { + $errors[] = "Ligne " . ($rowIndex + 2) . ": Erreur lors de l'insertion"; } } + + $db->transComplete(); + + } catch (\Exception $e) { + $db->transRollback(); + throw $e; + } finally { + $db->query("SET @IMPORT_MODE = 0"); + } + + $message = "$countInserted produits importés avec succès"; + if (!empty($errors)) { + $message .= ". Erreurs: " . implode(', ', array_slice($errors, 0, 5)); + if (count($errors) > 5) { + $message .= "... et " . (count($errors) - 5) . " autres erreurs"; + } } return $this->response->setJSON([ - 'success' => true, - 'messages' => "$countInserted produits importés avec succès" + 'success' => $countInserted > 0, + 'messages' => $message, + 'imported' => $countInserted, + 'errors' => count($errors) ]); } catch (\Exception $e) { + try { + $db = \Config\Database::connect(); + $db->query("SET @IMPORT_MODE = 0"); + } catch (\Exception $ex) { + // Ignorer les erreurs de désactivation + } + return $this->response->setJSON([ 'success' => false, 'messages' => "Erreur lors de l'import: " . $e->getMessage() ]); } } -} +} \ No newline at end of file diff --git a/app/Models/Historique.php b/app/Models/Historique.php new file mode 100644 index 00000000..dc002a6b --- /dev/null +++ b/app/Models/Historique.php @@ -0,0 +1,183 @@ +select('*') + ->orderBy('created_at', 'DESC'); + + if ($limit !== null) { + $builder->limit($limit, $offset); + } + + return $builder->get()->getResultArray(); + } + + /** + * Récupérer l'historique pour un produit spécifique + */ + public function getHistoriqueByProduct($productId) + { + return $this->where('row_id', $productId) + ->where('table_name', 'products') + ->orderBy('created_at', 'DESC') + ->findAll(); + } + + /** + * Récupérer l'historique pour un magasin spécifique + */ + public function getHistoriqueByStore($storeName) + { + return $this->where('store_name', $storeName) + ->orderBy('created_at', 'DESC') + ->findAll(); + } + + /** + * Récupérer l'historique par type d'action + */ + public function getHistoriqueByAction($action) + { + return $this->where('action', $action) + ->orderBy('created_at', 'DESC') + ->findAll(); + } + + /** + * Récupérer les statistiques d'historique + */ + public function getHistoriqueStats() + { + $stats = []; + + // Total des mouvements + $stats['total_mouvements'] = $this->countAll(); + + // Mouvements par action + $actions = ['CREATE', 'UPDATE', 'DELETE', 'ASSIGN_STORE', 'ENTRER', 'SORTIE']; + foreach ($actions as $action) { + $stats['mouvements_' . strtolower($action)] = $this->where('action', $action)->countAllResults(); + } + + // Mouvements aujourd'hui + $stats['mouvements_today'] = $this->where('DATE(created_at)', date('Y-m-d'))->countAllResults(); + + // Mouvements cette semaine + $stats['mouvements_week'] = $this->where('created_at >=', date('Y-m-d', strtotime('-7 days')))->countAllResults(); + + return $stats; + } + + /** + * Enregistrer un mouvement dans l'historique + */ + public function logMovement($tableName, $action, $rowId, $productName, $sku, $storeName, $description = null) + { + $data = [ + 'table_name' => $tableName, + 'action' => $action, + 'row_id' => $rowId, + 'product_name' => $productName, + 'sku' => $sku, + 'store_name' => $storeName, + 'description' => $description, + 'created_at' => date('Y-m-d H:i:s') + ]; + + return $this->insert($data); + } + + /** + * Nettoyer l'historique ancien (plus de X jours) + */ + public function cleanOldHistory($days = 365) + { + $cutoffDate = date('Y-m-d', strtotime("-{$days} days")); + return $this->where('created_at <', $cutoffDate)->delete(); + } + + /** + * Récupérer l'historique avec filtres + * + * @param array $filters Filtres pour la requête + * @return array + */ + public function getHistoriqueWithFilters($filters = []) + { + $builder = $this->select('*'); + + if (!empty($filters['action']) && $filters['action'] !== 'all') { + $builder->where('action', $filters['action']); + } + + if (!empty($filters['store_name']) && $filters['store_name'] !== 'all') { + $builder->where('store_name', $filters['store_name']); + } + + if (!empty($filters['product_name'])) { + $builder->like('product_name', $filters['product_name']); + } + + if (!empty($filters['sku'])) { + $builder->like('sku', $filters['sku']); + } + + if (!empty($filters['date_from'])) { + $builder->where('created_at >=', $filters['date_from'] . ' 00:00:00'); + } + + if (!empty($filters['date_to'])) { + $builder->where('created_at <=', $filters['date_to'] . ' 23:59:59'); + } + + return $builder->orderBy('created_at', 'DESC')->findAll(); + } + + /** + * Exporter l'historique en CSV + */ + public function exportHistorique($filters = []) + { + $data = $this->getHistoriqueWithFilters($filters); + + $csvData = "ID,Table,Action,ID Produit,Nom Produit,SKU,Magasin,Description,Date/Heure\n"; + + foreach ($data as $row) { + $csvData .= '"' . $row['id'] . '",'; + $csvData .= '"' . $row['table_name'] . '",'; + $csvData .= '"' . $row['action'] . '",'; + $csvData .= '"' . $row['row_id'] . '",'; + $csvData .= '"' . str_replace('"', '""', $row['product_name']) . '",'; + $csvData .= '"' . $row['sku'] . '",'; + $csvData .= '"' . $row['store_name'] . '",'; + $csvData .= '"' . str_replace('"', '""', $row['description'] ?? '') . '",'; + $csvData .= '"' . $row['created_at'] . '"' . "\n"; + } + + return $csvData; + } +} \ No newline at end of file diff --git a/app/Views/avances/avance.php b/app/Views/avances/avance.php index 6a546495..8cad673c 100644 --- a/app/Views/avances/avance.php +++ b/app/Views/avances/avance.php @@ -326,6 +326,7 @@ + \ No newline at end of file diff --git a/app/Views/products/index.php b/app/Views/products/index.php index c6f347f1..ffa0dbca 100644 --- a/app/Views/products/index.php +++ b/app/Views/products/index.php @@ -31,6 +31,7 @@ getFlashdata('error'); ?> +
@@ -49,25 +50,22 @@ > Importer un fichier -
-
+
- - -
@@ -75,67 +73,77 @@ +
@@ -157,7 +165,6 @@ -
@@ -168,7 +175,6 @@
- @@ -192,8 +198,6 @@ - - @@ -240,7 +244,7 @@ - + + + - \ No newline at end of file diff --git a/app/Views/templates/header.php b/app/Views/templates/header.php index b865402b..416b569a 100644 --- a/app/Views/templates/header.php +++ b/app/Views/templates/header.php @@ -106,6 +106,8 @@ + + diff --git a/app/Views/templates/side_menubar.php b/app/Views/templates/side_menubar.php index cffcbb22..e20a40f6 100644 --- a/app/Views/templates/side_menubar.php +++ b/app/Views/templates/side_menubar.php @@ -100,6 +100,31 @@ + + + +
  • + + + Historique + + + + + +
  • + + + + diff --git a/app/Views/users/index.php b/app/Views/users/index.php index 5243b3d5..e7a7f46b 100644 --- a/app/Views/users/index.php +++ b/app/Views/users/index.php @@ -22,7 +22,7 @@ getFlashdata('success'); ?> getFlashdata('error')): ?> - + - +