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'); ?>
+
@@ -75,67 +73,77 @@
+
@@ -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')): ?>
-
+
getFlashdata('error'); ?>
@@ -35,7 +35,7 @@
-
+
@@ -65,6 +65,7 @@
+
@@ -86,7 +87,7 @@
-
+
@@ -211,10 +212,6 @@
-
-
-
-
+
\ No newline at end of file