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