13 changed files with 1613 additions and 578 deletions
@ -0,0 +1,315 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Controllers; |
||||
|
|
||||
|
use App\Models\Historique; |
||||
|
use App\Models\Products; |
||||
|
use App\Models\Stores; |
||||
|
|
||||
|
class HistoriqueController extends AdminController |
||||
|
{ |
||||
|
private $pageTitle = 'Historique des Mouvements'; |
||||
|
|
||||
|
public function __construct() |
||||
|
{ |
||||
|
parent::__construct(); |
||||
|
helper(['form', 'url']); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Page principale de l'historique |
||||
|
*/ |
||||
|
public function index() |
||||
|
{ |
||||
|
$this->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' => '<span class="label label-success">Création</span>', |
||||
|
'UPDATE' => '<span class="label label-warning">Modification</span>', |
||||
|
'DELETE' => '<span class="label label-danger">Suppression</span>', |
||||
|
'ASSIGN_STORE' => '<span class="label label-info">Assignation</span>', |
||||
|
'ENTRER' => '<span class="label label-primary">Entrée</span>', |
||||
|
'SORTIE' => '<span class="label label-default">Sortie</span>', |
||||
|
'IMPORT' => '<span class="label label-success"><i class="fa fa-upload"></i> Import</span>' |
||||
|
]; |
||||
|
|
||||
|
return $badges[$action] ?? '<span class="label label-secondary">' . $action . '</span>'; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 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); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,183 @@ |
|||||
|
<?php |
||||
|
|
||||
|
namespace App\Models; |
||||
|
|
||||
|
use CodeIgniter\Model; |
||||
|
|
||||
|
class Historique extends Model |
||||
|
{ |
||||
|
protected $table = 'historique'; |
||||
|
protected $primaryKey = 'id'; |
||||
|
protected $allowedFields = [ |
||||
|
'table_name', |
||||
|
'action', |
||||
|
'row_id', |
||||
|
'product_name', |
||||
|
'sku', |
||||
|
'store_name', |
||||
|
'description', |
||||
|
'created_at' |
||||
|
]; |
||||
|
protected $useTimestamps = false; |
||||
|
protected $createdField = 'created_at'; |
||||
|
|
||||
|
/** |
||||
|
* Récupérer tous les historiques avec pagination |
||||
|
*/ |
||||
|
public function getHistoriqueData($limit = null, $offset = null) |
||||
|
{ |
||||
|
$builder = $this->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; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,433 @@ |
|||||
|
<div class="content-wrapper"> |
||||
|
<section class="content-header"> |
||||
|
<h1> |
||||
|
<i class="fa fa-history text-blue"></i> Gérer l' |
||||
|
<small>Historique</small> |
||||
|
</h1> |
||||
|
<ol class="breadcrumb"> |
||||
|
<li><a href="#"><i class="fa fa-dashboard"></i> Accueil</a></li> |
||||
|
<li class="active">Historique</li> |
||||
|
</ol> |
||||
|
</section> |
||||
|
|
||||
|
<section class="content"> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-12 col-xs-12"> |
||||
|
|
||||
|
<div id="messages"></div> |
||||
|
|
||||
|
<div class="box box-primary shadow-sm"> |
||||
|
<div class="box-header with-border"> |
||||
|
<h3 class="box-title"><i class="fa fa-list"></i> <?= $page_title ?></h3> |
||||
|
<div class="box-tools pull-right"> |
||||
|
<button type="button" class="btn btn-sm btn-primary" data-toggle="modal" data-target="#filterModal"> |
||||
|
<i class="fa fa-filter"></i> Filtrer |
||||
|
</button> |
||||
|
<button type="button" class="btn btn-sm btn-success" onclick="exportHistorique()"> |
||||
|
<i class="fa fa-download"></i> Exporter |
||||
|
</button> |
||||
|
<!-- <button type="button" class="btn btn-info" onclick="showStats()"> |
||||
|
<i class="fa fa-bar-chart"></i> Statistiques |
||||
|
</button> --> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="box-body"> |
||||
|
<div class="row" style="margin-bottom: 15px;"> |
||||
|
<div class="col-md-4"> |
||||
|
<div class="form-group"> |
||||
|
<label><i class="fa fa-home"></i> Sélectionner un magasin</label> |
||||
|
<select class="form-control input-sm" id="store_top_filter" name="store_id"> |
||||
|
<option value="all">Tous les magasins</option> |
||||
|
<?php foreach ($stores as $store): ?> |
||||
|
<option value="<?= $store['name'] ?>"><?= $store['name'] ?></option> |
||||
|
<?php endforeach; ?> |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="row" style="margin-bottom:10px;"> |
||||
|
<div class="col-md-12"> |
||||
|
<div class="btn-group" role="group"> |
||||
|
<button type="button" class="btn btn-default btn-sm filter-quick" data-filter="today"><i class="fa fa-calendar-day"></i> Aujourd'hui</button> |
||||
|
<button type="button" class="btn btn-default btn-sm filter-quick" data-filter="week"><i class="fa fa-calendar-week"></i> Cette semaine</button> |
||||
|
<button type="button" class="btn btn-default btn-sm filter-quick" data-filter="month"><i class="fa fa-calendar"></i> Ce mois</button> |
||||
|
<button type="button" class="btn btn-default btn-sm filter-quick active" data-filter="all"><i class="fa fa-infinity"></i> Tout</button> |
||||
|
</div> |
||||
|
<div class="btn-group pull-right" role="group"> |
||||
|
<button type="button" class="btn btn-default btn-sm action-filter active" data-action="all"><i class="fa fa-list"></i> Toutes</button> |
||||
|
<button type="button" class="btn btn-success btn-sm action-filter" data-action="CREATE"><i class="fa fa-plus-circle"></i> Création</button> |
||||
|
<button type="button" class="btn btn-warning btn-sm action-filter" data-action="UPDATE"><i class="fa fa-edit"></i> Modification</button> |
||||
|
<!-- <button type="button" class="btn btn-warning action-filter" data-action="ASSIGN_STORE">Assignation</button> --> |
||||
|
<button type="button" class="btn btn-primary btn-sm action-filter" data-action="ENTRER"><i class="fa fa-arrow-down"></i> Entrée</button> |
||||
|
<button type="button" class="btn btn-danger btn-sm action-filter" data-action="SORTIE"><i class="fa fa-arrow-up"></i> Sortie</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="box-body"> |
||||
|
<div class="table-responsive"> |
||||
|
<table id="historiqueTable" class="table table-bordered table-striped table-hover nowrap" style="width:100%;"> |
||||
|
<thead class="bg-light-blue"> |
||||
|
<tr> |
||||
|
<th>Date</th> |
||||
|
<th>Produit</th> |
||||
|
<th>SKU</th> |
||||
|
<th>Magasin</th> |
||||
|
<th>Action</th> |
||||
|
<th>Description</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody></tbody> |
||||
|
</table> |
||||
|
</div> |
||||
|
|
||||
|
<!-- Loader --> |
||||
|
<div id="loading" style="display:none;text-align:center;margin:20px;"> |
||||
|
<i class="fa fa-spinner fa-spin fa-2x text-blue"></i> |
||||
|
<p>Chargement des données...</p> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</section> |
||||
|
</div> |
||||
|
|
||||
|
<div class="modal fade" id="filterModal" tabindex="-1" role="dialog"> |
||||
|
<div class="modal-dialog" role="document"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal">×</button> |
||||
|
<h4 class="modal-title">Filtres avancés</h4> |
||||
|
</div> |
||||
|
<div class="modal-body"> |
||||
|
<form id="filterForm"> |
||||
|
<input type="hidden" id="movement_type" name="movement_type" value=""> |
||||
|
|
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="form-group"> |
||||
|
<label>Date de début</label> |
||||
|
<input type="date" class="form-control" id="date_from" name="date_from"> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="form-group"> |
||||
|
<label>Date de fin</label> |
||||
|
<input type="date" class="form-control" id="date_to" name="date_to"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="row"> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="form-group"> |
||||
|
<label>Magasin</label> |
||||
|
<select class="form-control" id="store_filter" name="store_id"> |
||||
|
<option value="all">Tous les magasins</option> |
||||
|
<option value="0">TOUS</option> |
||||
|
<?php foreach ($stores as $store): ?> |
||||
|
<option value="<?= $store['id'] ?>"><?= $store['name'] ?></option> |
||||
|
<?php endforeach; ?> |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="col-md-6"> |
||||
|
<div class="form-group"> |
||||
|
<label>Action</label> |
||||
|
<select class="form-control" id="action_filter" name="action"> |
||||
|
<option value="all">Toutes les actions</option> |
||||
|
<option value="CREATE">Création</option> |
||||
|
<option value="UPDATE">Modification</option> |
||||
|
<option value="DELETE">Suppression</option> |
||||
|
<option value="ASSIGN_STORE">Assignation</option> |
||||
|
<option value="ENTRER">Entrée</option> |
||||
|
<option value="SORTIE">Sortie</option> |
||||
|
</select> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</form> |
||||
|
</div> |
||||
|
<div class="modal-footer"> |
||||
|
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button> |
||||
|
<button type="button" class="btn btn-default" onclick="resetFilters()">Réinitialiser</button> |
||||
|
<button type="button" class="btn btn-primary" onclick="applyFilters()">Appliquer</button> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<div class="modal fade" id="statsModal" tabindex="-1" role="dialog"> |
||||
|
<div class="modal-dialog modal-lg" role="document"> |
||||
|
<div class="modal-content"> |
||||
|
<div class="modal-header"> |
||||
|
<button type="button" class="close" data-dismiss="modal">×</button> |
||||
|
<h4 class="modal-title">Statistiques de l'historique</h4> |
||||
|
</div> |
||||
|
<div class="modal-body" id="statsContent"> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
|
||||
|
<style> |
||||
|
.btn-group .btn.active { |
||||
|
font-weight: bold; |
||||
|
border: 1px solid #444; |
||||
|
} |
||||
|
.table td, .table th { |
||||
|
vertical-align: middle !important; |
||||
|
} |
||||
|
.bg-light-blue { |
||||
|
background: #3c8dbc; |
||||
|
color: white; |
||||
|
} |
||||
|
.box.shadow-sm { |
||||
|
box-shadow: 0 1px 3px rgba(0,0,0,0.12), 0 1px 2px rgba(0,0,0,0.24); |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<script> |
||||
|
// Déclaration de la variable de table en dehors du document ready pour un accès global |
||||
|
var historiqueTable; |
||||
|
|
||||
|
$(document).ready(function() { |
||||
|
|
||||
|
// On s'assure que le fichier de langue est chargé et on étend les options par défaut de DataTables. |
||||
|
// Cette configuration est copiée de votre deuxième script pour un affichage de pagination uniforme. |
||||
|
$.extend(true, $.fn.dataTable.defaults, { |
||||
|
language: { |
||||
|
sProcessing: "Traitement en cours...", |
||||
|
sSearch: "Rechercher :", |
||||
|
sLengthMenu: "Afficher _MENU_ éléments", |
||||
|
sInfo: "Affichage de l'élement _START_ à _END_ sur _TOTAL_ éléments", |
||||
|
sInfoEmpty: "Affichage de l'élement 0 à 0 sur 0 élément", |
||||
|
sInfoFiltered: "(filtré de _MAX_ éléments au total)", |
||||
|
sLoadingRecords: "Chargement en cours...", |
||||
|
sZeroRecords: "Aucun élément à afficher", |
||||
|
sEmptyTable: "Aucune donnée disponible dans le tableau", |
||||
|
oPaginate: { |
||||
|
sFirst: "Premier", |
||||
|
sPrevious: "Précédent", |
||||
|
sNext: "Suivant", |
||||
|
sLast: "Dernier" |
||||
|
}, |
||||
|
oAria: { |
||||
|
sSortAscending: ": activer pour trier la colonne par ordre croissant", |
||||
|
sSortDescending: ": activer pour trier la colonne par ordre décroissant" |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Initialisation du DataTable |
||||
|
historiqueTable = $('#historiqueTable').DataTable({ |
||||
|
"processing": true, |
||||
|
"serverSide": true, |
||||
|
"ajax": { |
||||
|
"url": "<?= base_url('historique/fetchHistoriqueData') ?>", |
||||
|
"type": "GET", |
||||
|
"data": function(d) { |
||||
|
d.store_name = $('#store_top_filter').val(); |
||||
|
d.date_from = $('#date_from').val(); |
||||
|
d.date_to = $('#date_to').val(); |
||||
|
d.action = $('#action_filter').val(); |
||||
|
d.movement_type = $('#movement_type').val(); |
||||
|
}, |
||||
|
"beforeSend": function() { $("#loading").show(); }, |
||||
|
"complete": function() { $("#loading").hide(); } |
||||
|
}, |
||||
|
"columns": [ |
||||
|
{ "data": 0, "width": "15%" }, |
||||
|
{ "data": 1, "width": "20%" }, |
||||
|
{ "data": 2, "width": "10%" }, |
||||
|
{ "data": 3, "width": "12%" }, |
||||
|
{ |
||||
|
"data": 4, |
||||
|
"width": "10%", |
||||
|
"render": function(data) { |
||||
|
let badgeClass = "badge bg-gray"; |
||||
|
if (data === "CREATE") badgeClass = "badge bg-green"; |
||||
|
else if (data === "UPDATE") badgeClass = "badge bg-yellow"; |
||||
|
else if (data === "ENTRER") badgeClass = "badge bg-blue"; |
||||
|
else if (data === "SORTIE") badgeClass = "badge bg-red"; |
||||
|
return '<span class="'+badgeClass+'">'+data+'</span>'; |
||||
|
} |
||||
|
}, |
||||
|
{ "data": 5, "width": "33%" } |
||||
|
], |
||||
|
"order": [[0, "desc"]], |
||||
|
"pageLength": 25, |
||||
|
"lengthMenu": [ |
||||
|
[5, 10, 20, 50, -1], |
||||
|
['5', '10', '20', '50', 'Tous'] |
||||
|
], |
||||
|
"dom": 'Blfrtip', |
||||
|
"searching": false, |
||||
|
"buttons": [ |
||||
|
'copy', 'csv', 'excel', 'pdf', 'print' |
||||
|
] |
||||
|
}); |
||||
|
|
||||
|
|
||||
|
// Événement pour le nouveau champ de sélection de magasin |
||||
|
$('#store_top_filter').change(function() { |
||||
|
// Met à jour la valeur du filtre avancé pour la synchronisation |
||||
|
$('#store_filter').val('all'); |
||||
|
historiqueTable.ajax.reload(); |
||||
|
}); |
||||
|
|
||||
|
// S'assurer que le filtre avancé par magasin recharge le tableau |
||||
|
$('#store_filter').change(function() { |
||||
|
historiqueTable.ajax.reload(); |
||||
|
}); |
||||
|
|
||||
|
// Gestion des filtres rapides (période) |
||||
|
$('.filter-quick').click(function() { |
||||
|
$('.filter-quick').removeClass('active'); |
||||
|
$(this).addClass('active'); |
||||
|
|
||||
|
var filter = $(this).data('filter'); |
||||
|
var today = new Date().toISOString().split('T')[0]; |
||||
|
|
||||
|
switch(filter) { |
||||
|
case 'today': |
||||
|
$('#date_from').val(today); |
||||
|
$('#date_to').val(today); |
||||
|
break; |
||||
|
case 'week': |
||||
|
var weekAgo = new Date(); |
||||
|
weekAgo.setDate(weekAgo.getDate() - 7); |
||||
|
$('#date_from').val(weekAgo.toISOString().split('T')[0]); |
||||
|
$('#date_to').val(today); |
||||
|
break; |
||||
|
case 'month': |
||||
|
var monthAgo = new Date(); |
||||
|
monthAgo.setMonth(monthAgo.getMonth() - 1); |
||||
|
$('#date_from').val(monthAgo.toISOString().split('T')[0]); |
||||
|
$('#date_to').val(today); |
||||
|
break; |
||||
|
case 'all': |
||||
|
$('#date_from').val(''); |
||||
|
$('#date_to').val(''); |
||||
|
break; |
||||
|
} |
||||
|
|
||||
|
historiqueTable.ajax.reload(); |
||||
|
}); |
||||
|
|
||||
|
// Gestion des filtres d'action rapides |
||||
|
$('.action-filter').click(function() { |
||||
|
$('.action-filter').removeClass('active'); |
||||
|
$(this).addClass('active'); |
||||
|
|
||||
|
var action = $(this).data('action'); |
||||
|
|
||||
|
if (action === 'ENTRER' || action === 'SORTIE') { |
||||
|
$('#action_filter').val(action); |
||||
|
$('#movement_type').val(action.toLowerCase()); |
||||
|
} else if (action === 'all') { |
||||
|
$('#action_filter').val('all'); |
||||
|
$('#movement_type').val(''); |
||||
|
} else { |
||||
|
$('#action_filter').val(action); |
||||
|
$('#movement_type').val(''); |
||||
|
} |
||||
|
|
||||
|
historiqueTable.ajax.reload(); |
||||
|
}); |
||||
|
}); |
||||
|
|
||||
|
function applyFilters() { |
||||
|
$('#filterModal').modal('hide'); |
||||
|
// Mettre à jour le sélecteur rapide en fonction du filtre avancé |
||||
|
var selectedStoreId = $('#store_filter').val(); |
||||
|
if (selectedStoreId === 'all') { |
||||
|
$('#store_top_filter').val('all'); |
||||
|
} else { |
||||
|
// Trouver le nom du magasin correspondant à l'ID |
||||
|
var storeName = $('#store_filter option[value="' + selectedStoreId + '"]').text(); |
||||
|
$('#store_top_filter').val(storeName); |
||||
|
} |
||||
|
historiqueTable.ajax.reload(); |
||||
|
} |
||||
|
|
||||
|
function resetFilters() { |
||||
|
$('#filterForm')[0].reset(); |
||||
|
$('#store_filter').val('all'); |
||||
|
$('#store_top_filter').val('all'); // Réinitialise aussi le champ supérieur |
||||
|
$('#action_filter').val('all'); |
||||
|
$('#movement_type').val(''); |
||||
|
$('.filter-quick, .action-filter').removeClass('active'); |
||||
|
$('.filter-quick[data-filter="all"]').addClass('active'); |
||||
|
$('.action-filter[data-action="all"]').addClass('active'); |
||||
|
historiqueTable.ajax.reload(); |
||||
|
} |
||||
|
|
||||
|
function exportHistorique() { |
||||
|
var params = new URLSearchParams(); |
||||
|
params.append('date_from', $('#date_from').val()); |
||||
|
params.append('date_to', $('#date_to').val()); |
||||
|
params.append('store_name', $('#store_top_filter').val()); // Utilise le nouveau champ |
||||
|
params.append('action', $('#action_filter').val()); |
||||
|
params.append('movement_type', $('#movement_type').val()); |
||||
|
|
||||
|
window.location.href = '<?= base_url('historique/export') ?>?' + params.toString(); |
||||
|
} |
||||
|
|
||||
|
function showStats() { |
||||
|
$.ajax({ |
||||
|
url: '<?= base_url('historique/getStats') ?>', |
||||
|
type: 'GET', |
||||
|
dataType: 'json', |
||||
|
success: function(data) { |
||||
|
var html = '<div class="row">'; |
||||
|
|
||||
|
html += '<div class="col-md-4">'; |
||||
|
html += '<div class="info-box bg-aqua">'; |
||||
|
html += '<span class="info-box-icon"><i class="fa fa-history"></i></span>'; |
||||
|
html += '<div class="info-box-content">'; |
||||
|
html += '<span class="info-box-text">Total événements</span>'; |
||||
|
html += '<span class="info-box-number">' + data.total_mouvements + '</span>'; |
||||
|
html += '</div></div></div>'; |
||||
|
|
||||
|
html += '<div class="col-md-8">'; |
||||
|
html += '<h4>Répartition par action</h4>'; |
||||
|
html += '<div class="row">'; |
||||
|
|
||||
|
var total = data.total_mouvements; |
||||
|
var actions = { |
||||
|
'CREATE': data.mouvements_create, |
||||
|
'UPDATE': data.mouvements_update, |
||||
|
'DELETE': data.mouvements_delete, |
||||
|
'ASSIGN_STORE': data.mouvements_assign_store, |
||||
|
'ENTRER': data.mouvements_entrer, |
||||
|
'SORTIE': data.mouvements_sortie, |
||||
|
}; |
||||
|
|
||||
|
for (var action in actions) { |
||||
|
var value = actions[action] || 0; |
||||
|
var percent = (total > 0) ? (value / total * 100).toFixed(2) : 0; |
||||
|
html += '<div class="col-md-6">'; |
||||
|
html += '<small>' + action + '</small>'; |
||||
|
html += '<div class="progress">'; |
||||
|
html += '<div class="progress-bar" style="width: ' + percent + '%">'; |
||||
|
html += value + ' (' + percent + '%)'; |
||||
|
html += '</div></div></div>'; |
||||
|
} |
||||
|
|
||||
|
html += '</div></div></div>'; |
||||
|
|
||||
|
$('#statsContent').html(html); |
||||
|
$('#statsModal').modal('show'); |
||||
|
}, |
||||
|
error: function(xhr, status, error) { |
||||
|
// Remplacer l'alerte par un message plus élégant |
||||
|
$('#statsContent').html('<div class="alert alert-danger" role="alert">Erreur lors du chargement des statistiques.</div>'); |
||||
|
$('#statsModal').modal('show'); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
</script> |
||||
Loading…
Reference in new issue