Compare commits

...

6 Commits

Author SHA1 Message Date
e02671e860 feat: corrections du 09-04-2026
- Page Historique : remplacement SKU par N° de série (vue + export CSV)
- Facture : colonne N° CHASSIS remplacée par N° CHASSIS / MOTEUR (affiche les deux valeurs)
- Facture : montant en lettres vérifié et fonctionnel

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-09 07:37:04 +02:00
38d37a0987 feat: modifications et corrections du 03-04-2026
- Bouton impression conditionnel : 2 boutons (Facture + BL) si 1 produit, 1 bouton (BL) si plusieurs produits
- Ajout filtres (date, point de vente, utilisateur) sur la page Rapports principale
- Ajout filtres (date, point de vente) sur la page Rapports/Stock pour les 3 tableaux
- Remplacement affichage "UGS" par "N° SERIE" dans toutes les pages
- Mise en page facture avec remise : titre FACTURE repositionné, tableau plus compact
- Correction remise commandes multi-produits : total_price recevait un tableau au lieu d'un nombre
- SuperAdmin voit toutes les remises (tous statuts) au lieu de seulement "En attente"

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 08:49:40 +02:00
fe80b9c4f8 fix: corrections et améliorations du 01-04-2026
## Recouvrement
- Liste triée par date décroissante
- Pop-up de confirmation anti-doublons
- Affichage avec permission "Voir" seule

## Mise à jour produit
- Correction erreur "Column count doesn't match" (triggers MySQL corrigés)
- Formulaire corrigé (action, catégorie, champs null)
- Catégorie et date d'arrivage correctement préremplis
- Historique affiche le nom de l'utilisateur qui modifie

## Espace commercial
- Colonne "Disponibilité" ajoutée avec statut "En attente de livraison"
- Bouton panier caché pour les motos commandées
- Surbrillance jaune pour les motos en attente

## Notifications
- Caissière reçoit les notifications via notifCommande
- notifSortieCaisse réservé à Direction/Admin

## Avances
- Colonne "N° Série" ajoutée dans toutes les listes
- Compteurs sur les boutons Incomplètes/Complètes

## Facture / BL
- Total, Remise, Total à payer affichés
- "Ariary" ne se répète plus
- Prix individuel par moto
- Impression automatique : 1 produit = Facture, 2+ = BL
- Remise multiple : colonne product changée en TEXT

## Rapports
- Filtre par date dans le rapport stock
- Filtre par commercial et mécanicien dans les performances
- Correction rapport stock (GROUP BY marque)
- Liens absolus (correction erreur 404)

## Sidebar
- Marge en haut supprimée (production)
- Padding en bas ajouté pour scroll complet

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-01 19:56:34 +02:00
a195d24e78 feat: refonte gestion commandes, sécurité, impressions et notifications
## Gestion Moto & Commandes
- Correction des notifications pour la Caissière
- Le bouton "Enregistrer" changé en "Payer"
- La moto commandée reste visible dans la liste jusqu'à livraison par la sécurité

## Espace Sécurité
- Ajout des notifications de livraison
- Transfert vers l'espace commande après livraison

## Espace SuperAdmin
- Rejet de commande : le produit redevient disponible en stock automatiquement
- Correction de la gestion des rôles (permissions inversées)
- Avance complète : s'affiche directement chez la Caissière

## Historique des Actions
- Ajout de l'historique des actions pour SuperAdmin (traçabilité)

## Dashboard
- Filtre par date ajouté (par défaut : aujourd'hui)
- Affichage uniquement des données du site concerné

## Espace Commercial
- Liste des produits disponibles sur la liste déroulante dans l'ajout des commandes
- Le bouton "+" se cache après le premier clic pour les clients particuliers

## Impression Documents
- Refonte facture, bon de livraison, facture d'acompte (QR codes, infos dynamiques)

## Sidebar
- Correction des animations et du logo dynamique

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 07:28:05 +02:00
OmegaRak
a18ccbf34b debug:
notification
ajout commande
2026-03-25 09:19:55 +01:00
83374f96c3 Merge pull request 'feat: afficher les infos complètes du moto dans les notifications de remise' (#1) from feat/notification-remise-info-moto into master
Reviewed-on: #1
2026-03-05 12:58:31 +03:00
55 changed files with 3361 additions and 2007 deletions

View File

@ -22,7 +22,6 @@ use App\Controllers\ReservationController;
use App\Controllers\SecuriteController;
use App\Controllers\SortieCaisseController;
use App\Controllers\RemiseController;
use App\Controllers\PerformanceController;
/**
* auth route
* the option array filter make a filter,
@ -50,6 +49,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
* dashboard route
*/
$routes->get('/', [Dashboard::class, 'index']);
$routes->get('/dashboard/getTresorerieData', [Dashboard::class, 'getTresorerieData']);
$routes->get('/ventes', [Auth::class, 'ventes']);
$routes->get('/ventes/(:num)', [Auth::class, 'addImage']);
$routes->get('/ventes/fetchProductVente/(:num)', [Auth::class, 'fetchProductVente']);
@ -183,7 +183,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->get('update/(:num)', [ProductCOntroller::class, 'update']);
$routes->post('update/(:num)', [ProductCOntroller::class, 'update']);
$routes->post('remove', [ProductCOntroller::class, 'remove']);
$routes->get('generateqrcode/(:num)', [QrCodeCOntroller::class, 'generate']);
$routes->get('generateqrcode/(:num)', [QrCodeController::class, 'generate']);
$routes->post('assign_store', [ProductCOntroller::class, 'assign_store']);
$routes->post('createByExcel', [ProductCOntroller::class, 'createByExcel']);
$routes->post('checkProductAvailability', [ProductCOntroller::class, 'checkProductAvailability']);
@ -226,7 +226,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->get('detail/stock', [ReportController::class, 'stockDetail']);
// Corrections fetct → fetch
$routes->get('detail/fetchData/(:num)', [ReportController::class, 'fetchProductSold/$1']);
$routes->get('detail/fetchData/(:num)', [ReportController::class, 'fetchProductSodled/$1']);
$routes->get('detail/fetchDataStock/(:num)', [ReportController::class, 'fetchProductStock/$1']);
$routes->get('detail/fetchDataStock2/(:num)', [ReportController::class, 'fetchProductStock2/$1']);
@ -389,6 +389,13 @@ $routes->group('avances', function ($routes) {
$routes->post('validateAvance', [AvanceController::class, 'validateAvance']);
});
// Historique des actions (SuperAdmin)
$routes->group('action-log', ['filter' => 'auth'], static function ($routes) {
$routes->get('/', 'ActionLogController::index');
$routes->get('fetchData', 'ActionLogController::fetchData');
$routes->get('export', 'ActionLogController::export');
});
// historique
$routes->group('historique', ['filter' => 'auth'], static function ($routes) {
$routes->get('/', 'HistoriqueController::index');

View File

@ -59,4 +59,6 @@ class View extends BaseView
* @var list<class-string<ViewDecoratorInterface>>
*/
public array $decorators = [];
public string $appOverridesFolder = APPPATH . 'Views/';
}

View File

@ -0,0 +1,154 @@
<?php
namespace App\Controllers;
use App\Models\Historique;
use App\Models\Stores;
class ActionLogController extends AdminController
{
private $pageTitle = 'Historique des Actions';
public function __construct()
{
parent::__construct();
helper(['form', 'url']);
}
public function index()
{
$session = session();
$user = $session->get('user');
if ($user['group_name'] !== 'SuperAdmin') {
return redirect()->to('/');
}
$storesModel = new Stores();
$data['page_title'] = $this->pageTitle;
$data['stores'] = $storesModel->getActiveStore();
return $this->render_template('action_log/index', $data);
}
public function fetchData()
{
$session = session();
$user = $session->get('user');
if ($user['group_name'] !== 'SuperAdmin') {
return $this->response->setJSON(['data' => []]);
}
$historiqueModel = new Historique();
$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'),
];
$allData = $historiqueModel->getHistoriqueWithFilters($filters);
$result = ['data' => []];
foreach ($allData as $row) {
$result['data'][] = [
date('d/m/Y H:i', strtotime($row['created_at'])),
$row['user_name'] ?? '<em>Système</em>',
$this->getActionBadge($row['action']),
$this->getTableLabel($row['table_name']),
$row['description'] ?? '',
];
}
return $this->response->setJSON($result);
}
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>',
'PAYMENT' => '<span class="label label-primary">Paiement</span>',
'VALIDATE' => '<span class="label label-info">Validation</span>',
'REFUSE' => '<span class="label label-danger">Refus</span>',
'DELIVERY' => '<span class="label label-success">Livraison</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">Import</span>',
'LOGIN' => '<span class="label label-default">Connexion</span>',
];
return $badges[$action] ?? '<span class="label label-secondary">' . $action . '</span>';
}
private function getTableLabel($tableName)
{
$labels = [
'orders' => 'Commande',
'products' => 'Produit',
'users' => 'Utilisateur',
'groups' => 'Rôle',
'avances' => 'Avance',
'securite' => 'Sécurité',
'remise' => 'Remise',
'sortie_caisse' => 'Décaissement',
'autres_encaissements' => 'Encaissement',
'recouvrement' => 'Recouvrement',
'stores' => 'Point de vente',
'brands' => 'Marque',
'categories' => 'Catégorie',
];
return $labels[$tableName] ?? $tableName;
}
public function export()
{
$session = session();
$user = $session->get('user');
if ($user['group_name'] !== 'SuperAdmin') {
return redirect()->to('/');
}
$historiqueModel = new Historique();
$filters = [
'action' => $this->request->getGet('action'),
'store_name' => $this->request->getGet('store_name'),
'date_from' => $this->request->getGet('date_from'),
'date_to' => $this->request->getGet('date_to'),
];
$data = $historiqueModel->getHistoriqueWithFilters($filters);
$csv = "\xEF\xBB\xBF"; // BOM UTF-8 pour Excel
$csv .= "Date;Heure;Utilisateur;Action;Module;Description\n";
foreach ($data as $row) {
$date = date('d-m-Y', strtotime($row['created_at']));
$heure = date('H:i', strtotime($row['created_at']));
$userName = $row['user_name'] ?? 'Système';
$action = $row['action'];
$module = $this->getTableLabel($row['table_name']);
$description = str_replace('"', '""', $row['description'] ?? '');
$csv .= "{$date};{$heure};{$userName};{$action};{$module};\"{$description}\"\n";
}
$filename = 'historique_actions_' . date('Y-m-d_H-i') . '.csv';
return $this->response
->setHeader('Content-Type', 'text/csv; charset=utf-8')
->setHeader('Content-Disposition', 'attachment; filename="' . $filename . '"')
->setBody($csv);
}
}

View File

@ -44,6 +44,13 @@ abstract class AdminController extends BaseController
protected function render_template($page = null, $data = [])
{
$data['user_permission'] = $this->permission;
// Charger le logo dynamiquement
if (!isset($data['company_logo'])) {
$companyModel = new Company();
$companyData = $companyModel->getCompanyData(1);
$data['company_logo'] = $companyData['logo'] ?? 'assets/images/company_logo.jpg';
$data['company_name'] = $companyData['company_name'] ?? 'MotorBike';
}
echo view('templates/header', $data);
echo view('templates/header_menu', $data);
echo view('templates/side_menubar', $data);
@ -51,6 +58,14 @@ abstract class AdminController extends BaseController
echo view('templates/footer', $data);
}
// Get company logo path
protected function getCompanyLogo()
{
$model = new Company();
$data = $model->getCompanyData(1);
return $data['logo'] ?? 'assets/images/company_logo.jpg';
}
// Get company currency using model
public function company_currency()
{

View File

@ -6,6 +6,7 @@ use App\Models\ProductImage;
use App\Models\Users;
use App\Models\Stores;
use App\Models\Products;
use App\Models\Historique;
class Auth extends AdminController
{
@ -79,6 +80,10 @@ public function loginPost()
'logged_in' => true
]);
// Log connexion
$historique = new Historique();
$historique->logAction('users', 'LOGIN', $user['id'], "Connexion de {$user['firstname']} {$user['lastname']} ({$user['group_name']})");
// Redirect to dashboard
return redirect()->to('/');
}
@ -124,7 +129,8 @@ public function loginPost()
$buttons .= " <a href='/ventes/show/" . $value['id'] . "' class='btn btn-default'><i class='fa fa-eye'></i></a>";
}
if (is_array($this->permission) && in_array('createOrder', $this->permission)) {
$productSoldStatus = (int)($value['product_sold'] ?? 0);
if (is_array($this->permission) && in_array('createOrder', $this->permission) && $productSoldStatus === 0) {
$buttons .= ($value['qty'] == 1)
? " <a href='/orders/createFromEspace/" . $value['id'] . "' class='btn btn-default'><i class='fa fa-shopping-cart'></i></a>"
: " <button class='btn btn-default' title='0 en stock'><i class='fa fa-shopping-cart'></i></button>";
@ -134,6 +140,16 @@ public function loginPost()
$img = '<img src="' . base_url('assets/images/product_image/' . $value['image']) . '" alt="' . $value['image'] . '" class="img-circle" width="50" height="50" />';
// Statut basé sur product_sold
$productSold = (int)($value['product_sold'] ?? 0);
if ($productSold === 2) {
$statut = '<span class="label label-warning">En attente de livraison</span>';
} elseif ($productSold === 1) {
$statut = '<span class="label label-default">Livré</span>';
} else {
$statut = '<span class="label label-success">Disponible</span>';
}
// Populate the result data
$result['data'][] = [
$img,
@ -142,6 +158,7 @@ public function loginPost()
number_format($value['prix_vente'], 0, ',', ' '),
$value['puissance'] . ' CC',
$value['numero_de_moteur'],
$statut,
$buttons
];
}

View File

@ -5,6 +5,7 @@ namespace App\Controllers;
use App\Models\AutresEncaissements;
use App\Models\Stores;
use App\Models\Users;
use App\Models\Historique;
class AutresEncaissementsController extends AdminController
{
@ -41,7 +42,7 @@ class AutresEncaissementsController extends AdminController
*/
public function create()
{
if ($this->request->getMethod() !== 'post') {
if (strtolower($this->request->getMethod()) !== 'post') {
return $this->response->setJSON([
'success' => false,
'messages' => 'Méthode non autorisée'
@ -103,35 +104,13 @@ class AutresEncaissementsController extends AdminController
$notificationMessage = "Nouvel encaissement {$finalType} créé par {$user['firstname']} {$user['lastname']} - Montant: {$montantFormate} Ar";
// ✅ Envoyer notification à DAF, Direction et SuperAdmin de TOUS les stores
foreach ($allStores as $store) {
$storeId = (int)$store['id'];
$Notification->notifyGroupsByPermissionAllStores('notifEncaissement', $notificationMessage, 'encaissements');
// Notification pour DAF
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'encaissements'
);
log_message('info', "✅ Encaissement {$encaissementId} créé - Notifications envoyées");
// Notification pour Direction
$Notification->createNotification(
$notificationMessage,
"Direction",
$storeId,
'encaissements'
);
// Notification pour SuperAdmin
$Notification->createNotification(
$notificationMessage,
"SuperAdmin",
$storeId,
'encaissements'
);
}
log_message('info', "✅ Encaissement {$encaissementId} créé - Notifications envoyées à DAF/Direction/SuperAdmin de tous les stores");
// Log de l'action
$historique = new Historique();
$historique->logAction('autres_encaissements', 'CREATE', $encaissementId, "Encaissement {$finalType} - Montant: {$montantFormate} Ar");
return $this->response->setJSON([
'success' => true,
@ -204,30 +183,7 @@ class AutresEncaissementsController extends AdminController
$notificationMessage = "Encaissement {$finalType} modifié par {$user['firstname']} {$user['lastname']} - Ancien montant: {$ancienMontant} Ar → Nouveau: {$montantFormate} Ar";
foreach ($allStores as $store) {
$storeId = (int)$store['id'];
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'encaissements'
);
$Notification->createNotification(
$notificationMessage,
"Direction",
$storeId,
'encaissements'
);
$Notification->createNotification(
$notificationMessage,
"SuperAdmin",
$storeId,
'encaissements'
);
}
$Notification->notifyGroupsByPermissionAllStores('notifEncaissement', $notificationMessage, 'encaissements');
log_message('info', "✅ Encaissement {$id} modifié - Notifications envoyées");
@ -287,30 +243,7 @@ class AutresEncaissementsController extends AdminController
$notificationMessage = "⚠️ Encaissement {$type} supprimé par {$user['firstname']} {$user['lastname']} - Montant: {$montantFormate} Ar";
foreach ($allStores as $store) {
$storeId = (int)$store['id'];
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'encaissements'
);
$Notification->createNotification(
$notificationMessage,
"Direction",
$storeId,
'encaissements'
);
$Notification->createNotification(
$notificationMessage,
"SuperAdmin",
$storeId,
'encaissements'
);
}
$Notification->notifyGroupsByPermissionAllStores('notifEncaissement', $notificationMessage, 'encaissements');
log_message('info', "✅ Encaissement {$id} supprimé - Notifications envoyées");

View File

@ -116,13 +116,14 @@ private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCai
{
$date_time = date('d-m-Y h:i a', strtotime($value['avance_date']));
// ✅ Gestion sécurisée du nom du produit
// Gestion du nom du produit et numéro de série
$productSku = '';
if ($value['type_avance'] === 'mere') {
$productName = $value['product_name'] ?? 'Produit sur mer';
} else {
$productName = !empty($value['product_name'])
? $value['product_name']
: $product->getProductNameById($value['product_id'] ?? 0);
$productData = !empty($value['product_id']) ? $product->find($value['product_id']) : null;
$productName = $productData['name'] ?? ($value['product_name'] ?? 'N/A');
$productSku = $productData['sku'] ?? '';
}
if ($isAdmin) {
@ -131,6 +132,7 @@ private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCai
$value['customer_phone'],
$value['customer_address'],
$productName,
$productSku,
number_format((int)$value['gross_amount'], 0, ',', ' '),
number_format((int)$value['avance_amount'], 0, ',', ' '),
number_format((int)$value['amount_due'], 0, ',', ' '),
@ -141,6 +143,7 @@ private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCai
return [
$value['avance_id'],
$productName,
$productSku,
number_format((int)$value['avance_amount'], 0, ',', ' '),
number_format((int)$value['amount_due'], 0, ',', ' '),
$date_time,
@ -195,7 +198,75 @@ private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData')
public function fetchAvanceBecameOrder()
{
return $this->fetchAvanceDataGeneric('getCompletedAvances');
helper(['url', 'form']);
$Avance = new Avance();
$product = new Products();
$result = ['data' => []];
$data = $Avance->getCompletedAvances();
$session = session();
$users = $session->get('user');
$isAdmin = $this->isAdmin($users);
$isCommerciale = $this->isCommerciale($users);
$isCaissier = $this->isCaissier($users);
foreach ($data as $key => $value) {
$isOwner = $users['id'] === $value['user_id'];
$buttons = $this->buildActionButtons(
$value,
$isAdmin,
$isOwner,
$isCaissier,
$isCommerciale
);
// Déterminer le statut
if ($value['is_order'] == 1) {
$status = '<span class="label label-success"><i class="fa fa-check"></i> Payé</span>';
} else {
$status = '<span class="label label-warning"><i class="fa fa-clock-o"></i> En attente de paiement</span>';
}
$date_time = date('d-m-Y h:i a', strtotime($value['avance_date']));
$productSku = '';
if ($value['type_avance'] === 'mere') {
$productName = $value['product_name'] ?? 'Produit sur mer';
} else {
$productData = !empty($value['product_id']) ? $product->find($value['product_id']) : null;
$productName = $productData['name'] ?? ($value['product_name'] ?? 'N/A');
$productSku = $productData['sku'] ?? '';
}
if ($isAdmin) {
$result['data'][] = [
$value['customer_name'],
$value['customer_phone'],
$value['customer_address'],
$productName,
$productSku,
number_format((int)$value['gross_amount'], 0, ',', ' '),
number_format((int)$value['avance_amount'], 0, ',', ' '),
$status,
$date_time,
$buttons,
];
} elseif ($isCommerciale || $isCaissier) {
$result['data'][] = [
$value['avance_id'],
$productName,
$productSku,
number_format((int)$value['avance_amount'], 0, ',', ' '),
$status,
$date_time,
$buttons,
];
}
}
return $this->response->setJSON($result);
}
public function fetchExpiredAvance()
@ -318,28 +389,11 @@ public function validateAvance()
$customerName = $avance['customer_name'];
$avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT);
// Notifier le commercial
$Notification->createNotification(
"✅ Votre avance N°{$avanceNumber} pour le client {$customerName} a été validée par la caissière",
"COMMERCIALE",
(int)$users['store_id'],
'avances'
);
// Notifier les commerciaux
$Notification->notifyGroupsByPermission('notifRemise', "✅ Votre avance N°{$avanceNumber} pour le client {$customerName} a été validée par la caissière", (int)$users['store_id'], 'avances');
// Notifier Direction/DAF
$Notification->createNotification(
"La caissière a validé l'avance N°{$avanceNumber} créée par un commercial",
"Direction",
(int)$users['store_id'],
'avances'
);
$Notification->createNotification(
"La caissière a validé l'avance N°{$avanceNumber} créée par un commercial",
"DAF",
(int)$users['store_id'],
'avances'
);
// Notifier les groupes ayant updateAvance
$Notification->notifyGroupsByPermission('notifAvance', "La caissière a validé l'avance N°{$avanceNumber} créée par un commercial", (int)$users['store_id'], 'avances');
return $this->response->setJSON([
'success' => true,
@ -565,7 +619,7 @@ public function validateAvance()
{
$this->verifyRole('createAvance');
if ($this->request->getMethod() !== 'post') {
if (strtolower($this->request->getMethod()) !== 'post') {
return $this->response->setJSON([
'success' => false,
'messages' => 'Méthode non autorisée'
@ -672,43 +726,12 @@ public function validateAvance()
$notificationMessage = "Nouvelle avance {$typeAvanceLabel} créée par {$users['firstname']} {$users['lastname']} - Client: {$customerName} - Montant: {$avanceAmount} Ar";
// ✅ Envoyer notification à DAF, Direction et SuperAdmin de TOUS les stores
foreach ($allStores as $store) {
$storeId = (int)$store['id'];
// Notification pour DAF
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'avances'
);
// Notification pour Direction
$Notification->createNotification(
$notificationMessage,
"Direction",
$storeId,
'avances'
);
// Notification pour SuperAdmin
$Notification->createNotification(
$notificationMessage,
"SuperAdmin",
$storeId,
'avances'
);
}
// ✅ Notifier tous les groupes ayant updateAvance de TOUS les stores
$Notification->notifyGroupsByPermissionAllStores('notifAvance', $notificationMessage, 'avances');
// ✅ Notification à la Caissière UNIQUEMENT du store concerné (si l'utilisateur est COMMERCIALE)
if ($this->isCommerciale($users)) {
$Notification->createNotification(
'Une nouvelle avance a été créée par un commercial',
"Caissière",
(int)$users['store_id'],
'avances'
);
$Notification->notifyGroupsByPermission('notifSortieCaisse', 'Une nouvelle avance a été créée par un commercial', (int)$users['store_id'], 'avances');
}
log_message('info', "✅ Avance {$avance_id} créée - Notifications envoyées à DAF/Direction/SuperAdmin de tous les stores");
@ -737,7 +760,7 @@ public function validateAvance()
{
$this->verifyRole('updateAvance');
if ($this->request->getMethod() !== 'post') {
if (strtolower($this->request->getMethod()) !== 'post') {
return $this->response->setJSON([
'success' => false,
'messages' => 'Méthode non autorisée'
@ -901,12 +924,7 @@ public function validateAvance()
}
// ✅ NOTIFICATION (modification simple)
$Notification->createNotification(
'Une avance a été modifiée',
"Caissière",
(int)$users['store_id'],
'avances'
);
$Notification->notifyGroupsByPermission('notifSortieCaisse', 'Une avance a été modifiée', (int)$users['store_id'], 'avances');
return $this->response->setJSON([
'success' => true,
@ -1051,13 +1069,21 @@ public function printInvoice($avance_id)
return redirect()->back()->with('error', 'Accès non autorisé');
}
// ✅ CORRECTION SIMPLIFIÉE
// Récupérer les données de l'entreprise
$companyModel = new Company();
$companyData = $companyModel->getCompanyData(1);
// Récupérer les détails du produit
$product = null;
$brandName = '';
if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) {
$productName = $avance['product_name'];
$productDetails = [
'marque' => $avance['product_name'],
'numero_moteur' => '',
'puissance' => ''
'puissance' => '',
'couleur' => '',
'chasis' => '',
'type_avance' => 'mere',
];
} else {
$product = $Products->find($avance['product_id']);
@ -1066,9 +1092,6 @@ public function printInvoice($avance_id)
return redirect()->back()->with('error', 'Produit non trouvé');
}
$productName = $product['name'] ?? 'N/A';
// ✅ Récupérer le nom de la marque
$brandName = 'N/A';
if (!empty($product['marque'])) {
$db = \Config\Database::connect();
@ -1085,11 +1108,14 @@ public function printInvoice($avance_id)
$productDetails = [
'marque' => $brandName,
'numero_moteur' => $product['numero_de_moteur'] ?? '',
'puissance' => $product['puissance'] ?? ''
'puissance' => $product['puissance'] ?? '',
'couleur' => $product['cler'] ?? '',
'chasis' => $product['chasis'] ?? '',
'type_avance' => $avance['type_avance'] ?? 'terre',
];
}
$html = $this->generatePrintableInvoiceHTML($avance, $productName, $productDetails);
$html = $this->generatePrintableInvoiceHTML($avance, $productDetails, $companyData);
return $this->response->setBody($html);
@ -1756,23 +1782,9 @@ public function notifyPrintInvoice()
$customerName = $avance['customer_name'];
$avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT);
$Notification->createNotification(
"La caissière a imprimé la facture N°{$avanceNumber} pour le client {$customerName}",
"Direction",
(int)$users['store_id'],
'avances'
);
$Notification->createNotification(
"Il y a une avance N°{$avanceNumber} pour le client {$customerName}",
"DAF",
(int)$users['store_id'],
'avances'
);
$Notification->createNotification(
"Il y a une avance N°{$avanceNumber} pour le client {$customerName}",
"SuperAdmin",
$Notification->notifyGroupsByPermission(
'updateAvance',
"La caissière a imprimé la facture avance N°{$avanceNumber} pour le client {$customerName}",
(int)$users['store_id'],
'avances'
);
@ -1888,19 +1900,34 @@ public function getFullInvoiceForPrint($avance_id)
/**
* Générer le HTML optimisé pour l'impression (version identique à printInvoice)
*/
private function generatePrintableInvoiceHTML($avance, $productName, $productDetails)
private function generatePrintableInvoiceHTML($avance, $productDetails, $companyData)
{
$avanceDate = date('d/m/Y', strtotime($avance['avance_date']));
$avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT);
$customerName = strtoupper(esc($avance['customer_name']));
$customerPhone = esc($avance['customer_phone']);
$customerCin = esc($avance['customer_cin']);
$customerPhone = esc($avance['customer_phone'] ?? '');
$grossAmount = number_format($avance['gross_amount'], 0, ',', ' ');
$avanceAmount = number_format($avance['avance_amount'], 0, ',', ' ');
$amountDue = number_format($avance['amount_due'], 0, ',', ' ');
$marque = esc($productDetails['marque']) ?: $productName;
$marque = esc($productDetails['marque']);
$numeroMoteur = esc($productDetails['numero_moteur']);
$puissance = esc($productDetails['puissance']);
$couleur = esc($productDetails['couleur'] ?? '');
$chasis = esc($productDetails['chasis'] ?? '');
$typeAvance = $productDetails['type_avance'] ?? 'terre';
$nSerieOuArrivage = ($typeAvance === 'mere') ? 'Arrivage' : ($chasis ?: $numeroMoteur);
$typePayment = esc($avance['type_payment'] ?? '');
$unite = '1';
// Company data
$companyName = esc($companyData['company_name'] ?? 'MOTORBIKE STORE');
$companyNIF = esc($companyData['NIF'] ?? '');
$companySTAT = esc($companyData['STAT'] ?? '');
$companyPhone = esc($companyData['phone'] ?? '');
$companyPhone2 = esc($companyData['phone2'] ?? '');
$companyAddress = esc($companyData['address'] ?? '');
$year = date('Y');
return <<<HTML
<!DOCTYPE html>
@ -1908,7 +1935,7 @@ private function generatePrintableInvoiceHTML($avance, $productName, $productDet
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Facture Avance - KELY SCOOTERS</title>
<title>Facture d'Acompte - {$companyName}</title>
<style>
@media print {
body { margin: 0; padding: 0; }
@ -1921,276 +1948,347 @@ private function generatePrintableInvoiceHTML($avance, $productName, $productDet
body {
font-family: Arial, sans-serif;
font-size: 12px;
line-height: 1.4;
font-size: 13px;
line-height: 1.6;
background: #fff;
}
.page {
width: 210mm;
height: 297mm;
padding: 15mm;
min-height: 297mm;
padding: 15mm 20mm;
margin: 0 auto;
background: white;
position: relative;
}
.invoice-container {
border: 2px solid #000;
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.header {
/* === RECTO === */
.recto-title {
text-align: center;
border-bottom: 2px solid #000;
padding-bottom: 10px;
margin-bottom: 15px;
}
.header h1 {
font-size: 24px;
font-size: 20px;
font-weight: bold;
margin-bottom: 5px;
text-decoration: underline;
margin-bottom: 25px;
letter-spacing: 1px;
}
.header-info {
.recto-header {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 11px;
align-items: flex-start;
margin-bottom: 20px;
}
.header-left, .header-right {
text-align: left;
.recto-company {
font-size: 12px;
line-height: 1.5;
}
.invoice-title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 15px 0;
padding: 10px;
border: 1px solid #000;
.recto-company strong {
font-size: 14px;
}
.invoice-title h2 {
font-size: 18px;
.recto-date-qr {
text-align: right;
}
.recto-date-qr .date-block {
font-size: 13px;
margin-bottom: 10px;
}
.qr-code {
width: 100px;
height: 100px;
}
.watermark {
position: absolute;
top: 45%;
left: 50%;
transform: translate(-50%, -50%) rotate(-25deg);
font-size: 60px;
color: rgba(0, 0, 0, 0.06);
font-weight: bold;
letter-spacing: 10px;
pointer-events: none;
z-index: 0;
}
.original-badge {
background: #000;
color: #fff;
padding: 5px 15px;
font-weight: bold;
transform: skewX(-10deg);
.recto-body {
position: relative;
z-index: 1;
}
.customer-info {
margin: 15px 0;
padding: 10px;
border: 1px solid #000;
.field-line {
margin: 12px 0;
font-size: 13px;
}
.customer-info div {
margin: 5px 0;
.field-line strong {
display: inline-block;
min-width: 180px;
}
.field-line .field-value {
border-bottom: 1px dotted #000;
display: inline-block;
min-width: 300px;
padding-bottom: 2px;
}
.field-indent {
margin-left: 30px;
}
.product-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
margin-top: 25px;
}
.product-table th, .product-table td {
border: 1px solid #000;
padding: 8px;
text-align: left;
border: 2px solid #000;
padding: 10px 12px;
text-align: center;
font-size: 13px;
}
.product-table th {
background: #f0f0f0;
font-weight: bold;
font-size: 12px;
}
.product-table td {
height: 40px;
}
/* === VERSO === */
.verso-header {
font-size: 12px;
line-height: 1.5;
margin-bottom: 30px;
}
.verso-header strong {
font-size: 14px;
}
.verso-title {
text-align: center;
font-size: 18px;
font-weight: bold;
text-decoration: underline;
margin-bottom: 25px;
}
.verso-intro {
margin-bottom: 15px;
font-size: 13px;
}
.article {
margin: 15px 0;
font-size: 13px;
}
.article-title {
font-weight: bold;
margin-bottom: 5px;
}
.article ul {
margin-left: 15px;
list-style: disc;
}
.article li {
margin: 3px 0;
}
.signature-section {
display: flex;
justify-content: space-between;
margin-top: auto;
padding: 20px 0;
margin-top: 50px;
}
.signature-box {
text-align: center;
width: 45%;
}
.signature-box p {
font-weight: bold;
margin-bottom: 50px;
}
/* Style spécifique pour le verso */
.contract-section {
margin-top: 20px;
padding: 15px;
width: 40%;
border: 2px solid #000;
font-size: 10px;
min-height: 80px;
padding: 10px;
}
.contract-section h3 {
text-align: center;
font-size: 14px;
margin-bottom: 10px;
text-decoration: underline;
}
.contract-article {
margin: 10px 0;
}
.contract-article strong {
text-decoration: underline;
}
.verso-content {
flex: 1;
display: flex;
flex-direction: column;
.signature-box .sig-title {
font-weight: bold;
font-size: 13px;
margin-bottom: 60px;
}
</style>
</head>
<body>
<!-- RECTO -->
<!-- ==================== RECTO ==================== -->
<div class="page">
<div class="invoice-container">
<!-- Header -->
<div class="header">
<h1>KELY SCOOTERS</h1>
<div class="header-info">
<div class="header-left">
<div>NIF: 401 840 5554</div>
<div>STAT: 46101 11 2024 00317</div>
<div class="watermark">ORIGINAL</div>
<div class="recto-title">FACTURE D'ACOMPTE DE RESERVATION</div>
<div class="recto-header">
<div class="recto-company">
<strong>{$companyName}</strong><br>
NIF : {$companyNIF}<br>
STAT : {$companySTAT}<br>
Contact : {$companyPhone} / {$companyPhone2}<br>
{$companyAddress}
</div>
<div class="header-right">
<div>Contact: +261 34 27 946 35 / +261 34 07 079 69</div>
<div>Antsakaviro en face WWF</div>
<div class="recto-date-qr">
<div class="date-block">
Date : <strong>{$avanceDate}</strong><br>
: <strong>{$avanceNumber}</strong>
</div>
<canvas id="qrcode" class="qr-code"></canvas>
</div>
</div>
<!-- Invoice Title -->
<div class="invoice-title">
<div>
<h2>FACTURE</h2>
<div>Date: {$avanceDate}</div>
<div>: {$avanceNumber}CI 2025</div>
</div>
<div class="original-badge">DOIT ORIGINAL</div>
<div class="recto-body">
<div class="field-line"><strong>DOIT</strong></div>
<div class="field-line">
<strong>NOM :</strong>
<span class="field-value">{$customerName}</span>
</div>
<!-- Customer Info -->
<div class="customer-info">
<div><strong>NOM:</strong> {$customerName} ({$customerPhone})</div>
<div><strong>CIN:</strong> {$customerCin}</div>
<div><strong>PC:</strong> {$grossAmount} Ar</div>
<div><strong>AVANCE:</strong> {$avanceAmount} Ar</div>
<div><strong>RAP:</strong> {$amountDue} Ar</div>
<div class="field-line">
<strong>N de Série ou Arrivage :</strong>
<span class="field-value">{$nSerieOuArrivage}</span>
</div>
<!-- Product Table -->
<div class="field-line field-indent">
Si Arrivage, préciser la caractéristique de la moto : Couleur
<span class="field-value">{$couleur}</span>
</div>
<div class="field-line">
<strong>MOTO :</strong>
<span class="field-value">{$marque}</span>
</div>
<div class="field-line field-indent">
<strong>Unité :</strong>
<span class="field-value">{$unite}</span>
</div>
<div class="field-line field-indent">
<strong>Prix Unitaire :</strong>
<span class="field-value">{$grossAmount} Ar</span>
</div>
<div class="field-line">
<strong>Montant Total :</strong>
<span class="field-value">{$grossAmount} Ar</span>
</div>
<div class="field-line">
<strong>Mode de Paiement :</strong>
<span class="field-value">{$typePayment}</span>
</div>
<div class="field-line">
<strong>AVANCE :</strong>
<span class="field-value">{$avanceAmount} Ar</span>
</div>
<div class="field-line">
<strong>Reste à payer :</strong>
<span class="field-value">{$amountDue} Ar</span>
</div>
<!-- Tableau récapitulatif -->
<table class="product-table">
<thead>
<tr>
<th>MARQUE</th>
<th>N°MOTEUR</th>
<th> Châssis/Arrivage</th>
<th>PUISSANCE</th>
<th>RAP (Ariary)</th>
<th>Reste à payer (Ariary)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{$marque}</td>
<td>{$numeroMoteur}</td>
<td>{$chasis}</td>
<td>{$puissance}</td>
<td>{$amountDue}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- VERSO -->
<!-- ==================== VERSO ==================== -->
<div class="page">
<div class="invoice-container">
<div class="invoice-title">
<div>
<h2>CONDITIONS GÉNÉRALES</h2>
<div>Date: {$avanceDate}</div>
<div>: {$avanceNumber}CI 2025</div>
<div class="verso-content">
<!-- Contract Section -->
<div class="contract-section">
<h3>FIFANEKENA ARA-BAROTRA (Réservations)</h3>
<p><strong>Ry mpanjifa hajaina,</strong></p>
<p>Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.</p>
<div class="contract-article">
<strong>Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA</strong>
<p>Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).</p>
<div class="verso-header">
<strong>{$companyName}</strong><br>
NIF : {$companyNIF}<br>
STAT : {$companySTAT}<br>
Contact : {$companyPhone} / {$companyPhone2}<br>
{$companyAddress}
</div>
<div class="contract-article">
<strong>Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE)</strong>
<p>Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.</p>
<p>Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.</p>
<div class="verso-title">FIFANEKENA ARA-BAROTRA (Reservations)</div>
<div class="verso-intro">
Ry mpanjifa hajaina,<br><br>
Natao ity fifanekena ity mba hialana amin'ny fivadiahampitokisana amin'ny andaniny sy ankilany.
</div>
<div class="contract-article">
<strong>Andininy faha-3: FAMERENANA VOLA</strong>
<div class="article">
<div class="article-title">&bull; Andininy faha-1 : FAMANDRIHANA SY FANDOAVAM-BOLA</div>
<p>Ny mpividy dia manao famandrihana amin'ny alalan'ny fandoavam-bola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).</p>
</div>
<div class="article">
<div class="article-title">&bull; Andininy faha-2 : FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE)</div>
<p>Rehefa tonga ny moto/pieces dia tsy maintsy mandoha ny 50 isan-jato ny vidin'entana ny mpamandrika.</p>
<p>Manana 15 andro kosa andoaovana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.</p>
</div>
<div class="article">
<div class="article-title">&bull; Andininy faha-3 : FAMERENANA VOLA</div>
<p>Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.</p>
</div>
<div class="contract-article">
<strong>Andininy faha-4: FEPETRA FANAMPINY</strong>
<ul style="margin-left: 20px;">
<div class="article">
<div class="article-title">&bull; Andininy faha-4 : FEPETRA FANAMPINY</div>
<ul>
<li>Tsy misafidy raha toa ka mamafa no ifanarahana.</li>
<li>Tsy azo atao ny mamerina ny entana efa nofandrahana.</li>
<li>Tsy azo atao ny manakalo ny entana efa nofandrahana.</li>
<li>Tsy azo atao ny mamerina vola efa naloha.</li>
<li>Tsy azo atao ny manakalo ny entana efa nofandrihana.</li>
</ul>
</div>
</div>
<!-- Additional space for notes -->
<div style="margin-top: 20px; padding: 10px; border: 1px solid #000; flex: 1;">
<strong>OBSERVATIONS / NOTES:</strong>
<div style="height: 100px; margin-top: 10px;"></div>
</div>
<!-- Signatures for verso -->
<!-- Signatures -->
<div class="signature-section">
<div class="signature-box">
<p>NY MPAMANDRIKA</p>
<div style="border-top: 1px solid #000; padding-top: 5px;">Signature</div>
</div>
<div class="signature-box">
<p>NY MPIVAROTRA</p>
<div style="border-top: 1px solid #000; padding-top: 5px;">
<strong>KELY SCOOTERS</strong><br>
NIF: 401 840 5554
</div>
</div>
<div class="sig-title">NY MPAMANDRIKA</div>
</div>
<div class="signature-box" style="text-align: right;">
<div class="sig-title">NY MPIVAROTRA</div>
</div>
</div>
</div>
<!-- QR Code JS -->
<script src="https://cdn.jsdelivr.net/npm/qrious@4.0.2/dist/qrious.min.js"></script>
<script>
new QRious({
element: document.getElementById('qrcode'),
value: "FACTURE D'ACOMPTE DE RESERVATION\\nN° {$avanceNumber}\\nClient: {$customerName}\\nMontant Total: {$grossAmount} Ar\\nAvance: {$avanceAmount} Ar\\nReste: {$amountDue} Ar\\nDate: {$avanceDate}\\nFacebook: https://www.facebook.com/MOTORBIKESTORE2021/",
size: 100,
level: 'H'
});
window.onload = function() { window.print(); };
</script>
</body>
</html>
HTML;

View File

@ -100,7 +100,7 @@ class AvanceController extends AdminController
$Products = new Products();
$Notification = New NotificationController();
if ($this->request->getMethod() === 'post' && $validation->run($validationData)) {
if (strtolower($this->request->getMethod()) === 'post' && $validation->run($validationData)) {
$session = session();
$users = $session->get('user');
@ -120,7 +120,7 @@ class AvanceController extends AdminController
$posts = $products;
if($avance_id = $Avance->createAvance($data)){
$Notification->createNotification('Une avance a été créé', "Conseil",$users['store_id'], 'avance');
$Notification->notifyGroupsByPermission('notifAvance', 'Une avance a été créé', $users['store_id'], 'avance');
return $this->response->setJSON([
'success' => true,
'messages' => 'Avance créé avec succès !'
@ -143,7 +143,7 @@ class AvanceController extends AdminController
$Products = new Products();
$Avance = new Avance();
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
$data = [
'customer_name' => $this->request->getPost('customer_name_avance'),
'customer_address'=> $this->request->getPost('customer_address_avance'),

View File

@ -29,7 +29,7 @@ class CompanyController extends AdminController
$Company = new Company();
// die(var_dump($validation->getErrors()));
if ($this->request->getMethod() == 'post' && $validation->withRequest($this->request)->run()) {
if (strtolower($this->request->getMethod()) === 'post' && $validation->withRequest($this->request)->run()) {
// If the form is valid
$data = [
'company_name' => $this->request->getPost('company_name'),
@ -45,6 +45,14 @@ class CompanyController extends AdminController
'currency' => $this->request->getPost('currency'),
];
// Upload du logo
$logoFile = $this->request->getFile('logo');
if ($logoFile && $logoFile->isValid() && !$logoFile->hasMoved()) {
$newName = 'company_logo.' . $logoFile->getExtension();
$logoFile->move(FCPATH . 'assets/images/', $newName, true);
$data['logo'] = 'assets/images/' . $newName;
}
if ($Company->updateCompany($data, 1)) {
session()->setFlashdata('success', 'Successfully updated');
return redirect()->to('/company');

View File

@ -284,4 +284,89 @@ class Dashboard extends AdminController
return $this->render_template('dashboard', $data);
}
/**
* AJAX : Recalculer la trésorerie avec filtre de dates
*/
public function getTresorerieData()
{
$session = session();
$user_id = $session->get('user');
$startDate = $this->request->getGet('start_date');
$endDate = $this->request->getGet('end_date');
$orderModel = new Orders();
$Avance = new Avance();
$sortieCaisse = new SortieCaisse();
$Recouvrement = new Recouvrement();
$autresEncaissementsModel = new AutresEncaissements();
$isAdmin = in_array($user_id['group_name'], ['DAF', 'Direction', 'SuperAdmin']);
$storeIdFilter = $isAdmin ? null : $user_id['store_id'];
// Données avec filtre de dates
$paymentData = $orderModel->getPaymentModes($startDate, $endDate);
$totalAvance = $Avance->getTotalAvance($startDate, $endDate);
$paymentDataAvance = $Avance->getPaymentModesAvance($startDate, $endDate);
$totalRecouvrement = $Recouvrement->getTotalRecouvrements(null, $startDate, $endDate);
$total_sortie_caisse = $sortieCaisse->getTotalSortieCaisse($startDate, $endDate);
$totauxAutresEncaissements = $autresEncaissementsModel->getTotalEncaissementsByMode($storeIdFilter, $startDate, $endDate);
// Autres encaissements
$total_autres_enc_espece = $totauxAutresEncaissements['total_espece'];
$total_autres_enc_mvola = $totauxAutresEncaissements['total_mvola'];
$total_autres_enc_virement = $totauxAutresEncaissements['total_virement'];
$total_autres_encaissements = $total_autres_enc_espece + $total_autres_enc_mvola + $total_autres_enc_virement;
// Sorties
$total_sortie_espece = isset($total_sortie_caisse->total_espece) ? (float) $total_sortie_caisse->total_espece : 0;
$total_sortie_mvola = isset($total_sortie_caisse->total_mvola) ? (float) $total_sortie_caisse->total_mvola : 0;
$total_sortie_virement = isset($total_sortie_caisse->total_virement) ? (float) $total_sortie_caisse->total_virement : 0;
// Recouvrements
$me = isset($totalRecouvrement->me) ? (float) $totalRecouvrement->me : 0;
$bm = isset($totalRecouvrement->bm) ? (float) $totalRecouvrement->bm : 0;
$be = isset($totalRecouvrement->be) ? (float) $totalRecouvrement->be : 0;
$mb = isset($totalRecouvrement->mb) ? (float) $totalRecouvrement->mb : 0;
// Orders
$total_orders = isset($paymentData->total) ? (float) $paymentData->total : 0;
$mv1_orders = isset($paymentData->total_mvola1) ? (float) $paymentData->total_mvola1 : 0;
$mv2_orders = isset($paymentData->total_mvola2) ? (float) $paymentData->total_mvola2 : 0;
$es1_orders = isset($paymentData->total_espece1) ? (float) $paymentData->total_espece1 : 0;
$es2_orders = isset($paymentData->total_espece2) ? (float) $paymentData->total_espece2 : 0;
$vb1_orders = isset($paymentData->total_virement_bancaire1) ? (float) $paymentData->total_virement_bancaire1 : 0;
$vb2_orders = isset($paymentData->total_virement_bancaire2) ? (float) $paymentData->total_virement_bancaire2 : 0;
// Avances
$total_avances = isset($paymentDataAvance->total) ? (float) $paymentDataAvance->total : 0;
$mv_avances = isset($paymentDataAvance->total_mvola) ? (float) $paymentDataAvance->total_mvola : 0;
$es_avances = isset($paymentDataAvance->total_espece) ? (float) $paymentDataAvance->total_espece : 0;
$vb_avances = isset($paymentDataAvance->total_virement_bancaire) ? (float) $paymentDataAvance->total_virement_bancaire : 0;
// Brut
$total_mvola_brut = $mv1_orders + $mv2_orders + $mv_avances + $total_autres_enc_mvola;
$total_espece_brut = $es1_orders + $es2_orders + $es_avances + $total_autres_enc_espece;
$total_vb_brut = $vb1_orders + $vb2_orders + $vb_avances + $total_autres_enc_virement;
$total_brut = $total_orders + $total_avances + $total_autres_encaissements;
// Final
$total_sortie_global = $total_sortie_espece + $total_sortie_mvola + $total_sortie_virement;
$total_mvola_final = $total_mvola_brut - $me - $mb + $bm - $total_sortie_mvola;
$total_espece_final = $total_espece_brut + $me + $be - $total_sortie_espece;
$total_vb_final = $total_vb_brut - $be - $bm + $mb - $total_sortie_virement;
$total_final = $total_brut - $total_sortie_global;
return $this->response->setJSON([
'total_brut' => number_format($total_brut, 0, '.', ' '),
'total_sorties' => number_format($total_sortie_global, 0, '.', ' '),
'total_net' => number_format($total_final, 0, '.', ' '),
'espece' => number_format($total_espece_final, 0, '.', ' '),
'mvola' => number_format($total_mvola_final, 0, '.', ' '),
'banque' => number_format($total_vb_final, 0, '.', ' '),
'avance_mvola' => number_format($mv_avances, 0, '.', ' '),
'avance_espece' => number_format($es_avances, 0, '.', ' '),
'avance_banque' => number_format($vb_avances, 0, '.', ' '),
]);
}
}

View File

@ -45,7 +45,7 @@ class GroupController extends AdminController
]);
// Check if form validation is successful
if ($this->request->getMethod() == 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
if ($validation->withRequest($this->request)->run()) {
@ -93,7 +93,7 @@ class GroupController extends AdminController
'group_name' => 'required',
]);
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
if ($validation->withRequest($this->request)->run()) {
// Validation passed
@ -149,7 +149,7 @@ class GroupController extends AdminController
}
// Vérifier si c'est une requête POST avec confirmation
if ($this->request->getMethod() === 'post' && $this->request->getPost('confirm')) {
if (strtolower($this->request->getMethod()) === 'post' && $this->request->getPost('confirm')) {
// Supprimer d'abord toutes les associations dans user_group
$groupsModel->removeUsersFromGroup($id);

View File

@ -71,6 +71,7 @@ class HistoriqueController extends AdminController
$row['product_name'] ?? 'N/A',
$row['sku'] ?? 'N/A',
$row['store_name'] ?? 'N/A',
$row['user_name'] ?? 'N/A',
$this->getActionBadge($row['action']),
$row['description'] ?? ''
];

View File

@ -3,6 +3,8 @@
namespace App\Controllers;
use App\Models\Notification;
use App\Models\Groups;
use App\Models\Stores;
class NotificationController extends AdminController
{
@ -44,6 +46,27 @@ class NotificationController extends AdminController
$Notification->insertNotification($data);
}
// Notifier tous les groupes ayant une permission donnée, pour un store spécifique
public function notifyGroupsByPermission(string $permission, string $message, ?int $store_id, ?string $link): void
{
$groupsModel = new Groups();
$groups = $groupsModel->getGroupsWithPermission($permission);
foreach ($groups as $group) {
$this->createNotification($message, $group['group_name'], $store_id, $link);
}
}
// Notifier tous les groupes ayant une permission donnée, pour TOUS les stores actifs
public function notifyGroupsByPermissionAllStores(string $permission, string $message, ?string $link): void
{
$storesModel = new Stores();
$allStores = $storesModel->getActiveStore();
if (!is_array($allStores) || empty($allStores)) return;
foreach ($allStores as $store) {
$this->notifyGroupsByPermission($permission, $message, (int)$store['id'], $link);
}
}
// Marquer toutes les notifications comme lues pour l'utilisateur connecté
public function markAllAsRead()
{

File diff suppressed because it is too large Load Diff

View File

@ -101,15 +101,20 @@ class ProductCOntroller extends AdminController
$store_name = $store_info && isset($store_info['name']) ? $store_info['name'] : "Inconnu";
}
// Disponibilité basée sur qty ET availability
// Disponibilité basée sur qty, availability et product_sold
$productSold = (int)($value['product_sold'] ?? 0);
$isInStock = ((int)$value['qty'] > 0);
$isAvailable = ((int)$value['availability'] === 1);
$isProductAvailable = $isInStock && $isAvailable;
$availability = $isProductAvailable ?
'<span class="label label-success">En stock</span>' :
'<span class="label label-danger">Rupture</span>';
if ($productSold === 2) {
$availability = '<span class="label label-warning">En attente de livraison</span>';
} elseif ($productSold === 1) {
$availability = '<span class="label label-default">Livré</span>';
} elseif ($isInStock && $isAvailable) {
$availability = '<span class="label label-success">En stock</span>';
} else {
$availability = '<span class="label label-danger">Rupture</span>';
}
// Construction des boutons
$buttons = '';
@ -185,7 +190,7 @@ class ProductCOntroller extends AdminController
'price_min' => 'required|numeric',
]);
if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) {
if (strtolower($this->request->getMethod()) === 'post' && $validation->withRequest($this->request)->run()) {
$prix_min = (float)$this->request->getPost('price_min');
$prix_vente = (float)$this->request->getPost('price_vente');
@ -224,7 +229,7 @@ class ProductCOntroller extends AdminController
'date_arivage' => $this->request->getPost('datea'),
'puissance' => $this->request->getPost('puissance'),
'cler' => $this->request->getPost('cler'),
'categorie_id' => json_encode($this->request->getPost('categorie[]')),
'categorie_id' => $this->request->getPost('categorie') ? implode(',', $this->request->getPost('categorie')) : '',
'etats' => $this->request->getPost('etats'),
'infoManquekit' => $this->request->getPost('infoManquekit'),
'info' => $this->request->getPost('info'),
@ -243,12 +248,7 @@ class ProductCOntroller extends AdminController
]);
session()->setFlashdata('success', 'Créé avec succès');
$Notification->createNotification(
"Un nouveau Produit a été créé",
"COMMERCIALE",
$store_id1,
'products/'
);
$Notification->notifyGroupsByPermission('notifProduit', "Un nouveau Produit a été créé", $store_id1, 'products/');
return redirect()->to('/products');
} else {
@ -302,6 +302,11 @@ class ProductCOntroller extends AdminController
public function update(int $id)
{
log_message('error', '=== PRODUCT UPDATE CALLED === ID: ' . $id . ' METHOD: ' . $_SERVER['REQUEST_METHOD']);
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
log_message('error', 'POST DATA KEYS: ' . implode(', ', array_keys($_POST)));
}
$Products = new Products();
$Stores = new Stores();
$Category = new Category();
@ -321,32 +326,32 @@ class ProductCOntroller extends AdminController
$prix_minimal_data = $FourchettePrix->where('product_id', $id)->first();
$prix_minimal = $prix_minimal_data['prix_minimal'] ?? '';
if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) {
if ($_SERVER['REQUEST_METHOD'] === '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'),
'name' => $this->request->getPost('nom_de_produit') ?? '',
'sku' => $this->request->getPost('numero_de_serie') ?? '',
'price' => $this->request->getPost('price') ?? '0',
'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'),
'description' => $this->request->getPost('description') ?? '',
'numero_de_moteur' => $this->request->getPost('numero_de_moteur') ?? '',
'marque' => $this->request->getPost('marque') ?? '0',
'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'),
'prix_vente' => $this->request->getPost('price_vente') ?? '0',
'date_arivage'=> $this->request->getPost('datea') ?: date('Y-m-d'),
'puissance' => $this->request->getPost('puissance') ?? '',
'cler' => $this->request->getPost('cler') ?? '',
'categorie_id'=> $this->request->getPost('categorie') ? implode(',', $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') ?? '',
];
// ---- GESTION PRIX MINIMAL ----
@ -372,19 +377,41 @@ class ProductCOntroller extends AdminController
}
// Upload image si fournie
if ($this->request->getFile('product_image')->isValid()) {
$productImage = $this->request->getFile('product_image');
if ($productImage && $productImage->isValid() && !$productImage->hasMoved()) {
$uploadImage = $this->uploadImage();
$Products->update($id, ['image' => $uploadImage]);
}
// Mise à jour du produit
if ($Products->updateProduct($data, $id)) {
try {
$db = \Config\Database::connect();
$session = session();
$currentUser = $session->get('user');
$userName = ($currentUser['firstname'] ?? '') . ' ' . ($currentUser['lastname'] ?? '');
$userId = $currentUser['id'] ?? 0;
// Passer l'utilisateur au trigger via variables MySQL
$db->query("SET @current_user_id = ?", [$userId]);
$db->query("SET @current_user_name = ?", [trim($userName)]);
$builder = $db->table('products');
$result = $builder->where('id', $id)->update($data);
if ($result) {
session()->setFlashdata('success', 'Produit mis à jour avec succès.');
return redirect()->to('/products');
} else {
log_message('error', 'Update produit failed - DB error: ' . $db->error()['message']);
session()->setFlashdata('errors', 'Une erreur est survenue lors de la mise à jour.');
return redirect()->to('/products/update/' . $id);
}
} catch (\Exception $e) {
log_message('error', 'Erreur update produit: ' . $e->getMessage());
log_message('error', 'Data: ' . json_encode($data));
session()->setFlashdata('errors', 'Erreur: ' . $e->getMessage());
return redirect()->to('/products/update/' . $id);
}
} else {
// Affichage du formulaire

View File

@ -138,24 +138,30 @@ class RecouvrementController extends AdminController
"data" => []
];
foreach ($data as $key => $value) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
$hasActions = in_array('updateRecouvrement', $this->permission) || in_array('deleteRecouvrement', $this->permission);
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$result['data'][$key] = [
foreach ($data as $key => $value) {
$row = [
$value['recouvrement_id'],
number_format($value['recouvrement_montant'], 0, '.', ' '),
$value['recouvrement_date'],
$value['recouvrement_personnel'],
$value['send_money'],
$value['get_money'],
$buttons
];
if ($hasActions) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$row[] = $buttons;
}
$result['data'][$key] = $row;
}
return $this->response->setJSON($result);
}
@ -170,24 +176,30 @@ class RecouvrementController extends AdminController
"data" => []
];
foreach ($data as $key => $value) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
$hasActions = in_array('updateRecouvrement', $this->permission) || in_array('deleteRecouvrement', $this->permission);
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$result['data'][$key] = [
foreach ($data as $key => $value) {
$row = [
$value['recouvrement_id'],
number_format($value['recouvrement_montant'], 0, '.', ' '),
$value['recouvrement_date'],
$value['recouvrement_personnel'],
$value['send_money'],
$value['get_money'],
$buttons
];
if ($hasActions) {
$buttons = '';
if (in_array('updateRecouvrement', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#updateModal"><i class="fa fa-pencil"></i></button>';
}
if (in_array('deleteRecouvrement', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['recouvrement_id'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$row[] = $buttons;
}
$result['data'][$key] = $row;
}
return $this->response->setJSON($result);
}
@ -221,31 +233,8 @@ class RecouvrementController extends AdminController
"Montant: " . number_format($recouvrementData['recouvrement_montant'], 0, ',', ' ') . " Ar<br>" .
"Par: " . $users['firstname'] . ' ' . $users['lastname'];
// ✅ Notifier Direction, DAF et SuperAdmin de TOUS les stores
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification(
$message,
"Direction",
(int)$store['id'],
'recouvrement'
);
$Notification->createNotification(
$message,
"DAF",
(int)$store['id'],
'recouvrement'
);
$Notification->createNotification(
$message,
"SuperAdmin",
(int)$store['id'],
'recouvrement'
);
}
}
// ✅ Notifier tous les groupes ayant updateRecouvrement de TOUS les stores
$Notification->notifyGroupsByPermissionAllStores('notifRecouvrement', $message, 'recouvrement');
}
} catch (\Exception $e) {
log_message('error', 'Erreur notification removeRecouvrement: ' . $e->getMessage());
@ -333,34 +322,8 @@ class RecouvrementController extends AdminController
"Store: " . $this->returnStoreName($users['store_id']) . "<br>" .
"Par: " . $fullname;
// ✅ Notifier Direction, DAF et SuperAdmin de TOUS les stores
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
// Notifier Direction
$Notification->createNotification(
$message,
"Direction",
(int)$store['id'],
'recouvrement'
);
// Notifier DAF
$Notification->createNotification(
$message,
"DAF",
(int)$store['id'],
'recouvrement'
);
// Notifier SuperAdmin
$Notification->createNotification(
$message,
"SuperAdmin",
(int)$store['id'],
'recouvrement'
);
}
}
// ✅ Notifier tous les groupes ayant updateRecouvrement de TOUS les stores
$Notification->notifyGroupsByPermissionAllStores('notifRecouvrement', $message, 'recouvrement');
}
} catch (\Exception $e) {
log_message('error', 'Erreur notification createRecouvrement: ' . $e->getMessage());
@ -405,7 +368,7 @@ class RecouvrementController extends AdminController
$Recouvrement = new Recouvrement();
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
$data = [
'recouvrement_montant' => (int) $this->request->getPost('recouvrement_montant_edit'),
'recouvrement_date' => $this->request->getPost('recouvrement_date_edit')
@ -426,31 +389,8 @@ class RecouvrementController extends AdminController
"Nouveau montant: " . number_format($data['recouvrement_montant'], 0, ',', ' ') . " Ar<br>" .
"Par: " . $users['firstname'] . ' ' . $users['lastname'];
// ✅ Notifier Direction, DAF et SuperAdmin de TOUS les stores
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification(
$message,
"Direction",
(int)$store['id'],
'recouvrement'
);
$Notification->createNotification(
$message,
"DAF",
(int)$store['id'],
'recouvrement'
);
$Notification->createNotification(
$message,
"SuperAdmin",
(int)$store['id'],
'recouvrement'
);
}
}
// ✅ Notifier tous les groupes ayant updateRecouvrement de TOUS les stores
$Notification->notifyGroupsByPermissionAllStores('notifRecouvrement', $message, 'recouvrement');
}
} catch (\Exception $e) {
log_message('error', 'Erreur notification updateRecouvrement: ' . $e->getMessage());

View File

@ -5,8 +5,11 @@ namespace App\Controllers;
use App\Controllers\AdminController;
use App\Models\Notification;
use App\Models\Orders;
use App\Models\OrderItems;
use App\Models\Products;
use App\Models\Remise;
use App\Models\Stores;
use App\Models\Historique;
class RemiseController extends AdminController
{
@ -19,7 +22,10 @@ class RemiseController extends AdminController
public function index()
{
$this->verifyRole('viewRemise');
if (!in_array('viewRemise', $this->permission) && !in_array('validateRemise', $this->permission)) {
redirect()->to('/')->send();
exit();
}
$data = json_decode($this->fetchTotal(),true);
@ -58,8 +64,8 @@ class RemiseController extends AdminController
foreach ($data as $key => $value) {
$buttons = '';
// ✅ SEUL LE SUPERADMIN PEUT VALIDER/REFUSER
if ($role === 'SuperAdmin' && $value['demande_status'] == 'En attente') {
// ✅ TOUS LES GROUPES AYANT validateRemise PEUVENT VALIDER/REFUSER
if (in_array('validateRemise', $this->permission) && $value['demande_status'] == 'En attente') {
$buttons .= '<button type="submit" class="btn btn-success" onclick="valideFunc(' . $value['id_demande'] . ')">';
$buttons .= '<i class="fa fa-check-circle"></i>';
$buttons .= '</button>';
@ -69,11 +75,6 @@ class RemiseController extends AdminController
$buttons .= '</button>';
}
// ✅ DIRECTION ET DAF VOIENT SEULEMENT (pas de boutons d'action)
if (in_array($role, ['Direction', 'DAF'])) {
$buttons = '<span class="label label-info">Consultation uniquement</span>';
}
$result['data'][$key] = [
$value['id_demande'],
$value['product'],
@ -92,19 +93,10 @@ class RemiseController extends AdminController
*/
public function updateRemise($id_demande)
{
// ✅ VÉRIFIER QUE SEUL LE SUPERADMIN PEUT VALIDER
$this->verifyRole('validateRemise');
$session = session();
$user = $session->get('user');
$role = $user['group_name'] ?? '';
if ($role !== 'SuperAdmin') {
return $this->response->setJSON([
'success' => false,
'messages' => 'Seul le SuperAdmin peut valider ou refuser les demandes de remise.'
]);
}
$this->verifyRole('validateRemise');
$validation = \Config\Services::validation();
$data['page_title'] = $this->pageTitle;
@ -119,7 +111,7 @@ class RemiseController extends AdminController
$Remise = new Remise();
if ($this->request->getMethod() == 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
$today = date('Y-m-d');
$demande_status = $this->request->getPost('demande_status');
@ -160,42 +152,35 @@ class RemiseController extends AdminController
if ($demande_status == 'Refusé') {
if ($order_id) {
$ordersModel->update($order_id, ['paid_status' => 0]);
// Libérer les produits de la commande (retour en stock)
$orderItemsModel = new OrderItems();
$productModel = new Products();
$orderItems = $orderItemsModel->getOrdersItemData($order_id);
foreach ($orderItems as $item) {
if (!empty($item['product_id'])) {
$productModel->update($item['product_id'], ['product_sold' => 0]);
}
}
}
// Message de refus
$messageRefus = "❌ Demande de remise refusée : {$bill_no}<br>" .
"Produit : {$remise_product}<br>" .
"Décision : SuperAdmin";
"Décision : {$user['firstname']} {$user['lastname']} ({$user['group_name']})";
// Notifier le commercial du store concerné
$Notification->createNotification(
$messageRefus . "<br><em>Veuillez modifier la commande.</em>",
"COMMERCIALE",
(int)$store_id,
"orders"
);
// Notifier les commerciaux du store concerné
$Notification->notifyGroupsByPermission('notifRemise', $messageRefus . "<br><em>Veuillez modifier la commande.</em>", (int)$store_id, "orders");
// ✅ NOTIFIER DIRECTION ET DAF DE TOUS LES STORES (POUR INFORMATION)
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification(
$messageRefus . "<br><em>Pour information</em>",
"Direction",
(int)$store['id'],
'remise/'
);
$Notification->createNotification(
$messageRefus . "<br><em>Pour information</em>",
"DAF",
(int)$store['id'],
'remise/'
);
}
}
// ✅ NOTIFIER TOUS LES GROUPES AYANT validateRemise (POUR INFORMATION)
$Notification->notifyGroupsByPermissionAllStores('notifRemise', $messageRefus . "<br><em>Pour information</em>", 'remise/');
$message = 'La demande de remise a été refusée. Le commercial et les responsables ont été notifiés.';
// Log refus remise
$historique = new Historique();
$historique->logAction('remise', 'REFUSE', $id_demande, "Refus de la demande de remise - Facture: {$bill_no}");
} elseif ($demande_status == 'Accepté' || $demande_status == 'Validé') {
// ✅ SI ACCEPTÉ PAR LE SUPERADMIN
if ($order_id) {
@ -204,44 +189,22 @@ class RemiseController extends AdminController
$messageAcceptation = "✅ Demande de remise acceptée : {$bill_no}<br>" .
"Produit : {$remise_product}<br>" .
"Décision : SuperAdmin";
"Décision : {$user['firstname']} {$user['lastname']} ({$user['group_name']})";
// Notifier la Caissière du store concerné
$Notification->createNotification(
$messageAcceptation . "<br><em>Commande prête à être traitée</em>",
"Caissière",
(int)$store_id,
"orders"
);
// Notifier les caissières du store concerné
$Notification->notifyGroupsByPermission('notifSortieCaisse', $messageAcceptation . "<br><em>Commande prête à être traitée</em>", (int)$store_id, "orders");
// Notifier le commercial du store concerné
$Notification->createNotification(
$messageAcceptation,
"COMMERCIALE",
(int)$store_id,
"orders"
);
// Notifier les commerciaux du store concerné
$Notification->notifyGroupsByPermission('notifRemise', $messageAcceptation, (int)$store_id, "orders");
// ✅ NOTIFIER DIRECTION ET DAF DE TOUS LES STORES (POUR INFORMATION)
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification(
$messageAcceptation . "<br><em>Pour information</em>",
"Direction",
(int)$store['id'],
'remise/'
);
$Notification->createNotification(
$messageAcceptation . "<br><em>Pour information</em>",
"DAF",
(int)$store['id'],
'remise/'
);
}
}
// ✅ NOTIFIER TOUS LES GROUPES AYANT validateRemise (POUR INFORMATION)
$Notification->notifyGroupsByPermissionAllStores('notifRemise', $messageAcceptation . "<br><em>Pour information</em>", 'remise/');
$message = 'La demande de remise a été acceptée. La caissière, le commercial et les responsables ont été notifiés.';
// Log validation remise
$historique = new Historique();
$historique->logAction('remise', 'VALIDATE', $id_demande, "Validation de la demande de remise - Facture: {$bill_no}");
}
return $this->response->setJSON([

View File

@ -32,13 +32,36 @@ class ReportController extends AdminController
$today_year = $this->request->getPost('select_year');
}
// Récupérer les filtres
$startDate = $this->request->getPost('start_date') ?: null;
$endDate = $this->request->getPost('end_date') ?: null;
$storeId = $this->request->getPost('store_id') ?: null;
$userId = $this->request->getPost('user_id') ?: null;
// Fetch order data and years
$Reports = new Reports();
$Orders = new Orders();
$Store = new Stores();
$parking_data = $Reports->getOrderData($today_year);
$parking_data = $Reports->getOrderData($today_year, $startDate, $endDate, $storeId, $userId);
$data['report_years'] = $Reports->getOrderYear();
// Charger les stores et users pour les filtres
$data['stores'] = $Store->getActiveStore();
$db = \Config\Database::connect();
$data['users_list'] = $db->table('users u')
->select('u.id, u.firstname, u.lastname, g.group_name')
->join('user_group ug', 'u.id = ug.user_id')
->join('groups g', 'ug.group_id = g.id')
->whereIn('g.group_name', ['COMMERCIALE', 'Caissière', "Cheffe d'Agence"])
->orderBy('u.firstname', 'ASC')
->get()->getResultArray();
// Conserver les filtres sélectionnés
$data['selected_start_date'] = $startDate;
$data['selected_end_date'] = $endDate;
$data['selected_store_id'] = $storeId;
$data['selected_user_id'] = $userId;
// Process the parking data and calculate total amounts
$final_parking_data = [];
@ -47,7 +70,6 @@ class ReportController extends AdminController
if (!empty($orders)) {
foreach ($orders as $order) {
// Utiliser le montant selon la logique : discount si existe, sinon gross_amount
if (!empty($order['discount']) && $order['discount'] > 0) {
$total_amount_earned += (float) $order['discount'];
} else {
@ -58,18 +80,19 @@ class ReportController extends AdminController
$final_parking_data[$month] = $total_amount_earned;
}
// Data for the camembert (pie chart)
$paymentModes = $Orders->getPaymentModes();
$total_mvola1 = $paymentModes->total_mvola1;
$total_mvola2 = $paymentModes->total_mvola2;
$total_espece1 = $paymentModes->total_espece1;
$total_espece2 = $paymentModes->total_espece2;
$total_banque1 = $paymentModes->total_virement_bancaire1;
$total_banque2 = $paymentModes->total_virement_bancaire2;
$paymentModes = $Orders->getPaymentModes($startDate, $endDate, $storeId, $userId);
$total_mvola1 = $paymentModes->total_mvola1 ?? 0;
$total_mvola2 = $paymentModes->total_mvola2 ?? 0;
$total_espece1 = $paymentModes->total_espece1 ?? 0;
$total_espece2 = $paymentModes->total_espece2 ?? 0;
$total_banque1 = $paymentModes->total_virement_bancaire1 ?? 0;
$total_banque2 = $paymentModes->total_virement_bancaire2 ?? 0;
$total_mvola = $total_mvola1 + $total_mvola2;
$total_banque = $total_banque1 + $total_banque2;
$total_espece = $total_espece1 + $total_espece2;
$totalOrders = $Orders->getTotalOrders();
$totalOrders = $Orders->getTotalOrders($startDate, $endDate, $storeId, $userId);
$totalAmountPerPaymentModes = ["MVOLA" => $total_mvola, "Espece" => $total_espece, "Virement Bancaire" => $total_banque];
$totalOrdersCount = (int) $totalOrders->total_orders;
$labels = [];
@ -87,10 +110,10 @@ class ReportController extends AdminController
// Prepare data for product chart
$OrderItem = new OrderItems();
$productTable = $OrderItem->getAllSoldProductToday();
$productTable = $OrderItem->getAllSoldProductToday($startDate, $endDate, $storeId, $userId);
$product_sold = (int) $productTable->total_product_sold;
$unsold_product = (int) $productTable->total_unsold_product;
$product_sold = (int) ($productTable->total_product_sold ?? 0);
$unsold_product = (int) ($productTable->total_unsold_product ?? 0);
$labels1 = ["Produits vendus", "Produits non vendus"];
$totals2 = [$product_sold, $unsold_product];
@ -104,8 +127,8 @@ class ReportController extends AdminController
$data['results'] = $final_parking_data;
// Data for the camembert in dashboard
$totalStoreOrder = $Orders->getTotalOrderPerStore();
$totalOrders = $Orders->getTotalOrders();
$totalStoreOrder = $Orders->getTotalOrderPerStore($startDate, $endDate, $storeId, $userId);
$totalOrders = $Orders->getTotalOrders($startDate, $endDate, $storeId, $userId);
$totalOrdersCount = (int) $totalOrders->total_orders;
// Initialisation des variables pour éviter l'erreur "Undefined variable"
@ -115,7 +138,7 @@ class ReportController extends AdminController
foreach ($totalStoreOrder as $totalOrdersInStore) {
$storeList = $Store->getStoreById($totalOrdersInStore->store_id);
$labelStore[] = $storeList->name ?? 'Inconnu';
$totalPerStore[] = ((int) $totalOrdersInStore->total / $totalOrdersCount) * 100;
$totalPerStore[] = ($totalOrdersCount > 0) ? ((int) $totalOrdersInStore->total / $totalOrdersCount) * 100 : 0;
}
$data['labelStore'] = json_encode($labelStore);
@ -169,13 +192,13 @@ class ReportController extends AdminController
{
$Orders = new Orders();
$productVente = $Orders->getTotalProductvente2($id);
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$productVente = $Orders->getTotalProductvente2($id, $startDate, $endDate);
$result = ['data' => []];
foreach ($productVente as $key => $value) {
// die(var_dump($value)); // Debugging: Check what $value contains
// Add the row data
$result['data'][$key] = [
$value->sku,
$value->date_time,
@ -183,7 +206,6 @@ class ReportController extends AdminController
];
}
// Return data in JSON format
return $this->response->setJSON($result);
}
@ -201,7 +223,7 @@ class ReportController extends AdminController
{
$Products = new Products();
$produitStock = $Products->getProductData2($id);
$produitStock = $Products->getStockByBrand($id);
$result = ['data' => []];
foreach ($produitStock as $key => $value) {
@ -223,15 +245,13 @@ class ReportController extends AdminController
{
$Products = new Orders();
$produitStock = $Products->getOrderVendue();
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$produitStock = $Products->getOrderVendue($id, $startDate, $endDate);
$result = ['data' => []];
// echo '<pre>';
// die(var_dump($produitStock));
foreach ($produitStock as $key => $value) {
// die(var_dump($value)); // Debugging: Check what $value contains
// Add the row data
$result['data'][$key] = [
$value['sku'],
$value['qty'],
@ -241,7 +261,6 @@ class ReportController extends AdminController
];
}
// Return data in JSON format
return $this->response->setJSON($result);
}
@ -251,10 +270,24 @@ class ReportController extends AdminController
$data['page_title'] = $this->pageTitle;
$Stores = new Stores();
// echo '<pre>';
// die(var_dump($orderTest));
$data['stores'] = $Stores->getActiveStore();
// Récupérer les commerciaux et mécaniciens pour les filtres
$db = \Config\Database::connect();
$data['commerciaux'] = $db->table('users u')
->select('u.id, u.firstname, u.lastname')
->join('user_group ug', 'u.id = ug.user_id')
->join('groups g', 'ug.group_id = g.id')
->where('g.group_name', 'COMMERCIALE')
->get()->getResultArray();
$data['mecaniciens'] = $db->table('users u')
->select('u.id, u.firstname, u.lastname')
->join('user_group ug', 'u.id = ug.user_id')
->join('groups g', 'ug.group_id = g.id')
->where('g.group_name', 'MECANICIEN')
->get()->getResultArray();
return $this->render_template('reports/performance', $data);
}
@ -265,32 +298,27 @@ class ReportController extends AdminController
$session = session();
$users = $session->get('user');
// ✅ RÉCUPÉRER LES PARAMÈTRES DE FILTRE
// Récupérer les paramètres de filtre
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$pvente = $this->request->getGet('pvente');
// ✅ CORRECTION : Bonne concaténation des chaînes
log_message('debug', 'Filtres Commercial reçus - startDate: ' . $startDate . ', endDate: ' . $endDate . ', pvente: ' . $pvente);
$commercialId = $this->request->getGet('commercial');
// Pour Direction et Conseil : afficher TOUTES les performances AVEC FILTRES
if ($users['group_name'] === "DAF" || $users['group_name'] === "Direction" || $users['group_name'] === "SuperAdmin") {
// ✅ PASSER LES FILTRES AU MODÈLE - UNIQUEMENT POUR L'ADMIN
$orderPaid = $Orders->getPerformanceByOrders($startDate, $endDate, $pvente);
$orderPaid = $Orders->getPerformanceByOrders($startDate, $endDate, $pvente, $commercialId);
foreach ($orderPaid as $key => $value) {
// Déterminer le prix de vente réel
$prix_vente_reel = (!empty($value['discount']) && $value['discount'] > 0)
? $value['discount']
: $value['prix_vente'];
// Calculer le bénéfice
$benefice = $prix_vente_reel - $value['price'];
$result['data'][$key] = [
$value['firstname'] . ' ' . $value['lastname'],
$value['email'],
($value['sku'] == "" ? $value['motoname'] : $value['sku']),
(new DateTime($value['datevente']))->format('Y-m-d'),
(new \DateTime($value['datevente']))->format('Y-m-d'),
number_format($value['price'], 0, '.', ' '),
number_format($prix_vente_reel, 0, '.', ' '),
$this->returnName($value['store_id']),
@ -517,7 +545,7 @@ class ReportController extends AdminController
$imageUrl, // 0 - Image
$order['bill_no'] ?? 'N/A', // 1 - N° Facture
$product['name'], // 2 - Désignation
$product['sku'], // 3 - UGS
$product['sku'], // 3 - N° SERIE
$brandName, // 4 - Marque
$order['customer_name'], // 5 - Client
$agentName, // 6 - Agent Sécurité

View File

@ -6,6 +6,7 @@ use App\Models\Securite;
use App\Models\Products;
use App\Models\Orders;
use App\Models\Stores;
use App\Models\Historique;
class SecuriteController extends AdminController
{
@ -48,7 +49,7 @@ class SecuriteController extends AdminController
// Bouton daction
$buttons = in_array('validateCommande1', $this->permission)
? '<button type="button" class="btn btn-success" onclick="editFunc(' . $securite['id'] . ')" data-toggle="modal" data-target="#editModal"><i class="fa fa-check"></i></button>'
? '<button type="button" class="btn btn-success" onclick="editFunc(' . $securite['id'] . ')"><i class="fa fa-check"></i></button>'
: '';
// Statut
@ -60,7 +61,7 @@ class SecuriteController extends AdminController
$result['data'][] = [
'image' => $img,
'ugs' => esc($product['sku']),
'num_serie' => esc($product['sku']),
'designation' => esc($product['name']),
'statut' => $statut,
'action' => $buttons
@ -87,7 +88,7 @@ class SecuriteController extends AdminController
$response = [
'image' => base_url('assets/images/product_image/' . $product['image']),
'nom' => $product['name'],
'ugs' => $product['sku'],
'num_serie' => $product['sku'],
'bill_no' => $data['bill_no'],
'customer_name' => $order_data['customer_name'],
'customer_address' => $order_data['customer_address'],
@ -102,7 +103,7 @@ class SecuriteController extends AdminController
public function update(int $id)
{
$this->verifyRole('updateCommande1');
$storeModel = new Securite();
$securiteModel = new Securite();
$post = $this->request->getPost();
$response = [];
@ -114,11 +115,39 @@ class SecuriteController extends AdminController
$session = session();
$users = $session->get('user');
$Notification = new NotificationController();
if ($storeModel->updateSecurite($data, $id)) {
if ($securiteModel->updateSecurite($data, $id)) {
if ($post['status'] === "Validé") {
$Notification->createNotification('Une commande a été validé', "COMMERCIALE",(int)$users['store_id'], 'orders');
// ✅ Récupérer les infos de la ligne securite
$securiteData = $securiteModel->getSecuriteData($id);
if ($securiteData) {
// ✅ Marquer le produit comme vendu (product_sold = 1)
$productModel = new Products();
$productModel->update($securiteData['product_id'], ['product_sold' => 1]);
// ✅ Mettre à jour la commande liée (paid_status = 3 = livré)
if (!empty($securiteData['bill_no'])) {
$orderModel = new Orders();
$order = $orderModel->getOrdersDataByBillNo($securiteData['bill_no']);
if ($order) {
$orderModel->update($order['id'], [
'paid_status' => 3,
'delivered_by' => $users['id'],
'delivered_at' => date('Y-m-d H:i:s')
]);
}
$response = ['success' => true, 'messages' => 'Mise à jour réussie'];
}
}
$Notification->notifyGroupsByPermission('notifRemise', 'Une commande a été validée et livrée', (int)$users['store_id'], 'orders');
}
// Log de l'action livraison
$historique = new Historique();
$billNo = $securiteData['bill_no'] ?? 'N/A';
$historique->logAction('securite', 'DELIVERY', $id, "Confirmation de livraison - Facture: {$billNo}");
$response = ['success' => true, 'messages' => 'Livraison confirmée avec succès'];
} else {
$response = ['success' => false, 'messages' => 'Erreur en base lors de la mise à jour'];
}

View File

@ -574,16 +574,7 @@ public function markAsPaid($id_sortie)
"Store: " . $this->returnStoreName($decaissement['store_id']) . "<br>" .
"Nouveau solde " . $mode_paiement_label . ": " . number_format($fonds_disponible - $montant_retire, 0, ',', ' ') . " Ar";
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification($message, "Direction", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "DAF", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'sortieCaisse');
}
}
$Notification->notifyGroupsByPermissionAllStores('notifSortieCaisse', $message, 'sortieCaisse');
}
} catch (\Exception $e) {
log_message('error', 'Erreur notification markAsPaid: ' . $e->getMessage());
@ -856,16 +847,7 @@ public function markAsPaid($id_sortie)
"Store: " . $this->returnStoreName($user['store_id']) . "<br>" .
"Demandeur: " . $user['firstname'] . ' ' . $user['lastname'];
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) {
$Notification->createNotification($message, "Direction", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "DAF", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'sortieCaisse');
}
}
$Notification->notifyGroupsByPermissionAllStores('notifSortieCaisse', $message, 'sortieCaisse');
}
} catch (\Exception $e) {
log_message('error', 'Erreur notification createSortieCaisse: ' . $e->getMessage());
@ -899,7 +881,7 @@ public function markAsPaid($id_sortie)
$data['page_title'] = $this->pageTitle;
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
$validation = \Config\Services::validation();
$validation->setRules([
@ -1032,7 +1014,7 @@ public function markAsPaid($id_sortie)
$data['page_title'] = $this->pageTitle;
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
$data = [
'admin_raison' => $this->request->getPost('admin_raison'),
'statut' => $this->request->getPost('statut'),
@ -1054,15 +1036,15 @@ public function markAsPaid($id_sortie)
switch ($statut) {
case "Valider":
$message = "✅ Le décaissement a été validé par la Direction<br>Store: " . $this->returnStoreName($store_id);
$Notification->createNotification($message, "Caissière", (int)$store_id, 'sortieCaisse');
$Notification->notifyGroupsByPermission('notifSortieCaisse', $message, (int)$store_id, 'sortieCaisse');
break;
case "Refuser":
$message = "❌ Le décaissement a été refusé par la Direction<br>Store: " . $this->returnStoreName($store_id) . "<br>Raison: " . $this->request->getPost('admin_raison');
$Notification->createNotification($message, "Caissière", (int)$store_id, 'sortieCaisse');
$Notification->notifyGroupsByPermission('notifSortieCaisse', $message, (int)$store_id, 'sortieCaisse');
break;
case "En attente":
$message = "⏳ Le décaissement a été mis en attente par la Direction<br>Store: " . $this->returnStoreName($store_id);
$Notification->createNotification($message, "Caissière", (int)$store_id, 'sortieCaisse');
$Notification->notifyGroupsByPermission('notifSortieCaisse', $message, (int)$store_id, 'sortieCaisse');
break;
}

View File

@ -209,7 +209,7 @@ class UserController extends AdminController
$data['page_title'] = $this->pageTitle;
// Check if it's a POST request before validating
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
// Load validation service and run validation
if (!$this->validate($validationRules)) {
@ -285,7 +285,7 @@ class UserController extends AdminController
// Check if the ID exists in the request
if ($id) {
// Check if it's a POST request before validating
if ($this->request->getMethod() === 'post') {
if (strtolower($this->request->getMethod()) === 'post') {
// Validate the form
if (!$this->validate($validationRules)) {
// Validation failed
@ -405,7 +405,7 @@ class UserController extends AdminController
]);
$Users = new Users();
// If validation passes for the first set of rules
if ($this->request->getMethod() === 'post' && $validation->withRequest($this->request)->run()) {
if (strtolower($this->request->getMethod()) === 'post' && $validation->withRequest($this->request)->run()) {
// Handle the case when password is not being updated
if (empty($this->request->getPost('password')) && empty($this->request->getPost('cpassword'))) {

View File

@ -189,7 +189,7 @@ class Avance extends Model {
}
// ✅ CORRECTION : getTotalAvance pour la caissière
public function getTotalAvance() {
public function getTotalAvance($startDate = null, $endDate = null) {
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction','DAF']);
@ -197,10 +197,16 @@ class Avance extends Model {
try {
$builder = $this->select('SUM(avance_amount) AS ta')
->where('is_order', 0)
->where('active', 1); // ✅ Ajout du filtre active
->where('active', 1);
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']); // ✅ Filtre par store pour caissière
$builder->where('store_id', $users['store_id']);
}
if ($startDate) {
$builder->where('DATE(avance_date) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(avance_date) <=', $endDate);
}
return $builder->get()->getRowObject();
@ -212,7 +218,7 @@ class Avance extends Model {
// ✅ CORRECTION PRINCIPALE : getPaymentModesAvance pour la caissière
// ✅ MODIFICATION : Ne compter QUE les avances VALIDÉES
public function getPaymentModesAvance()
public function getPaymentModesAvance($startDate = null, $endDate = null)
{
$session = session();
$users = $session->get('user');
@ -226,14 +232,19 @@ public function getPaymentModesAvance()
SUM(CASE WHEN LOWER(type_payment) = "en espèce" THEN avance_amount ELSE 0 END) AS total_espece,
SUM(CASE WHEN LOWER(type_payment) = "virement bancaire" THEN avance_amount ELSE 0 END) AS total_virement_bancaire
')
->where('validated', 1) // ✅ AJOUT : Uniquement les avances validées
->where('validated', 1)
->where('active', 1)
->where('is_order', 0);
// ✅ Filtre par store pour non-admin
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']);
}
if ($startDate) {
$builder->where('DATE(avance_date) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(avance_date) <=', $endDate);
}
$result = $builder->get()->getRowObject();
@ -442,9 +453,11 @@ public function getPaymentModesAvance()
$isCommercial = in_array($users['group_name'], ['COMMERCIALE']);
$isCaissier = in_array($users['group_name'], ['Caissière']);
$builder = $this->where('is_order', 0)
$builder = $this->where('amount_due', 0)
->groupStart()
->where('active', 1)
->where('amount_due', 0);
->orWhere('is_order', 1)
->groupEnd();
if ($isCommercial) {
$builder->where('user_id', $users['id']);

View File

@ -13,7 +13,7 @@ class Company extends Model
protected $table = 'company';
// List all the fields that are allowed to be updated or inserted
protected $allowedFields = [
'company_name', 'service_charge_value', 'vat_charge_value', 'address', 'phone', 'phone2', 'NIF', 'STAT', 'country', 'message', 'currency',
'company_name', 'service_charge_value', 'vat_charge_value', 'address', 'phone', 'phone2', 'NIF', 'STAT', 'country', 'message', 'currency', 'logo',
];
/**

View File

@ -99,4 +99,22 @@ class Groups extends Model
->get()
->getRowArray();
}
/**
* Get all groups that have a specific permission
* @param string $permission
* @return array
*/
public function getGroupsWithPermission(string $permission): array
{
$groups = $this->findAll();
$result = [];
foreach ($groups as $group) {
$perms = @unserialize($group['permission'] ?? '');
if (is_array($perms) && in_array($permission, $perms)) {
$result[] = $group;
}
}
return $result;
}
}

View File

@ -16,6 +16,8 @@ class Historique extends Model
'sku',
'store_name',
'description',
'user_id',
'user_name',
'created_at'
];
protected $useTimestamps = false;
@ -111,6 +113,32 @@ class Historique extends Model
return $this->insert($data);
}
/**
* Enregistrer une action utilisateur dans l'historique
*/
public function logAction($tableName, $action, $rowId, $description)
{
$session = session();
$user = $session->get('user');
$data = [
'table_name' => $tableName,
'action' => $action,
'row_id' => $rowId,
'product_name' => null,
'sku' => null,
'store_name' => $user['store_name'] ?? null,
'description' => $description,
'user_id' => $user['id'] ?? null,
'user_name' => isset($user['firstname'], $user['lastname'])
? $user['firstname'] . ' ' . $user['lastname']
: ($user['username'] ?? 'Système'),
'created_at' => date('Y-m-d H:i:s')
];
return $this->insert($data);
}
/**
* Nettoyer l'historique ancien (plus de X jours)
*/
@ -164,7 +192,7 @@ class Historique extends Model
{
$data = $this->getHistoriqueWithFilters($filters);
$csvData = "ID,Table,Action,ID Produit,Nom Produit,SKU,Magasin,Description,Date/Heure\n";
$csvData = "ID,Table,Action,ID Produit,Nom Produit,N° de série,Magasin,Description,Date/Heure\n";
foreach ($data as $row) {
$csvData .= '"' . $row['id'] . '",';

View File

@ -24,15 +24,32 @@ class OrderItems extends Model
return $this->where('order_id', $order_id)->findAll();
}
public function getAllSoldProductToday() {
return $this->select('
public function getAllSoldProductToday($startDate = null, $endDate = null, $storeId = null, $userId = null) {
$builder = $this->select('
COUNT(orders_item.id) as total_product_sold,
(SELECT SUM(products.qty) FROM products) as total_unsold_product
')
->join('orders', 'orders_item.order_id = orders.id')
->where('DATE(orders.date_time)', date('Y-m-d'))
->get()
->getRow();
->join('orders', 'orders_item.order_id = orders.id');
if (!empty($startDate) || !empty($endDate) || !empty($storeId) || !empty($userId)) {
if (!empty($startDate)) {
$builder->where('DATE(orders.date_time) >=', $startDate);
}
if (!empty($endDate)) {
$builder->where('DATE(orders.date_time) <=', $endDate);
}
} else {
$builder->where('DATE(orders.date_time)', date('Y-m-d'));
}
if (!empty($storeId)) {
$builder->where('orders.store_id', $storeId);
}
if (!empty($userId)) {
$builder->where('orders.user_id', $userId);
}
return $builder->get()->getRow();
}
public function getSumOrdersItemData($order_id = null)

View File

@ -124,6 +124,7 @@ class Orders extends Model
->join('orders_item', 'orders_item.order_id = orders.id', 'left')
->join('products', 'products.id = orders_item.product_id', 'left')
->select("IFNULL(GROUP_CONCAT(DISTINCT products.name SEPARATOR '\\n'), '') AS product_names", false)
->select("COUNT(DISTINCT orders_item.id) AS item_count", false)
->groupBy('orders.id');
// Récupération du groupe de l'utilisateur
@ -274,15 +275,15 @@ class Orders extends Model
$orderItemModel->where('order_id', $id)->delete();
// Insert new items
$count_product = count($data['product']);
$count_product = is_array($data['product']) ? count($data['product']) : 0;
for ($x = 0; $x < $count_product; $x++) {
$items = [
'order_id' => $id,
'product_id' => $data['product'][$x],
'rate' => $data['rate_value'][$x],
'qty' => isset($data['qty'][$x]) ? (int)$data['qty'][$x] : 1,
'puissance' => $data['puissance'][$x],
'amount' => $data['amount_value'][$x],
'product_id' => is_array($data['product']) ? $data['product'][$x] : $data['product'],
'rate' => is_array($data['rate_value']) ? ($data['rate_value'][$x] ?? 0) : ($data['rate_value'] ?? 0),
'qty' => is_array($data['qty']) ? (int)($data['qty'][$x] ?? 1) : 1,
'puissance' => is_array($data['puissance']) ? ($data['puissance'][$x] ?? '') : ($data['puissance'] ?? ''),
'amount' => is_array($data['amount_value']) ? ($data['amount_value'][$x] ?? 0) : ($data['amount_value'] ?? 0),
];
$orderItemModel->insert($items);
@ -387,7 +388,7 @@ class Orders extends Model
->findAll();
}
public function getPaymentModes()
public function getPaymentModes($startDate = null, $endDate = null, $storeId = null, $userId = null)
{
$session = session();
$users = $session->get('user');
@ -464,28 +465,67 @@ class Orders extends Model
')
->whereIn('orders.paid_status', [1, 3]);
if (!$isAdmin) {
if (!empty($storeId)) {
$baseQuery->where('orders.store_id', $storeId);
} elseif (!$isAdmin) {
$baseQuery->where('orders.store_id', $users['store_id']);
}
if (!empty($userId)) {
$baseQuery->where('orders.user_id', $userId);
}
if ($startDate) {
$baseQuery->where('DATE(orders.date_time) >=', $startDate);
}
if ($endDate) {
$baseQuery->where('DATE(orders.date_time) <=', $endDate);
}
return $baseQuery->get()->getRowObject();
}
public function getTotalOrders()
public function getTotalOrders($startDate = null, $endDate = null, $storeId = null, $userId = null)
{
return $this->db->table('orders')
->select(' COUNT(*) as total_orders')
->get()
->getRow();
$builder = $this->db->table('orders')
->select('COUNT(*) as total_orders');
if (!empty($storeId)) {
$builder->where('store_id', $storeId);
}
if (!empty($userId)) {
$builder->where('user_id', $userId);
}
if (!empty($startDate)) {
$builder->where('DATE(date_time) >=', $startDate);
}
if (!empty($endDate)) {
$builder->where('DATE(date_time) <=', $endDate);
}
public function getTotalOrderPerStore()
return $builder->get()->getRow();
}
public function getTotalOrderPerStore($startDate = null, $endDate = null, $storeId = null, $userId = null)
{
return $this->db->table('orders')
$builder = $this->db->table('orders')
->select('store_id, COUNT(*) as total')
->groupBy('store_id')
->get()
->getResult();
->groupBy('store_id');
if (!empty($storeId)) {
$builder->where('store_id', $storeId);
}
if (!empty($userId)) {
$builder->where('user_id', $userId);
}
if (!empty($startDate)) {
$builder->where('DATE(date_time) >=', $startDate);
}
if (!empty($endDate)) {
$builder->where('DATE(date_time) <=', $endDate);
}
return $builder->get()->getResult();
}
public function getPaymentModesPercentage()
@ -549,35 +589,31 @@ class Orders extends Model
return $order;
}
public function getTotalProductvente2(?int $id)
public function getTotalProductvente2(?int $id, $startDate = null, $endDate = null)
{
$builder = $this->db->table('orders')
->select('orders.id, orders.paid_status, orders.store_id, orders.date_time, brands.name, products.name as Pname, products.sku')
->join('orders_item', 'orders.id = orders_item.order_id')
->join('products', 'orders_item.product_id = products.id')
->join('brands', 'brands.id = products.marque')
->whereIn('orders.paid_status', [1, 3]);
if ($id != 0) {
$order = $this->db->table('orders')
->select('orders.id, orders.paid_status, orders.store_id, orders.date_time, brands.name, products.name as Pname, products.sku')
->join('orders_item', 'orders.id = orders_item.order_id')
->join('products', 'orders_item.product_id = products.id')
->join('brands', 'brands.id = products.marque')
->whereIn('orders.paid_status', [1, 3]) // ← CHANGÉ ICI
->where('orders.store_id', $id)
->get()
->getResult();
} else {
$order = $this->db->table('orders')
->select('orders.id, orders.paid_status, orders.store_id, orders.date_time, brands.name, products.name as Pname, products.sku')
->join('orders_item', 'orders.id = orders_item.order_id')
->join('products', 'orders_item.product_id = products.id')
->join('brands', 'brands.id = products.marque')
->whereIn('orders.paid_status', [1, 3]) // ← CHANGÉ ICI
->get()
->getResult();
$builder->where('orders.store_id', $id);
}
if (!empty($startDate)) {
$builder->where('DATE(orders.date_time) >=', $startDate);
}
if (!empty($endDate)) {
$builder->where('DATE(orders.date_time) <=', $endDate);
}
return $order;
return $builder->get()->getResult();
}
public function getOrderVendue()
public function getOrderVendue($storeId = null, $startDate = null, $endDate = null)
{
$order = $this->db->table('products')
$builder = $this->db->table('products')
->select('
products.*,
products.name as Pname,
@ -588,17 +624,25 @@ class Orders extends Model
ELSE orders.gross_amount
END as totalNet,
orders.date_time as DateTime
', false) // ← MODIFIÉ ICI
', false)
->join('orders_item', 'products.id = orders_item.product_id')
->join('orders', 'orders_item.order_id = orders.id')
->whereIn('orders.paid_status', [1, 3]) // ← ET ICI
->get()
->getResultArray();
->whereIn('orders.paid_status', [1, 3]);
return $order;
if (!empty($storeId) && $storeId != 0) {
$builder->where('orders.store_id', $storeId);
}
if (!empty($startDate)) {
$builder->where('DATE(orders.date_time) >=', $startDate);
}
if (!empty($endDate)) {
$builder->where('DATE(orders.date_time) <=', $endDate);
}
public function getPerformanceByOrders($startDate = null, $endDate = null, $pvente = null)
return $builder->get()->getResultArray();
}
public function getPerformanceByOrders($startDate = null, $endDate = null, $pvente = null, $commercialId = null)
{
$builder = $this->db->table('orders')
->select('orders.id as orderid, orders.net_amount, orders.date_time as datevente, orders.discount, products.price, products.sku, products.prix_vente, products.name as motoname, stores.id as store_id, users.firstname, users.lastname, users.email')
@ -608,7 +652,6 @@ class Orders extends Model
->join('users', 'users.id = orders.user_id')
->whereIn('orders.paid_status', [1, 3]);
// ✅ AJOUT : FILTRES PAR DATE
if (!empty($startDate) && !empty($endDate)) {
$builder->where('DATE(orders.date_time) >=', $startDate);
$builder->where('DATE(orders.date_time) <=', $endDate);
@ -618,11 +661,14 @@ class Orders extends Model
$builder->where('DATE(orders.date_time) <=', $endDate);
}
// ✅ AJOUT : FILTRE PAR POINT DE VENTE
if (!empty($pvente) && $pvente !== 'TOUS') {
$builder->where('stores.name', $pvente);
}
if (!empty($commercialId) && $commercialId !== 'TOUS') {
$builder->where('orders.user_id', $commercialId);
}
$builder->orderBy('orders.date_time', 'DESC');
return $builder->get()->getResultArray();

View File

@ -21,7 +21,7 @@ class Products extends Model
$isAdmin = in_array($user['group_name'], ['SuperAdmin', 'Direction', 'DAF']);
$builder = $this->where('is_piece', 0)
->where('product_sold', 0);
->whereIn('product_sold', [0, 2]);
if (!$isAdmin) {
if (empty($user['store_id']) || $user['store_id'] == 0) {
@ -58,11 +58,27 @@ class Products extends Model
return $builder->join('brands', 'brands.id = products.marque')
->orderBy('products.id', 'DESC')
->select('brands.name as brand_name,COUNT( products.id) as total_product, products.store_id as store_id,products.*')
->groupBy('store_id')
->select('brands.name as brand_name, products.store_id as store_id, products.*')
->findAll();
}
public function getStockByBrand(int $id = 0)
{
$builder = $this->db->table('products')
->select('brands.name as brand_name, products.store_id, COUNT(*) as total_product')
->join('brands', 'brands.id = products.marque')
->where('products.is_piece', 0)
->where('products.product_sold', 0);
if ($id > 0) {
$builder->where('products.store_id', $id);
}
return $builder->groupBy('brands.name, products.store_id')
->orderBy('total_product', 'DESC')
->get()->getResultArray();
}
public function getProductData3(int $id)
{
if ($id == 0) {
@ -75,7 +91,7 @@ class Products extends Model
public function getProductDataStore(int $store_id, bool $excludeAvance = true, int $currentProductId = null)
{
$builder = $this->where('is_piece', 0)
->where('product_sold', 0)
->whereIn('product_sold', [0, 2])
->where('availability', 1)
->where('store_id', $store_id);
@ -92,11 +108,12 @@ class Products extends Model
$builder->where("id NOT IN ($subQueryAvances)", null, false);
}
// ✅ LISTE : Exclure TOUS les produits ayant une commande (statuts 1, 2, 3)
// ✅ LISTE : Exclure uniquement les produits dont la commande est LIVRÉE (paid_status = 3)
// Les produits commandés mais non livrés restent visibles dans les produits disponibles
$subQueryOrders = $db->table('orders_item')
->select('orders_item.product_id')
->join('orders', 'orders.id = orders_item.order_id')
->whereIn('orders.paid_status', [1, 2, 3]) // ✅ Disparaît de la liste dès qu'il y a une commande
->where('orders.paid_status', 3)
->getCompiledSelect();
$builder->where("id NOT IN ($subQueryOrders)", null, false);

View File

@ -16,10 +16,9 @@ class Remise extends Model
$session = session();
$users = $session->get('user');
// ✅ SUPERADMIN VOIT TOUTES LES DEMANDES "EN ATTENTE" (POUR VALIDATION)
// ✅ SUPERADMIN VOIT TOUTES LES DEMANDES (TOUS STATUTS)
if ($users["group_name"] === "SuperAdmin") {
return $this->where('demande_status', 'En attente')
->orderBy('date_demande', 'DESC')
return $this->orderBy('date_demande', 'DESC')
->findAll();
}

View File

@ -39,13 +39,28 @@ class Reports extends Model
* @param mixed $year
* @return array<array>
*/
public function getOrderData($year)
public function getOrderData($year, $startDate = null, $endDate = null, $storeId = null, $userId = null)
{
if ($year) {
$months = $this->months();
// Fetch orders with paid_status = 1 or 3
$result = $this->whereIn('paid_status', [1, 3])->findAll();
$builder = $this->whereIn('paid_status', [1, 3]);
if (!empty($storeId)) {
$builder->where('store_id', $storeId);
}
if (!empty($userId)) {
$builder->where('user_id', $userId);
}
if (!empty($startDate)) {
$builder->where('DATE(date_time) >=', $startDate);
}
if (!empty($endDate)) {
$builder->where('DATE(date_time) <=', $endDate);
}
$result = $builder->findAll();
$final_data = [];
@ -56,11 +71,10 @@ class Reports extends Model
foreach ($result as $v) {
$month_year = date('Y-m', strtotime($v['date_time']));
if ($get_mon_year == $month_year) {
// Modifier le montant selon votre logique
if (!empty($v['discount']) && $v['discount'] > 0) {
$v['amount_to_display'] = $v['discount']; // Utiliser le rabais
$v['amount_to_display'] = $v['discount'];
} else {
$v['amount_to_display'] = $v['gross_amount']; // Utiliser gross_amount
$v['amount_to_display'] = $v['gross_amount'];
}
$final_data[$get_mon_year][] = $v;
}

View File

@ -12,7 +12,7 @@ class Securite extends Model
*/
protected $table = 'securite';
protected $primaryKey = 'id'; // Primary key column
protected $allowedFields = ['product_id', 'status', 'date', 'active'];
protected $allowedFields = ['product_id', 'bill_no', 'store_id', 'status', 'date', 'active'];

View File

@ -165,25 +165,24 @@ class SortieCaisse extends Model
/**
* MODIFICATION : DAF, Direction, SuperAdmin voient TOUS les totaux
*/
public function getTotalSortieCaisse() {
public function getTotalSortieCaisse($startDate = null, $endDate = null) {
$session = session();
$users = $session->get('user');
// ✅ DAF, Direction, SuperAdmin : Voir TOUS les stores
$isAdmin = in_array($users['group_name'], ['Direction', 'DAF', 'SuperAdmin']);
if ($isAdmin) {
try {
return $this->select('
$builder = $this->select('
SUM(CASE WHEN mode_paiement = "En espèce" AND statut = "Payé" THEN montant_retire ELSE 0 END) AS total_espece,
SUM(CASE WHEN mode_paiement = "MVOLA" AND statut = "Payé" THEN montant_retire ELSE 0 END) AS total_mvola,
SUM(CASE WHEN mode_paiement = "Virement Bancaire" AND statut = "Payé" THEN montant_retire ELSE 0 END) AS total_virement,
SUM(CASE WHEN statut = "Payé" THEN montant_retire ELSE 0 END) AS mr
')
// ✅ CHANGEMENT : Uniquement statut = "Payé" (plus "Valider")
->where('statut', 'Payé')
->get()
->getRowObject();
->where('statut', 'Payé');
if ($startDate) { $builder->where('DATE(created_at) >=', $startDate); }
if ($endDate) { $builder->where('DATE(created_at) <=', $endDate); }
return $builder->get()->getRowObject();
} catch (\Exception $e) {
log_message('error', 'Erreur getTotalSortieCaisse (Admin) : ' . $e->getMessage());
return (object)[
@ -194,19 +193,18 @@ class SortieCaisse extends Model
];
}
} else {
// ✅ CAISSIÈRE : Uniquement son store ET statut "Payé"
try {
return $this->select('
$builder = $this->select('
SUM(CASE WHEN mode_paiement = "En espèce" AND statut = "Payé" THEN montant_retire ELSE 0 END) AS total_espece,
SUM(CASE WHEN mode_paiement = "MVOLA" AND statut = "Payé" THEN montant_retire ELSE 0 END) AS total_mvola,
SUM(CASE WHEN mode_paiement = "Virement Bancaire" AND statut = "Payé" THEN montant_retire ELSE 0 END) AS total_virement,
SUM(CASE WHEN statut = "Payé" THEN montant_retire ELSE 0 END) AS mr
')
->where('store_id', $users['store_id'])
// ✅ CHANGEMENT : Uniquement statut = "Payé" (plus "Valider")
->where('statut', 'Payé')
->get()
->getRowObject();
->where('statut', 'Payé');
if ($startDate) { $builder->where('DATE(created_at) >=', $startDate); }
if ($endDate) { $builder->where('DATE(created_at) <=', $endDate); }
return $builder->get()->getRowObject();
} catch (\Exception $e) {
log_message('error', 'Erreur getTotalSortieCaisse (Store) : ' . $e->getMessage());
return (object)[

View File

@ -0,0 +1,178 @@
<meta charset="UTF-8">
<div class="content-wrapper">
<section class="content-header">
<h1>
Historique des <small>Actions</small>
</h1>
<ol class="breadcrumb">
<li><a href="<?= base_url('/') ?>"><i class="fa fa-dashboard"></i> Accueil</a></li>
<li class="active">Historique des Actions</li>
</ol>
</section>
<section class="content">
<div id="messages"></div>
<!-- Filtres -->
<div class="box box-default">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-filter"></i> Filtres</h3>
<div class="box-tools pull-right">
<button type="button" class="btn btn-box-tool" data-widget="collapse"><i class="fa fa-minus"></i></button>
</div>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-2">
<label>Date de debut</label>
<input type="date" id="filterDateFrom" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-2">
<label>Date de fin</label>
<input type="date" id="filterDateTo" class="form-control" value="<?= date('Y-m-d') ?>">
</div>
<div class="col-md-2">
<label>Action</label>
<select id="filterAction" class="form-control">
<option value="">Toutes</option>
<option value="CREATE">Creation</option>
<option value="UPDATE">Modification</option>
<option value="DELETE">Suppression</option>
<option value="PAYMENT">Paiement</option>
<option value="VALIDATE">Validation</option>
<option value="REFUSE">Refus</option>
<option value="DELIVERY">Livraison</option>
<option value="ASSIGN_STORE">Assignation</option>
<option value="ENTRER">Entree</option>
<option value="SORTIE">Sortie</option>
<option value="LOGIN">Connexion</option>
</select>
</div>
<div class="col-md-2">
<label>Magasin</label>
<select id="filterStore" class="form-control">
<option value="">Tous</option>
<?php if (isset($stores) && is_array($stores)): ?>
<?php foreach ($stores as $store): ?>
<option value="<?= $store['name'] ?>"><?= $store['name'] ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-2">
<label>&nbsp;</label>
<div>
<button type="button" class="btn btn-primary btn-block" id="btnFilter">
<i class="fa fa-search"></i> Filtrer
</button>
</div>
</div>
<div class="col-md-2">
<label>&nbsp;</label>
<div>
<button type="button" class="btn btn-warning btn-block" id="btnReset">
<i class="fa fa-refresh"></i> Reinitialiser
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tableau -->
<div class="box">
<div class="box-header">
<div class="pull-right">
<button type="button" class="btn btn-success" id="btnExport">
<i class="fa fa-download"></i> Exporter CSV
</button>
</div>
<h3 class="box-title"><i class="fa fa-history"></i> Toutes les actions</h3>
</div>
<div class="box-body">
<table id="actionLogTable" class="table table-bordered table-striped" style="width:100%">
<thead>
<tr>
<th>Date</th>
<th>Utilisateur</th>
<th>Action</th>
<th>Module</th>
<th>Description</th>
</tr>
</thead>
</table>
</div>
</div>
</section>
</div>
<script>
$(document).ready(function() {
$("#mainActionLogNav").addClass('active');
$.extend(true, $.fn.dataTable.defaults, {
language: {
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sLengthMenu: "Afficher _MENU_ elements",
sInfo: "Affichage de l'element _START_ a _END_ sur _TOTAL_ elements",
sInfoEmpty: "Affichage de l'element 0 a 0 sur 0 element",
sInfoFiltered: "(filtre de _MAX_ elements au total)",
sLoadingRecords: "Chargement en cours...",
sZeroRecords: "Aucun element a afficher",
sEmptyTable: "Aucune donnee disponible",
oPaginate: {
sFirst: "Premier",
sPrevious: "Precedent",
sNext: "Suivant",
sLast: "Dernier"
}
}
});
var base_url = '<?= base_url() ?>';
function loadTable() {
if ($.fn.DataTable.isDataTable('#actionLogTable')) {
$('#actionLogTable').DataTable().destroy();
}
$('#actionLogTable').DataTable({
ajax: {
url: base_url + '/action-log/fetchData',
type: 'GET',
data: function(d) {
d.date_from = $('#filterDateFrom').val();
d.date_to = $('#filterDateTo').val();
d.action = $('#filterAction').val();
d.store_name = $('#filterStore').val();
}
},
order: [[0, 'desc']],
pageLength: 25
});
}
loadTable();
$('#btnFilter').on('click', function() {
loadTable();
});
$('#btnReset').on('click', function() {
$('#filterDateFrom').val('<?= date('Y-m-d') ?>');
$('#filterDateTo').val('<?= date('Y-m-d') ?>');
$('#filterAction').val('');
$('#filterStore').val('');
loadTable();
});
$('#btnExport').on('click', function() {
var params = '?date_from=' + $('#filterDateFrom').val()
+ '&date_to=' + $('#filterDateTo').val()
+ '&action=' + $('#filterAction').val()
+ '&store_name=' + $('#filterStore').val();
window.location.href = base_url + '/action-log/export' + params;
});
});
</script>

View File

@ -42,12 +42,16 @@ $isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Ca
<div class="col-md-3">
<button id="avance_no_order" class="btn btn-info w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-hourglass-half me-2"></i> Avances Incomplètes
<i class="fa fa-hourglass-half me-2"></i>
<span class="badge badge-light" id="incomplete-count">0</span>
Avances Incomplètes
</button>
</div>
<div class="col-md-3">
<button id="avance_order" class="btn btn-success w-100 rounded-pill shadow-sm py-2">
<i class="fa fa-check-circle me-2"></i> Avances Complètes
<i class="fa fa-check-circle me-2"></i>
<span class="badge badge-light" id="complete-count">0</span>
Avances Complètes
</button>
</div>
<div class="col-md-3">
@ -82,6 +86,7 @@ $isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Ca
<th>Téléphone</th>
<th>Adresse</th>
<th>Produit</th>
<th> Série</th>
<th>Prix</th>
<th>Avance</th>
<th>Reste à payer</th>
@ -96,6 +101,7 @@ $isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Ca
<tr>
<th>#</th>
<th>Produit</th>
<th> Série</th>
<th>Avance</th>
<th>Reste à payer</th>
<th>Date</th>
@ -500,6 +506,38 @@ $(document).ready(function() {
}
}
// Compteurs avances
function loadAvanceCounts() {
$.ajax({
url: base_url + 'avances/fetchAvanceData',
type: 'GET',
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#incomplete-count').text(count);
if (count > 0) {
$('#incomplete-count').removeClass('badge-light').addClass('badge-danger');
} else {
$('#incomplete-count').removeClass('badge-danger').addClass('badge-light');
}
}
});
$.ajax({
url: base_url + 'avances/fetchAvanceBecameOrder',
type: 'GET',
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#complete-count').text(count);
if (count > 0) {
$('#complete-count').removeClass('badge-light').addClass('badge-success');
} else {
$('#complete-count').removeClass('badge-success').addClass('badge-light');
}
}
});
}
// Variables de rôles
var isCaissier = <?php echo json_encode($isCaissier ?? false); ?>;
var isCommerciale = <?php echo json_encode($isCommerciale ?? false); ?>;
@ -516,6 +554,7 @@ $(document).ready(function() {
{ title: "Téléphone" },
{ title: "Adresse" },
{ title: "Produit" },
{ title: "N° Série" },
{
title: "Prix",
render: function(data, type, row) {
@ -542,14 +581,43 @@ $(document).ready(function() {
var manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns);
// Charger les compteurs
loadAvanceCounts();
setInterval(loadAvanceCounts, 30000);
$('#avance_no_order').on('click', function() {
$('#table-title').text('Avances Incomplètes');
manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns);
});
var adminCompletedColumns = [
{ title: "Client" },
{ title: "Téléphone" },
{ title: "Adresse" },
{ title: "Produit" },
{ title: "N° Série" },
{
title: "Prix",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{
title: "Avance",
render: function(data, type, row) {
return type === 'display' ? formatNumber(data) : data;
}
},
{ title: "Statut" },
{ title: "Date" }
<?php if (in_array('updateAvance', $user_permission) || in_array('deleteAvance', $user_permission)): ?>
,{ title: "Action", orderable: false, searchable: false }
<?php endif; ?>
];
$('#avance_order').on('click', function() {
$('#table-title').text('Avances Complètes');
manageTable = initAvanceTable(base_url + 'avances/fetchAvanceBecameOrder', adminColumns);
manageTable = initAvanceTable(base_url + 'avances/fetchAvanceBecameOrder', adminCompletedColumns);
});
$('#avance_expired').on('click', function() {
@ -563,6 +631,7 @@ $(document).ready(function() {
var userColumns = [
{ title: "#" },
{ title: "Produit" },
{ title: "N° Série" },
{
title: "Avance",
render: function(data, type, row) {
@ -593,10 +662,11 @@ $(document).ready(function() {
$('#avanceTable').DataTable().destroy();
}
// Reconstruire les headers (format caissière/commercial)
// Reconstruire les headers (format caissière/commercial)
rebuildTableHeaders([
'#',
'Produit',
'N° Série',
'Avance',
'Reste à payer',
'Date',
@ -629,19 +699,32 @@ $('#avance_order').on('click', function() {
rebuildTableHeaders([
'#',
'Produit',
'N° Série',
'Avance',
'Reste à payer',
'Statut',
'Date',
'Action'
]);
var completedUserColumns = [
{ title: "#" },
{ title: "Produit" },
{ title: "N° Série" },
{ title: "Avance" },
{ title: "Statut" },
{ title: "Date" }
<?php if (in_array('updateAvance', $user_permission) || in_array('deleteAvance', $user_permission)): ?>
,{ title: "Action", orderable: false, searchable: false }
<?php endif; ?>
];
manageTable = $('#avanceTable').DataTable({
ajax: {
url: base_url + 'avances/fetchAvanceBecameOrder',
type: 'GET',
dataSrc: 'data'
},
columns: userColumns,
columns: completedUserColumns,
language: datatableLangFr
});
@ -660,6 +743,7 @@ $('#avance_expired').on('click', function() {
rebuildTableHeaders([
'#',
'Produit',
'N° Série',
'Avance',
'Reste à payer',
'Date',
@ -679,8 +763,10 @@ $('#avance_expired').on('click', function() {
console.log('✅ DataTable Expirées initialisée');
});
// Charger le compteur d'avances en attente
// Charger les compteurs d'avances
loadPendingCount();
loadAvanceCounts();
setInterval(loadAvanceCounts, 30000);
setInterval(loadPendingCount, 30000);
console.log('✅ Module caissière initialisé');
@ -1547,18 +1633,17 @@ window.validateAvanceFunc = function(avance_id) {
};
// =====================================================
// 🔥 CHARGER COMPTEUR AVANCES EN ATTENTE
// CHARGER COMPTEURS AVANCES
// =====================================================
function loadPendingCount() {
$.ajax({
url: base_url + 'avances/fetchPendingValidation',
type: 'POST', // ✅ CORRECTION
type: 'POST',
dataType: 'json',
success: function(response) {
var count = response.data ? response.data.length : 0;
$('#pending-count').text(count);
if (count > 0) {
$('#pending-count').removeClass('badge-light').addClass('badge-danger');
$('#avance_pending').addClass('btn-pulse');
@ -1566,13 +1651,12 @@ function loadPendingCount() {
$('#pending-count').removeClass('badge-danger').addClass('badge-light');
$('#avance_pending').removeClass('btn-pulse');
}
},
error: function() {
console.error('Erreur chargement compteur avances en attente');
}
});
}
// loadAvanceCounts defini plus haut
// =====================================================
// 🔥 TRAITER AVANCES EXPIRÉES (ADMIN)
// =====================================================

View File

@ -27,6 +27,7 @@
<th>Prix</th>
<th>Puissances</th>
<th> Moteur</th>
<th>Disponibilité</th>
<th>Action</th>
</tr>
</thead>
@ -72,10 +73,15 @@ $.extend(true, $.fn.dataTable.defaults, {
'ajax': `<?= base_url('ventes/fetchProductVente') ?>/${id}`,
'order': [],
'columnDefs': [{
targets: 6,
targets: 7,
className: 'text-right'
} // Column index 3 corresponds to "Prix"
]
}
],
'createdRow': function(row, data, dataIndex) {
if (data[6] && data[6].indexOf('En attente de livraison') !== -1) {
$(row).css({'background-color': '#fff3cd', 'opacity': '0.85'});
}
}
});
})
</script>

View File

@ -34,7 +34,7 @@
<div class="box-header">
<h3 class="box-title">Gérer les informations sur l'entreprise</h3>
</div>
<form role="form" action="<?php base_url('company/update') ?>" method="post">
<form role="form" action="<?php base_url('company/update') ?>" method="post" enctype="multipart/form-data">
<div class="box-body">
<!-- Validation errors -->
@ -48,6 +48,19 @@
</div>
<?php endif; ?>
<div class="form-group">
<label>Logo de l'entreprise</label>
<div style="margin-bottom: 10px;">
<?php if (!empty($company_data['logo'])): ?>
<img src="<?= base_url($company_data['logo']) ?>?v=<?= time() ?>" alt="Logo" style="max-height: 80px; border: 1px solid #ddd; padding: 5px; border-radius: 4px;">
<?php else: ?>
<img src="<?= base_url('assets/images/company_logo.jpg') ?>?v=<?= time() ?>" alt="Logo par defaut" style="max-height: 80px; border: 1px solid #ddd; padding: 5px; border-radius: 4px; opacity: 0.4;">
<?php endif; ?>
</div>
<input type="file" name="logo" accept="image/*" class="form-control" style="line-height: normal; padding: 6px 12px; height: auto;">
<p class="help-block">Formats acceptes : JPG, PNG, GIF. Laissez vide pour garder le logo actuel.</p>
</div>
<div class="form-group">
<label for="company_name">Nom de l'entreprise</label>
<input type="text" class="form-control" id="company_name" name="company_name" placeholder="Nom de l'entreprise" value="<?php echo $company_data['company_name'] ?>" autocomplete="off">

View File

@ -311,6 +311,13 @@
<h3 class="box-title">
<i class="fa fa-calculator"></i> Résumé de Trésorerie
</h3>
<div class="pull-right" style="display: flex; align-items: center; gap: 8px;">
<input type="date" id="tresoDateFrom" class="form-control input-sm" value="<?= date('Y-m-d') ?>" style="width: 140px; display: inline-block;">
<span>au</span>
<input type="date" id="tresoDateTo" class="form-control input-sm" value="<?= date('Y-m-d') ?>" style="width: 140px; display: inline-block;">
<button type="button" class="btn btn-primary btn-sm" id="btnFilterTreso"><i class="fa fa-search"></i> Filtrer</button>
<button type="button" class="btn btn-warning btn-sm" id="btnResetTreso"><i class="fa fa-refresh"></i></button>
</div>
</div>
<div class="box-body">
@ -320,7 +327,7 @@
<div class="col-sm-3 col-xs-12">
<div class="amount-block brut">
<span class="amount-label" style="color: #388e3c;">💰 Total Brut</span>
<div class="amount-header" style="color: #2e7d32;">
<div class="amount-header" style="color: #2e7d32;" id="treso-brut">
<?php echo number_format($total_brut, 0, '.', ' '); ?> Ar
</div>
<span class="amount-sublabel">Orders + Avances</span>
@ -336,7 +343,7 @@
<div class="col-sm-3 col-xs-12">
<div class="amount-block sorties">
<span class="amount-label" style="color: #c62828;">💸 Décaissements</span>
<div class="amount-header" style="color: #d32f2f;">
<div class="amount-header" style="color: #d32f2f;" id="treso-sorties">
<?php echo number_format($total_sorties, 0, '.', ' '); ?> Ar
</div>
<span class="amount-sublabel">Espèce + MVOLA + Virement</span>
@ -352,7 +359,7 @@
<div class="col-sm-4 col-xs-12">
<div class="amount-block net">
<span class="amount-label" style="color: #1565c0;"> Solde Net</span>
<div class="amount-header" style="color: #1976d2; font-size: 36px;">
<div class="amount-header" style="color: #1976d2; font-size: 36px;" id="treso-net">
<?php echo number_format($total_caisse, 0, '.', ' '); ?> Ar
</div>
<span class="amount-sublabel">Disponible en caisse</span>
@ -371,10 +378,10 @@
<div class="payment-icon espece">
<i class="fa fa-money"></i>
</div>
<div class="payment-amount">
<div class="payment-amount" id="treso-espece">
<?php echo number_format($total_espece_caisse, 0, '.', ' '); ?> Ar
</div>
<div class="payment-label">💵 Espèce Disponible</div>
<div class="payment-label">Espece Disponible</div>
</div>
</div>
@ -384,10 +391,10 @@
<div class="payment-icon mvola">
<i class="fa fa-mobile"></i>
</div>
<div class="payment-amount">
<div class="payment-amount" id="treso-mvola">
<?php echo number_format($total_mvola_caisse, 0, '.', ' '); ?> Ar
</div>
<div class="payment-label">📱 MVOLA Disponible</div>
<div class="payment-label">MVOLA Disponible</div>
</div>
</div>
@ -397,10 +404,10 @@
<div class="payment-icon banque">
<i class="fa fa-bank"></i>
</div>
<div class="payment-amount">
<div class="payment-amount" id="treso-banque">
<?php echo number_format($total_vb_caisse, 0, '.', ' '); ?> Ar
</div>
<div class="payment-label">🏦 Banque Disponible</div>
<div class="payment-label">Banque Disponible</div>
</div>
</div>
</div>
@ -408,6 +415,48 @@
</div>
</div>
</div>
<script>
$(document).ready(function() {
var baseUrl = '<?= base_url() ?>';
function loadTresorerie() {
var startDate = $('#tresoDateFrom').val();
var endDate = $('#tresoDateTo').val();
$.ajax({
url: baseUrl + '/dashboard/getTresorerieData',
type: 'GET',
data: { start_date: startDate, end_date: endDate },
success: function(data) {
$('#treso-brut').text(data.total_brut + ' Ar');
$('#treso-sorties').text(data.total_sorties + ' Ar');
$('#treso-net').text(data.total_net + ' Ar');
$('#treso-espece').text(data.espece + ' Ar');
$('#treso-mvola').text(data.mvola + ' Ar');
$('#treso-banque').text(data.banque + ' Ar');
$('#avance-mvola').text(data.avance_mvola + ' Ar');
$('#avance-espece').text(data.avance_espece + ' Ar');
$('#avance-banque').text(data.avance_banque + ' Ar');
}
});
}
// Charger par défaut avec la date d'aujourd'hui
loadTresorerie();
$('#btnFilterTreso').on('click', function() {
loadTresorerie();
});
$('#btnResetTreso').on('click', function() {
$('#tresoDateFrom').val('<?= date('Y-m-d') ?>');
$('#tresoDateTo').val('<?= date('Y-m-d') ?>');
loadTresorerie();
});
});
</script>
<!-- Détail des avances par mode de paiement -->
<div class="container-fluid row" style="margin-top: 10px; margin-bottom: 20px;">
<div class="col-lg-12">
@ -419,19 +468,19 @@
<div class="row">
<div class="col-sm-4">
<div class="description-block">
<h5 class="description-header"><?php echo number_format($total_avances_mvola, 0, '.', ' '); ?> Ar</h5>
<h5 class="description-header" id="avance-mvola"><?php echo number_format($total_avances_mvola, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">MVOLA</span>
</div>
</div>
<div class="col-sm-4">
<div class="description-block">
<h5 class="description-header"><?php echo number_format($total_avances_espece, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">ESPÈCE</span>
<h5 class="description-header" id="avance-espece"><?php echo number_format($total_avances_espece, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">ESPECE</span>
</div>
</div>
<div class="col-sm-4">
<div class="description-block">
<h5 class="description-header"><?php echo number_format($total_avances_virement, 0, '.', ' '); ?> Ar</h5>
<h5 class="description-header" id="avance-banque"><?php echo number_format($total_avances_virement, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">BANQUE</span>
</div>
</div>

View File

@ -149,28 +149,27 @@
<tr>
<tr>
<td>Recouvrement</td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewRecouvrement" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="createRecouvrement" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="updateRecouvrement" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewRecouvrement" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="deleteRecouvrement" class="minimal"></td>
<td>-</td>
</tr>
<tr>
<td>Décaissement</td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewSortieCaisse" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="createSortieCaisse" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="updateSortieCaisse" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewSortieCaisse" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="deleteSortieCaisse" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="validateSortieCaisse" class="minimal"></td>
</tr>
<tr>
<td>Remise</td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewRemise" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="validateRemise" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="refusedRemise" class="minimal"></td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" value="validateRemise" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewRemise" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="refusedRemise" class="minimal"></td>
</tr>
<tr>
<td>Validation commande</td>
@ -181,11 +180,11 @@
</tr>
<tr>
<td>Securité</td>
<td> - </td>
<td> <input type="checkbox" name="permission[]" id="permission" value="viewSecurite" class="minimal"> </td>
<td><input type="checkbox" name="permission[]" id="permission" value="createSecurite" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="updateSecurite" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewSecurite" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="deleteSecurite" class="minimal"></td>
<td>-</td>
</tr>
<tr>
<td>Rapports</td>
@ -197,11 +196,11 @@
</tr>
<tr>
<td>Avances</td>
<td> - </td>
<td> <input type="checkbox" name="permission[]" id="permission" value="viewAvance" class="minimal"> </td>
<td><input type="checkbox" name="permission[]" id="permission" value="createAvance" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="updateAvance" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="viewAvance" class="minimal"></td>
<td><input type="checkbox" name="permission[]" id="permission" value="deleteAvance" class="minimal"></td>
<td>-</td>
</tr>
<tr>
<td>Entreprise</td>

View File

@ -75,6 +75,7 @@
<th>Voir</th>
<th>Supprimer</th>
<th>Assigner</th>
<th>Notification</th>
</tr>
</thead>
<tbody>
@ -96,6 +97,7 @@
</td>
<td>-</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Utilisateurs</td>
@ -140,6 +142,7 @@
}
}
?>></td>
<td>-</td>
</tr>
<tr>
<td>Rôle</td>
@ -176,6 +179,7 @@
}
?>></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Marques</td>
@ -204,6 +208,7 @@
}
} ?>></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Categories</td>
@ -236,6 +241,7 @@
}
} ?>></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Point de ventes</td>
@ -269,6 +275,7 @@
echo "checked";
}
} ?>></td>
<td>-</td>
</tr>
<td>Mécanicien</td>
@ -305,6 +312,7 @@
} ?>>
</td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Attributs</td>
@ -337,6 +345,7 @@
}
} ?>></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Produits</td>
@ -368,6 +377,12 @@
}
} ?>></td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifProduit"
<?php if ($serialize_permission) {
if (in_array('notifProduit', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
<td>Commandes</td>
@ -396,15 +411,15 @@
}
} ?>></td>
<td>-</td>
</tr>
<tr>
<td>Recouvrement</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewRecouvrement"
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifCommande"
<?php if ($serialize_permission) {
if (in_array('viewRecouvrement', $serialize_permission)) {
if (in_array('notifCommande', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
<td>Recouvrement</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="createRecouvrement"
<?php if ($serialize_permission) {
if (in_array('createRecouvrement', $serialize_permission)) {
@ -417,6 +432,12 @@
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewRecouvrement"
<?php if ($serialize_permission) {
if (in_array('viewRecouvrement', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="deleteRecouvrement"
<?php if ($serialize_permission) {
if (in_array('deleteRecouvrement', $serialize_permission)) {
@ -424,16 +445,16 @@
}
} ?>></td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifRecouvrement"
<?php if ($serialize_permission) {
if (in_array('notifRecouvrement', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
<td>Sortie caisse</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewSortieCaisse"
<?php if ($serialize_permission) {
if (in_array('viewSortieCaisse', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="createSortieCaisse"
<?php if ($serialize_permission) {
if (in_array('createSortieCaisse', $serialize_permission)) {
@ -446,6 +467,12 @@
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewSortieCaisse"
<?php if ($serialize_permission) {
if (in_array('viewSortieCaisse', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="deleteSortieCaisse"
<?php if ($serialize_permission) {
if (in_array('deleteSortieCaisse', $serialize_permission)) {
@ -458,6 +485,12 @@
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifSortieCaisse"
<?php if ($serialize_permission) {
if (in_array('notifSortieCaisse', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
<td>Autres encaissements</td>
@ -484,28 +517,43 @@
if (in_array('deleteEncaissement', $serialize_permission)) {
echo "checked";
}
} ?>>
</tr>
<tr>
<td>Remise</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewRemise"
} ?>></td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifEncaissement"
<?php if ($serialize_permission) {
if (in_array('viewRemise', $serialize_permission)) {
if (in_array('notifEncaissement', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
<td>Remise</td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="validateRemise"
<?php if ($serialize_permission) {
if (in_array('validateRemise', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewRemise"
<?php if ($serialize_permission) {
if (in_array('viewRemise', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="refusedRemise"
<?php if ($serialize_permission) {
if (in_array('refusedRemise', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifRemise"
<?php if ($serialize_permission) {
if (in_array('notifRemise', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
<td>Validation Commande</td>
@ -521,13 +569,15 @@
echo "checked";
}
} ?>></td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="deleteCommande1"
<?php if ($serialize_permission) {
if (in_array('deleteCommande1', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Sécurité</td>
@ -557,6 +607,13 @@
echo "checked";
}
} ?>> </td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifSecurite"
<?php if ($serialize_permission) {
if (in_array('notifSecurite', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
@ -587,6 +644,13 @@
echo "checked";
}
} ?>> </td>
<td>-</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="notifAvance"
<?php if ($serialize_permission) {
if (in_array('notifAvance', $serialize_permission)) {
echo "checked";
}
} ?>></td>
</tr>
<tr>
@ -601,6 +665,7 @@
} ?>></td>
<td> - </td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Entreprise</td>
@ -615,6 +680,7 @@
<td> - </td>
<td> - </td>
<td>-</td>
<td>-</td>
</tr>
<tr>
<td>Profile</td>
@ -628,6 +694,7 @@
} ?>></td>
<td> - </td>
<td>-</td>
<td>-</td>
</tr>
<tr>
@ -642,6 +709,7 @@
} ?>></td>
<td> - </td>
<td>-</td>
<td>-</td>
</tr>
<tr>
@ -657,6 +725,7 @@
<td> - </td>
<td> - </td>
<td>-</td>
<td>-</td>
</tr>
</tbody>
</table>

View File

@ -73,8 +73,9 @@
<tr>
<th>Date</th>
<th>Produit</th>
<th>SKU</th>
<th>de série</th>
<th>Magasin</th>
<th>Utilisateur</th>
<th>Action</th>
<th>Description</th>
</tr>
@ -238,13 +239,14 @@ $(document).ready(function() {
"complete": function() { $("#loading").hide(); }
},
"columns": [
{ "data": 0, "width": "15%" },
{ "data": 1, "width": "20%" },
{ "data": 0, "width": "13%" },
{ "data": 1, "width": "15%" },
{ "data": 2, "width": "10%" },
{ "data": 3, "width": "12%" },
{ "data": 4, "width": "10%" },
{
"data": 4,
"width": "10%",
"data": 5,
"width": "8%",
"render": function(data) {
let badgeClass = "badge bg-gray";
if (data === "CREATE") badgeClass = "badge bg-green";
@ -254,7 +256,7 @@ $(document).ready(function() {
return '<span class="'+badgeClass+'">'+data+'</span>';
}
},
{ "data": 5, "width": "33%" }
{ "data": 6, "width": "32%" }
],
"order": [[0, "desc"]],
"pageLength": 25,

View File

@ -52,14 +52,6 @@
</div>
<!-- /.box-header -->
<form role="form" action="<?php base_url('orders/create') ?>" method="post" class="form-horizontal">
<?php if (isset($validation) && $validation->getErrors()): ?>
<div class="alert alert-danger">
<ul>
<?= $validation->listErrors() ?>
</ul>
</div>
<?php endif; ?>
<div class="box-body">
@ -108,6 +100,31 @@
placeholder="Entrer ne CIN du client" autocomplete="off" required value="<?= old('customer_cin') ?>">
</div>
</div>
<div class="form-group">
<label for="customer_type" class="col-sm-5 control-label" style="text-align:left;">Type de client <span class="text-danger">*</span></label>
<div class="col-sm-7">
<select class="form-control" id="customer_type" name="customer_type" required>
<option value="">-- Sélectionner --</option>
<option value="particulier" <?= old('customer_type') == 'particulier' ? 'selected' : '' ?>>Particulier</option>
<option value="revendeur" <?= old('customer_type') == 'revendeur' ? 'selected' : '' ?>>Revendeur</option>
</select>
</div>
</div>
<div class="form-group">
<label for="source" class="col-sm-5 control-label" style="text-align:left;">Source <span class="text-danger">*</span></label>
<div class="col-sm-7">
<select class="form-control" id="source" name="source" required>
<option value="">-- Sélectionner --</option>
<option value="facebook_perso" <?= old('source') == 'facebook_perso' ? 'selected' : '' ?>>Facebook perso</option>
<option value="page_motorbike" <?= old('source') == 'page_motorbike' ? 'selected' : '' ?>>Page motorbike</option>
<option value="bouche_a_oreille" <?= old('source') == 'bouche_a_oreille' ? 'selected' : '' ?>>Bouche à oreille</option>
<option value="commercial_interne" <?= old('source') == 'commercial_interne' ? 'selected' : '' ?>>Commercial interne</option>
<option value="autre" <?= old('source') == 'autre' ? 'selected' : '' ?>>Autre</option>
</select>
</div>
</div>
</div>
@ -256,6 +273,29 @@
'<i class="glyphicon glyphicon-tag"></i>' +
'</button>';
// Gérer la visibilité du bouton "+" selon le type de client
function toggleAddRowButton() {
var customerType = $("#customer_type").val();
var rowCount = $("#product_info_table tbody tr").length;
if (customerType === 'particulier' && rowCount >= 1) {
$("#add_row").hide();
} else {
$("#add_row").show();
}
}
// Écouter le changement de type de client
$("#customer_type").on('change', function () {
var customerType = $(this).val();
if (customerType === 'particulier') {
// Supprimer toutes les lignes sauf la première
$("#product_info_table tbody tr:gt(0)").remove();
subAmount();
}
toggleAddRowButton();
});
// Add new row in the table
$("#add_row").unbind('click').bind('click', function () {
var table = $("#product_info_table");
@ -268,7 +308,6 @@
dataType: 'json',
success: function (response) {
// console.log(reponse.x);
var html = '<tr id="row_' + row_id + '">' +
'<td>' +
'<select class="form-control select_group product" data-row-id="' + row_id + '" id="product_' + row_id + '" name="product[]" style="width:100%;" onchange="getProductData(' + row_id + ')">' +
@ -296,6 +335,9 @@
$(".product").select2();
// Cacher le bouton "+" si particulier et déjà 2 lignes
toggleAddRowButton();
}
});
@ -413,6 +455,7 @@
function removeRow(tr_id) {
$("#product_info_table tbody tr#row_" + tr_id).remove();
subAmount();
toggleAddRowButton();
}
function formatAllNumbers() {
// Sélecteur pour tous les inputs que tu veux formater

View File

@ -365,7 +365,7 @@
<input type="hidden" name="vat_charge_rate" value="<?php echo $company_data['vat_charge_value'] ?>" autocomplete="off">
<a target="__blank" id="Imprimente" href="<?php echo base_url() . 'orders/printDiv/' . $order_data['order']['id'] ?>" class="btn btn-default">Imprimer</a>
<button type="submit" class="btn btn-primary">Enregistrer</button>
<button type="submit" class="btn btn-primary">Payer</button>
<a href="<?php echo base_url('orders/') ?>" class="btn btn-warning">Retour</a>
</div>
</form>

View File

@ -43,7 +43,7 @@
<h3 class="box-title">Mise à jours Moto</h3>
</div>
<form role="form" action="<?php base_url('users/update') ?>" method="post" enctype="multipart/form-data">
<form role="form" action="<?php echo base_url('products/update/' . $product_data['id']); ?>" method="post" enctype="multipart/form-data">
<div class="box-body">
<!-- Image actuelle -->
<div class="form-group">
@ -111,7 +111,7 @@
<!-- Date d'arrivage -->
<div class="form-group">
<label for="datea">Date d'arrivage</label>
<input type="date" class="form-control" id="datea" name="datea" autocomplete="off" value="<?php echo $product_data['date_arivage']; ?>" />
<input type="date" class="form-control" id="datea" name="datea" autocomplete="off" value="<?php echo !empty($product_data['date_arivage']) ? date('Y-m-d', strtotime($product_data['date_arivage'])) : ''; ?>" />
</div>
<!-- Puissance -->
@ -143,19 +143,23 @@
<label for="categorie">Catégories</label>
<?php
$rawCats = $product_data['categorie_id'] ?? null;
$catIds = [];
if (is_array($rawCats)) {
$catIds = array_map('intval', $rawCats);
} elseif (is_string($rawCats)) {
$decoded = json_decode($rawCats, true);
if (is_array($decoded)) {
$catIds = array_map('intval', $decoded);
} else {
$catIds = array_filter(array_map('intval', explode(',', $rawCats)), fn($id) => $id > 0);
}
} elseif (is_int($rawCats) || ctype_digit((string)$rawCats)) {
$catIds = [(int) $rawCats];
} else {
$catIds = [];
}
?>
<select class="form-control select_group" id="categorie" name="categorie[]" multiple="multiple">
<?php foreach ($categorie as $k => $v): ?>
<option value="<?= $v['id']; ?>" <?= in_array($v['id'], $catIds, true) ? 'selected="selected"' : '' ?>><?= esc($v['name']); ?></option>
<option value="<?= $v['id']; ?>" <?= in_array((int)$v['id'], $catIds) ? 'selected="selected"' : '' ?>><?= esc($v['name']); ?></option>
<?php endforeach; ?>
</select>
</div>

View File

@ -155,7 +155,7 @@
<thead>
<tr>
<th>Image</th>
<th>UGS</th>
<th>SERIE </th>
<th>Nom de produit</th>
<th>Prix</th>
<th>Magasin</th>
@ -340,7 +340,12 @@
'columnDefs': [{
targets: 3,
className: 'text-right'
}]
}],
'createdRow': function(row, data, dataIndex) {
if (data[5] && data[5].indexOf('En attente de livraison') !== -1) {
$(row).css({'background-color': '#fff3cd', 'opacity': '0.85'});
}
}
});
// CORRECTION: Utilisation de la délégation d'événements pour les boutons

View File

@ -67,7 +67,9 @@
<td></td>
<td></td>
<td></td>
<?php if (in_array('updateRecouvrement', $user_permission) || in_array('deleteRecouvrement', $user_permission)): ?>
<td></td>
<?php endif; ?>
</tr>
</tbody>
</table>
@ -275,7 +277,7 @@
// Initialisation du DataTable
manageTable = $('#manageTable').DataTable({
'ajax': '<?= base_url('recouvrement/fetchRecouvrementData') ?>',
'order': [],
'order': [[2, 'desc']],
'columnDefs': [{
targets: 1,
className: 'text-right rowmontant'
@ -306,12 +308,30 @@
})
// ====== CRÉATION DE RECOUVREMENT ======
$("#create_form").unbind('submit').on('submit', function() {
$("#create_form").unbind('submit').on('submit', function(e) {
e.preventDefault();
var form = $(this);
var submitBtn = form.find('button[type="submit"]');
// Supprimer les messages d'erreur
$(".text-danger").remove();
// Pop-up de confirmation
Swal.fire({
title: 'Confirmer la création',
text: 'Voulez-vous vraiment enregistrer ce recouvrement ?',
icon: 'question',
showCancelButton: true,
confirmButtonColor: '#3085d6',
cancelButtonColor: '#d33',
confirmButtonText: 'Oui, enregistrer',
cancelButtonText: 'Annuler'
}).then((result) => {
if (!result.isConfirmed) return;
// Désactiver le bouton pour éviter les doublons
submitBtn.prop('disabled', true).text('Enregistrement...');
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
@ -374,9 +394,15 @@ $("#create_form").unbind('submit').on('submit', function() {
'<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
'<strong>Erreur!</strong> Une erreur est survenue lors de la création.' +
'</div>');
},
complete: function() {
// Réactiver le bouton
submitBtn.prop('disabled', false).text('Enregistrer');
}
});
}); // fin Swal.then
return false;
});
// ====== SUPPRESSION DE RECOUVREMENT ======

View File

@ -13,8 +13,9 @@
<section class="content">
<div class="row">
<div class="col-md-12 col-xs-12">
<form class="form-inline" action="<?php echo base_url('reports/'); ?>" method="POST">
<div class="form-group">
<form action="<?php echo base_url('reports/'); ?>" method="POST">
<div class="row" style="margin-bottom: 15px;">
<div class="col-md-2">
<label for="select_year">Année</label>
<select class="form-control" name="select_year" id="select_year">
<?php foreach ($report_years as $value): ?>
@ -24,8 +25,47 @@
<?php endforeach; ?>
</select>
</div>
<button type="submit" class="btn btn-default">Envoyer</button>
<a href="detail/stock" class="btn btn-sm btn-success">Détails</a>
<div class="col-md-2">
<label for="start_date">Date début</label>
<input type="date" class="form-control" name="start_date" id="start_date" value="<?= $selected_start_date ?? '' ?>">
</div>
<div class="col-md-2">
<label for="end_date">Date fin</label>
<input type="date" class="form-control" name="end_date" id="end_date" value="<?= $selected_end_date ?? '' ?>">
</div>
<div class="col-md-2">
<label for="store_id">Point de vente</label>
<select class="form-control" name="store_id" id="store_id">
<option value="">TOUS</option>
<?php foreach ($stores as $store): ?>
<option value="<?= $store['id'] ?>" <?= (isset($selected_store_id) && $selected_store_id == $store['id']) ? 'selected' : '' ?>>
<?= $store['name'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2">
<label for="user_id">Utilisateur</label>
<select class="form-control" name="user_id" id="user_id">
<option value="">TOUS</option>
<?php foreach ($users_list as $u): ?>
<option value="<?= $u['id'] ?>" <?= (isset($selected_user_id) && $selected_user_id == $u['id']) ? 'selected' : '' ?>>
<?= $u['firstname'] . ' ' . $u['lastname'] ?>
</option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2" style="padding-top: 25px;">
<button type="submit" class="btn btn-primary"><i class="fa fa-filter"></i> Filtrer</button>
<a href="<?= base_url('reports/') ?>" class="btn btn-default" title="Réinitialiser"><i class="fa fa-refresh"></i></a>
</div>
</div>
<div class="row" style="margin-bottom: 10px;">
<div class="col-md-12">
<a href="<?= base_url('reports/detail/stock') ?>" class="btn btn-sm btn-success">Details Stock</a>
<a href="<?= base_url('reports/detail/performance') ?>" class="btn btn-sm btn-primary">Performances</a>
</div>
</div>
</form>
</div>

View File

@ -73,27 +73,28 @@
<label for="endDate" class="form-label">Date de fin</label>
<input type="date" id="endDate" class="form-control">
</div>
<div class="col-md-3">
<div class="col-md-2">
<label for="pvente" class="form-label">Points de ventes</label>
<select name="" id="pvente" class="form-control">
<select id="pvente" class="form-control">
<option value="TOUS">TOUS</option>
<?php
foreach ($stores as $value) {
?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?>
</option>
<?php
}
?>
<?php foreach ($stores as $value): ?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="col-md-2">
<label for="commercialFilter" class="form-label">Commercial</label>
<select id="commercialFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($commerciaux as $com): ?>
<option value="<?= $com['id']; ?>"><?= $com['firstname'] . ' ' . $com['lastname']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<br>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer
🔍</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter
📤</button>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter</button>
</div>
</div>
<table id="commperformance" class="table table-hover table-striped">
@ -102,7 +103,7 @@
<th>Nom et prénom</th>
<th>Email</th>
<th>Motos vendue</th>
<th>Date de vente</th><!-- return 2025-04-18 -->
<th>Date de vente</th>
<th>Prix d'achat</th>
<th>Prix de vente</th>
<th>Point de ventes</th>
@ -112,9 +113,9 @@
<tfoot>
<tr>
<th colspan="5" style="text-align:right">Total:</th>
<th></th> <!-- total Prix de vente -->
<th></th>
<th></th> <!-- total Bénéfices -->
<th></th>
<th></th>
</tr>
</tfoot>
</table>
@ -135,48 +136,48 @@
</div>
<div class="card-body">
<div class="row g-3 align-items-center mb-4" style="margin: 5px 0 5px 5px;">
<div class="col-md-3">
<div class="col-md-2">
<label for="startDate2" class="form-label">Date de début</label>
<input type="date" id="startDate" class="form-control">
<input type="date" id="startDate2" class="form-control">
</div>
<div class="col-md-3">
<div class="col-md-2">
<label for="endDate2" class="form-label">Date de fin</label>
<input type="date" id="endDate" class="form-control">
<input type="date" id="endDate2" class="form-control">
</div>
<div class="col-md-3">
<label for="pvente" class="form-label">Points de ventes</label>
<select name="" id="pvente2" class="form-control">
<div class="col-md-2">
<label for="pvente2" class="form-label">Points de ventes</label>
<select id="pvente2" class="form-control">
<option value="TOUS">TOUS</option>
<?php
foreach ($stores as $value) {
?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?>
</option>
<?php
}
?>
<?php foreach ($stores as $value): ?>
<option value="<?= $value['name']; ?>"><?= $value['name']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="col-md-2">
<label for="mecanicienFilter" class="form-label">Mécanicien</label>
<select id="mecanicienFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($mecaniciens as $mec): ?>
<option value="<?= $mec['id']; ?>"><?= $mec['firstname'] . ' ' . $mec['lastname']; ?></option>
<?php endforeach; ?>
</select>
</div>
<div class="col-md-2 d-flex align-items-end">
<br>
<button id="filteredB2" class="btn btn-primary w-100">Filtrer
🔍</button>
<button id="ExportBTN2" class="btn btn-success w-100">Exporter
📤</button>
<button id="filteredB2" class="btn btn-primary w-100">Filtrer</button>
<button id="ExportBTN2" class="btn btn-success w-100">Exporter</button>
</div>
</div>
<table id="mecperformance" class="table table-hover table-striped">
<thead>
<tr>
<th>Nom et prénom</th>
<th>Email</th>
<th>Motos vendue</th>
<th>Date de vente</th><!-- return 2025-04-18 -->
<th>Prix d'achat</th>
<th>Prix de vente</th>
<th>Point de ventes</th>
<th>Bénefices</th>
<th>Mécanicien</th>
<th>Image</th>
<th>Moto réparée</th>
<th> Série</th>
<th>Point de vente</th>
<th>Date début</th>
<th>Date fin</th>
</tr>
</thead>
</table>
@ -200,8 +201,10 @@
// Initialize the datatable
var baseUrl = '<?= base_url() ?>';
manageTable = $('#commperformance').DataTable({
'ajax': 'fetchPerformances',
'ajax': baseUrl + 'reports/detail/fetchPerformances',
'order': [],
'pageLength': 5,
'lengthMenu': [
@ -241,49 +244,44 @@
}
});
// Filtre commercial - côté serveur
$('#filteredB1').on('click', function () {
const startDate = $('#startDate').val();
const endDate = $('#endDate').val();
const pvente = $('#pvente').val();
var startDate = $('#startDate').val();
var endDate = $('#endDate').val();
var pvente = $('#pvente').val();
var commercial = $('#commercialFilter').val();
// Get all original data (you may need to fetch from server or already loaded)
manageTable.ajax.url('fetchPerformances').load(function () {
const filteredData = [];
manageTable.rows().every(function () {
const data = this.data();
const saleDate = data[3].split(' ')[0]; // extract YYYY-MM-DD from date
const store = data[6];
// Filter logic
const dateMatch = (!startDate && !endDate) ||
(startDate && endDate && saleDate >= startDate && saleDate <= endDate) ||
(startDate && !endDate && saleDate >= startDate) ||
(!startDate && endDate && saleDate <= endDate);
const storeMatch = (pvente === 'TOUS' || pvente === store);
if (dateMatch && storeMatch) {
filteredData.push(data);
}
var url = baseUrl + 'reports/detail/fetchPerformances?startDate=' + startDate + '&endDate=' + endDate + '&pvente=' + pvente + '&commercial=' + commercial;
manageTable.ajax.url(url).load();
});
// Clear and reload table with filtered data
manageTable.clear().rows.add(filteredData).draw();
// DataTable mécanicien
var mecTable = $('#mecperformance').DataTable({
'ajax': baseUrl + 'mecanicien/fetchMecanicienPerformances',
'order': [],
'pageLength': 10
});
// Filtre mécanicien - côté serveur
$('#filteredB2').on('click', function () {
var startDate = $('#startDate2').val();
var endDate = $('#endDate2').val();
var mecanic = $('#mecanicienFilter').val();
var url = baseUrl + 'mecanicien/fetchMecanicienPerformances?startDate=' + startDate + '&endDate=' + endDate + '&mecanic_id=' + (mecanic === 'TOUS' ? '' : mecanic);
mecTable.ajax.url(url).load();
});
});
// Export commercial
document.getElementById('ExportBTN1').addEventListener('click', function () {
// Select your table
var table = document.getElementById('commperformance');
// Convert it to a workbook
var wb = XLSX.utils.table_to_book(table, {
sheet: "Feuille1"
});
// Export it
var wb = XLSX.utils.table_to_book(document.getElementById('commperformance'), { sheet: "Commercial" });
XLSX.writeFile(wb, 'export-commercial-performance.xlsx');
});
// Export mécanicien
document.getElementById('ExportBTN2').addEventListener('click', function () {
var wb = XLSX.utils.table_to_book(document.getElementById('mecperformance'), { sheet: "Mecanicien" });
XLSX.writeFile(wb, 'export-mecanicien-performance.xlsx');
});
</script>

View File

@ -57,9 +57,17 @@
</div>
<div class="card-body">
<!-- Filter Bar -->
<div class="row g-3">
<div class="row g-3 align-items-center mb-3" style="margin: 5px 0;">
<div class="col-md-2">
<label for="startDateStock" class="form-label fw-bold">Date de début</label>
<input type="date" id="startDateStock" class="form-control">
</div>
<div class="col-md-2">
<label for="endDateStock" class="form-label fw-bold">Date de fin</label>
<input type="date" id="endDateStock" class="form-control">
</div>
<div class="col-md-3">
<label for="storeFilter" class="form-label fw-bold">🏪 Points de ventes</label>
<label for="storeFilter" class="form-label fw-bold">Points de ventes</label>
<select id="storeFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($stores as $value) { ?>
@ -67,9 +75,9 @@
<?php } ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<div class="col-md-2 d-flex align-items-end">
<br>
<button id="filterBtn" class="btn btn-primary w-100">🔍 Filtrer</button>
<button id="filterBtn" class="btn btn-primary w-100">Filtrer</button>
</div>
</div>
@ -128,20 +136,27 @@
</div>
<div class="card-body">
<div class="row g-3 align-items-center mb-4" style="margin: 5px 0 5px 5px;">
<div class="col-md-4">
<div class="col-md-3">
<label for="startDate" class="form-label">Date de début</label>
<input type="date" id="startDate" class="form-control">
</div>
<div class="col-md-4">
<div class="col-md-3">
<label for="endDate" class="form-label">Date de fin</label>
<input type="date" id="endDate" class="form-control">
</div>
<div class="col-md-4 d-flex align-items-end">
<div class="col-md-3">
<label for="exportStoreFilter" class="form-label">Point de vente</label>
<select id="exportStoreFilter" class="form-control">
<option value="TOUS">TOUS</option>
<?php foreach ($stores as $value) { ?>
<option value="<?= $value['id'] ?>"><?= $value['name'] ?></option>
<?php } ?>
</select>
</div>
<div class="col-md-3 d-flex align-items-end">
<br>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer
🔍</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter
📤</button>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter</button>
</div>
</div>
@ -188,7 +203,7 @@
manageTable = $('#venteTable').DataTable({
'ajax': 'fetctData/' + 0,
'ajax': 'fetchData/' + 0,
'order': [],
'pageLength': 5, // Set default rows per page
'lengthMenu': [
@ -198,7 +213,7 @@
});
manageTable2 = $('#stockTable').DataTable({
'ajax': 'fetctDataStock/' + 0,
'ajax': 'fetchDataStock/' + 0,
'order': [],
'pageLength': 5, // Set default rows per page
'lengthMenu': [
@ -209,7 +224,7 @@
manageTable3 = $('#export1').DataTable({
ajax: {
url: 'fetctDataStock2/' + 0,
url: 'fetchDataStock2/' + 0,
dataSrc: 'data'
},
order: [],
@ -220,14 +235,16 @@
]
});
const filterBtn = document.getElementById('storeFilter');
const storeSelect = document.getElementById('storeFilter');
filterBtn.addEventListener('change', function () {
let filterValue = filterBtn.value === "TOUS" ? "0" : filterBtn.value;
storeSelect.addEventListener('change', function () {
let filterValue = storeSelect.value === "TOUS" ? "0" : storeSelect.value;
let startDate = $('#startDateStock').val();
let endDate = $('#endDateStock').val();
let params = '?startDate=' + startDate + '&endDate=' + endDate;
// Update the DataTable dynamically without reinitialization
manageTable.ajax.url('fetctData/' + filterValue).load();
manageTable2.ajax.url('fetctDataStock/' + filterValue).load();
manageTable.ajax.url('fetchData/' + filterValue + params).load();
manageTable2.ajax.url('fetchDataStock/' + filterValue + params).load();
});
let productsSold = <?= $ventes ?>;
@ -249,6 +266,14 @@
// Trigger the filter on button click
$('#filterBtn').click(function () {
let storeId = $('#storeFilter').val();
let filterValue = storeId === "TOUS" ? "0" : storeId;
let startDate = $('#startDateStock').val();
let endDate = $('#endDateStock').val();
let params = '?startDate=' + startDate + '&endDate=' + endDate;
manageTable.ajax.url('fetchData/' + filterValue + params).load();
manageTable2.ajax.url('fetchDataStock/' + filterValue + params).load();
manageTable3.ajax.url('fetchDataStock2/' + filterValue + params).load();
applyFilter(storeId);
});
@ -256,32 +281,14 @@
applyFilter("TOUS");
});
$.fn.dataTable.ext.search.push(function (settings, data, dataIndex) {
if (settings.nTable.id !== 'export1') return true; // Apply only to your table
const startDate = $('#startDate').val();
const endDate = $('#endDate').val();
const saleDate = data[3]; // assuming column 4 is "Date de vente"
// If no start date, show everything (default)
if (!startDate) {
return true;
}
const sale = new Date(saleDate);
const start = new Date(startDate);
const end = endDate ? new Date(endDate) : null;
if (end) {
return sale >= start && sale <= end;
} else {
return sale >= start;
}
});
$('#filteredB1').on('click', function () {
manageTable3.draw(); // re-filter table
let storeId = $('#exportStoreFilter').val();
let filterValue = storeId === "TOUS" ? "0" : storeId;
let startDate = $('#startDate').val();
let endDate = $('#endDate').val();
let params = '?startDate=' + startDate + '&endDate=' + endDate;
manageTable3.ajax.url('fetchDataStock2/' + filterValue + params).load();
});

View File

@ -186,6 +186,33 @@
</div>
</div>
<!-- COMMANDES EN ATTENTE DE VALIDATION -->
<div class="row">
<div class="col-md-12">
<div class="box box-warning">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-clock-o"></i> Commandes en attente de livraison
</h3>
</div>
<div class="box-body">
<table id="pendingTable" class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Image</th>
<th> SERIE </th>
<th>Désignation</th>
<th>Statut</th>
<th>Action</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</div>
</div>
<!-- Filtres -->
<div class="row">
<div class="col-md-12">
@ -245,7 +272,7 @@
<th>Image</th>
<th> Facture</th>
<th>Désignation</th>
<th>UGS</th>
<th>SERIE </th>
<th>Marque</th>
<th>Client</th>
<th>Magasin</th>
@ -266,6 +293,38 @@
</section>
</div>
<!-- Modal de validation livraison -->
<div class="modal fade" id="editModal" tabindex="-1" role="dialog">
<div class="modal-dialog modal-lg" role="document">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #28a745 0%, #20c997 100%); color: white;">
<button type="button" class="close" data-dismiss="modal" style="color: white;">&times;</button>
<h4 class="modal-title"><i class="fa fa-check-circle"></i> Valider la livraison</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-4 text-center">
<img id="editImage" class="img-responsive img-thumbnail" src="" alt="Produit" style="max-height: 200px;">
</div>
<div class="col-md-8">
<p><strong>Produit :</strong> <span id="editNom"></span></p>
<p><strong> SERIE :</strong> <span id="editNumSerie"></span></p>
<p><strong> Facture :</strong> <span id="editBillNo"></span></p>
<p><strong>Client :</strong> <span id="editClient"></span></p>
<p><strong>Adresse :</strong> <span id="editAddress"></span></p>
<p><strong>Téléphone :</strong> <span id="editPhone"></span></p>
<p><strong>CIN :</strong> <span id="editCin"></span></p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button>
<button type="button" class="btn btn-success" id="btnValidate"><i class="fa fa-check"></i> Confirmer la livraison</button>
</div>
</div>
</div>
</div>
<!-- Modal Détails -->
<div class="modal fade" id="detailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
@ -294,8 +353,8 @@
<div class="col-xs-6"><span id="detailDesignation"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>UGS:</strong></div>
<div class="col-xs-6"><span id="detailUGS"></span></div>
<div class="col-xs-6"><strong>SERIE:</strong></div>
<div class="col-xs-6"><span id="detailNumSerie"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Marque:</strong></div>
@ -362,6 +421,94 @@
$(function() {
$("#securiteNav").addClass('active');
// ✅ TABLEAU DES COMMANDES EN ATTENTE
var pendingTable = $('#pendingTable').DataTable({
ajax: {
url: '<?= base_url('validateSecurite/fetchSecuriteData') ?>',
type: 'GET',
dataSrc: 'data'
},
columns: [
{ data: 'image', orderable: false },
{ data: 'num_serie' },
{ data: 'designation' },
{ data: 'statut' },
{ data: 'action', orderable: false }
],
order: [],
language: {
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sZeroRecords: "Aucune commande en attente",
sEmptyTable: "Aucune commande en attente de livraison",
sInfo: "Affichage de _START_ à _END_ sur _TOTAL_ éléments",
sInfoEmpty: "Aucun élément",
oPaginate: { sPrevious: "Précédent", sNext: "Suivant" }
}
});
// Rafraîchir les commandes en attente toutes les 10 secondes
setInterval(function() { pendingTable.ajax.reload(null, false); }, 10000);
// ✅ GESTION DE LA VALIDATION
var currentSecuriteId = null;
window.editFunc = function(id) {
currentSecuriteId = id;
$.ajax({
url: '<?= base_url('validateSecurite/fetchSecuriteDataById') ?>/' + id,
type: 'POST',
dataType: 'json',
success: function(response) {
$('#editImage').attr('src', response.image);
$('#editNom').text(response.nom);
$('#editNumSerie').text(response.num_serie);
$('#editBillNo').text(response.bill_no);
$('#editClient').text(response.customer_name);
$('#editAddress').text(response.customer_address);
$('#editPhone').text(response.customer_phone);
$('#editCin').text(response.customer_cin);
$('#editModal').modal('show');
}
});
};
$('#btnValidate').on('click', function() {
if (!currentSecuriteId) return;
var btn = $(this);
btn.prop('disabled', true).html('<i class="fa fa-spinner fa-spin"></i> Validation...');
$.ajax({
url: '<?= base_url('validateSecurite/update') ?>/' + currentSecuriteId,
type: 'POST',
data: { status: 'Validé' },
dataType: 'json',
success: function(response) {
$('#editModal').modal('hide');
btn.prop('disabled', false).html('<i class="fa fa-check"></i> Confirmer la livraison');
if (response.success) {
// Aussi marquer comme livré côté commande
pendingTable.ajax.reload(null, false);
if (typeof historyTable !== 'undefined') historyTable.ajax.reload(null, false);
$('#messages').html('<div class="alert alert-success alert-dismissible">' +
'<button type="button" class="close" data-dismiss="alert">&times;</button>' +
'<i class="fa fa-check"></i> ' + response.messages + '</div>');
setTimeout(function() { $('#messages').fadeOut('slow', function() { $(this).html('').show(); }); }, 3000);
} else {
$('#messages').html('<div class="alert alert-danger alert-dismissible">' +
'<button type="button" class="close" data-dismiss="alert">&times;</button>' +
response.messages + '</div>');
}
},
error: function() {
btn.prop('disabled', false).html('<i class="fa fa-check"></i> Confirmer la livraison');
$('#editModal').modal('hide');
}
});
});
// Configuration DataTable en français
$.extend(true, $.fn.dataTable.defaults, {
language: {
@ -620,7 +767,7 @@ $(function() {
data.push([
'N° Facture',
'Désignation',
'UGS',
'N° SERIE',
'Marque',
'Client',
'Magasin',
@ -671,7 +818,7 @@ function viewDetails(id) {
$('#detailImage').attr('src', data[0]);
$('#detailBillNo').text(data[1]);
$('#detailDesignation').text(data[2]);
$('#detailUGS').text(data[3]);
$('#detailNumSerie').text(data[3]);
$('#detailMarque').text(data[4]);
$('#detailSerie').text(data[3]);
$('#detailStore').text(data[7]);

View File

@ -3,7 +3,7 @@
<div>
<strong>Copyright &copy;<?php echo date('Y') ?>.</strong> All rights reserved.
</div>
<div class="pull-right hidden-xs"><h5 style="font-family: tahoma;">Designed and Managed by <strong>MYDEVUP</strong>
<div class="pull-right hidden-xs"><h5 style="font-family: tahoma;">Designed and Managed by <strong>COMPANY FOR MADAGASCAR</strong>
</footer>
<!-- Add the sidebar's background. This div must be placed
@ -17,26 +17,87 @@ function toggleSidebar() {
document.body.classList.toggle('sidebar-collapsed');
}
document.addEventListener('DOMContentLoaded', function() {
const treeviews = document.querySelectorAll('.sidebar-menu .treeview');
$(document).ready(function() {
// Supprimer TOUS les handlers AdminLTE sur le menu
$('.sidebar-menu').off();
$(document).off('click', '.sidebar-menu li a');
$(document).off('click', '.treeview > a');
treeviews.forEach(item => {
// Quand la souris entre dans un menu
item.addEventListener('mouseenter', function () {
if (document.body.classList.contains('sidebar-collapsed')) {
this.classList.add('menu-open');
// Aussi tenter de détruire le widget tree AdminLTE
try { $('.sidebar-menu').tree('destroy'); } catch(e) {}
// Re-supprimer après un court délai (AdminLTE peut rebind)
setTimeout(function() {
$('.sidebar-menu').off();
bindSidebarMenu();
}, 100);
function bindSidebarMenu() {
// Clic sur un treeview
$('.sidebar-menu .treeview > a').on('click', function(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
var $li = $(this).parent();
var menuId = $li.attr('id') || $('.sidebar-menu .treeview').index($li);
// Fermer les autres
$('.sidebar-menu .treeview').not($li).removeClass('menu-open')
.children('.treeview-menu').slideUp(200);
// Toggle
if ($li.hasClass('menu-open')) {
$li.removeClass('menu-open');
$li.children('.treeview-menu').slideUp(200);
localStorage.removeItem('openMenu');
} else {
$li.addClass('menu-open');
$li.children('.treeview-menu').slideDown(200);
localStorage.setItem('openMenu', String(menuId));
}
return false;
});
}
// Restaurer le menu ouvert au survol de la sidebar
var restored = false;
$('.main-sidebar').on('mouseenter', function() {
if (!restored && $('body').hasClass('sidebar-collapsed')) {
var openMenu = localStorage.getItem('openMenu');
if (openMenu !== null) {
var $target = $('#' + openMenu);
if (!$target.length) {
$target = $('.sidebar-menu .treeview').eq(parseInt(openMenu));
}
if ($target.length) {
$target.addClass('menu-open');
$target.children('.treeview-menu').slideDown(200);
}
}
restored = true;
}
});
// Quand la souris sort du menu
item.addEventListener('mouseleave', function () {
if (document.body.classList.contains('sidebar-collapsed')) {
this.classList.remove('menu-open');
// Quand la souris quitte : fermer visuellement mais garder en mémoire
var closeTimer = null;
$('.main-sidebar').on('mouseleave', function() {
if ($('body').hasClass('sidebar-collapsed')) {
closeTimer = setTimeout(function() {
$('.sidebar-menu .treeview').removeClass('menu-open');
$('.sidebar-menu .treeview-menu').slideUp(200);
restored = false;
}, 400);
}
});
$('.main-sidebar').on('mouseenter', function() {
if (closeTimer) {
clearTimeout(closeTimer);
closeTimer = null;
}
});
});
});
</script>

View File

@ -5,6 +5,13 @@
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title><?php echo $page_title; ?></title>
<?php if (!empty($company_logo)): ?>
<link rel="icon" type="image/x-icon" href="<?= base_url($company_logo) ?>?v=<?= time() ?>">
<link rel="shortcut icon" href="<?= base_url($company_logo) ?>?v=<?= time() ?>">
<?php else: ?>
<link rel="icon" type="image/x-icon" href="<?= base_url('assets/images/company_logo.jpg') ?>?v=<?= time() ?>">
<link rel="shortcut icon" href="<?= base_url('assets/images/company_logo.jpg') ?>?v=<?= time() ?>">
<?php endif; ?>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Bootstrap 3.3.7 -->
@ -118,7 +125,7 @@
</head>
<body class="hold-transition skin-blue sidebar-mini">
<body class="hold-transition skin-blue sidebar-mini sidebar-collapsed">
<div class="wrapper">
<style>
/* ============================================
@ -127,6 +134,26 @@
/* ============================================
VARIABLES CSS - Design System Couleurs Unies
============================================ */
/* Fix select dropdowns */
select.form-control {
width: 100% !important;
min-width: 0;
text-overflow: ellipsis;
-webkit-appearance: menulist;
-moz-appearance: menulist;
appearance: menulist;
padding-right: 30px !important;
color: #333 !important;
background-color: #fff !important;
height: auto !important;
min-height: 40px;
}
select.form-control option {
color: #333;
background: #fff;
padding: 5px 10px;
}
:root {
--primary-color: #3498db;
--success-color: #2ecc71;
@ -146,13 +173,12 @@ body {
margin: 0;
padding: 0;
font-family: 'Inter', 'Segoe UI', -apple-system, BlinkMacSystemFont, 'Source Sans Pro', sans-serif !important;
overflow-x: hidden;
min-height: 100vh;
background: #3498db !important;
}
.wrapper {
margin-top:30px;
margin-top:50px;
display: flex;
flex-direction: column;
min-height: 100vh;
@ -165,14 +191,14 @@ body {
.content-wrapper {
flex: 1;
overflow-y: auto;
padding-bottom: 60px;
overflow-x: auto;
margin-left: 230px;
padding-top: 50px;
transition: margin-left 0.3s ease;
width: calc(100% - 230px);
transition: margin-left 0.3s ease, width 0.3s ease;
background: #ecf0f1 !important;
box-shadow: -5px 0 20px rgba(0, 0, 0, 0.1);
min-height: 100vh;
padding: 30px !important;
padding: 75px 30px 80px 30px !important;
}
/* ============================================
@ -188,6 +214,8 @@ body {
height: 50px;
transition: margin-left 0.3s ease;
box-shadow: 0 2px 15px rgba(0, 0, 0, 0.2);
display: flex;
align-items: stretch;
}
.content-header {
@ -240,21 +268,31 @@ body {
============================================ */
.main-sidebar {
position: fixed;
top: 0;
top: 50px;
left: 0;
height: 100vh;
height: calc(100vh - 50px);
overflow-y: auto;
z-index: 810;
width: 230px;
background: #2c3e50 !important;
transition: width 0.3s ease;
box-shadow: 2px 0 20px rgba(0, 0, 0, 0.3);
padding-top: 0 !important;
padding-bottom: 80px;
}
.main-sidebar .logo {
display: none;
}
/* Logo */
.logo {
display: block;
padding: 15px;
flex-shrink: 0;
width: 230px;
display: flex;
align-items: center;
justify-content: center;
padding: 0 15px;
text-align: center;
background: #3498db !important;
color: white;
@ -262,8 +300,11 @@ body {
font-weight: 700;
overflow: hidden;
white-space: nowrap;
transition: padding 0.3s ease;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
transition: width 0.3s ease, padding 0.3s ease;
}
.main-header .navbar {
flex: 1;
}
.logo-mini {
@ -696,7 +737,7 @@ body {
left: 230px;
margin-left: 2px;
right: 0;
z-index: 1000;
z-index: 800;
background: #ffffff !important;
padding: 12px 20px;
border-top: 2px solid #3498db;
@ -713,20 +754,30 @@ body {
============================================ */
.sidebar-toggle {
position: fixed;
top: 0;
left: 230px;
height: 50px;
line-height: 30px;
z-index: 2000;
background: #3c8dbc;
background: transparent;
color: #fff;
padding: 8px 12px;
border-radius: 3px;
padding: 0 15px;
cursor: pointer;
transition: left 0.3s ease;
font-size: 18px;
}
.sidebar-collapsed .sidebar-toggle {
left: 70px;
}
.sidebar-toggle::before {
content: "\f0c9";
font-family: FontAwesome;
font-style: normal;
font-weight: normal;
}
/* ============================================
NOTIFICATIONS - Badge modernisé
============================================ */
@ -791,16 +842,69 @@ body {
.sidebar-collapsed .main-sidebar {
width: 70px;
overflow-y: auto;
overflow-x: visible;
overflow-x: hidden;
direction: rtl;
transition: width 0.35s ease;
}
/* Ouvrir la sidebar au survol */
.sidebar-collapsed .main-sidebar:hover {
width: 230px;
overflow-x: visible;
}
.sidebar-collapsed .main-sidebar:hover .sidebar-menu > li > a > span {
opacity: 1;
max-width: 180px;
white-space: nowrap;
transition: opacity 0.2s ease 0.15s, max-width 0.35s ease;
}
.sidebar-collapsed .main-sidebar:hover .sidebar-menu > li > a .pull-right-container {
opacity: 1;
max-width: 20px;
transition: opacity 0.2s ease 0.15s, max-width 0.35s ease;
}
.sidebar-collapsed .main-sidebar:hover .sidebar-menu > li > a {
text-align: left;
padding: 12px 15px;
width: auto;
justify-content: flex-start;
}
.sidebar-collapsed .main-sidebar:hover .sidebar-menu i {
margin-right: 10px;
display: inline-block;
font-size: 16px;
}
.sidebar-collapsed .main-sidebar:hover .logo {
width: 230px;
padding: 15px;
}
.sidebar-collapsed .main-sidebar:hover .logo-mini {
display: none !important;
opacity: 0;
max-width: 0;
}
.sidebar-collapsed .main-sidebar:hover .logo-lg {
display: inline !important;
opacity: 1 !important;
max-width: 200px !important;
overflow: visible !important;
transition: opacity 0.25s ease 0.15s, max-width 0.35s ease;
}
/* Sous-menus gérés plus bas */
.sidebar-collapsed .main-sidebar .sidebar-menu {
direction: ltr;
}
.sidebar-collapsed .content-wrapper {
margin-left: 70px;
width: calc(100% - 70px);
}
.sidebar-collapsed .main-sidebar:hover ~ .content-wrapper {
margin-left: 230px;
width: calc(100% - 230px);
transition: margin-left 0.35s ease, width 0.35s ease;
}
.sidebar-collapsed .main-header {
@ -813,11 +917,35 @@ body {
margin-left: 2px;
}
.sidebar-collapsed .main-sidebar:hover ~ .main-footer {
left: 230px;
transition: left 0.35s ease;
}
.sidebar-collapsed .sidebar-toggle {
left: 70px;
}
.sidebar-collapsed .logo {
.sidebar-collapsed .main-header .logo {
width: 70px;
padding: 0 5px;
}
.sidebar-collapsed .main-header .logo-mini {
display: inline-block;
}
.sidebar-collapsed .main-header .logo-lg {
display: none;
}
.logo-mini img {
max-height: 40px;
max-width: 50px;
border-radius: 4px;
}
.sidebar-collapsed .main-sidebar .logo {
padding: 15px 5px;
text-align: center;
display: flex;
@ -827,22 +955,37 @@ body {
box-sizing: border-box;
}
.sidebar-collapsed .logo-mini {
.sidebar-collapsed .main-sidebar .logo-mini {
display: inline-block;
text-align: center;
width: 100%;
opacity: 1;
max-width: 60px;
overflow: hidden;
transition: opacity 0.25s ease 0.1s, max-width 0.35s ease;
}
.sidebar-collapsed .logo-lg {
display: none;
.sidebar-collapsed .main-sidebar .logo-lg {
opacity: 0;
max-width: 0;
overflow: hidden;
white-space: nowrap;
transition: opacity 0.25s ease, max-width 0.35s ease;
}
.sidebar-collapsed .sidebar-menu > li > a > span {
display: none;
opacity: 0;
max-width: 0;
overflow: hidden;
white-space: nowrap;
transition: opacity 0.2s ease, max-width 0.35s ease;
}
.sidebar-collapsed .sidebar-menu > li > a .pull-right-container {
display: none;
opacity: 0;
max-width: 0;
overflow: hidden;
transition: opacity 0.2s ease, max-width 0.35s ease;
}
.sidebar-collapsed .sidebar-menu > li > a {
@ -854,7 +997,17 @@ body {
justify-content: center;
width: 70px;
box-sizing: border-box;
margin: 5px auto;
margin: 0 auto;
}
.sidebar-collapsed .sidebar-menu > li {
margin: 0;
padding: 0;
}
.sidebar-collapsed .sidebar-menu > li.treeview > .treeview-menu {
margin: 0;
padding: 0;
}
.sidebar-collapsed .sidebar-menu i {
@ -865,114 +1018,31 @@ body {
text-align: center;
}
/* Pas de tooltip en mode réduit */
.sidebar-collapsed .sidebar-menu > li > a::before {
content: attr(data-tooltip);
position: absolute;
left: 70px;
top: 50%;
transform: translateY(-50%);
background: #3498db;
color: #fff;
padding: 10px 15px;
border-radius: 8px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: all 0.3s ease;
z-index: 9998;
font-size: 14px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
font-weight: 600;
pointer-events: none;
display: none;
}
.sidebar-collapsed .sidebar-menu > li:hover > a::before {
opacity: 1;
visibility: visible;
left: 80px;
}
.sidebar-collapsed .treeview-menu {
position: fixed;
left: 80px;
top: auto;
width: 200px;
max-width: calc(100vw - 100px);
z-index: 9999;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.25);
border-radius: 4px;
border-left: 2px solid #3c8dbc;
opacity: 0;
visibility: hidden;
transition: opacity 0.3s ease, visibility 0.3s ease, transform 0.3s ease;
display: block !important;
background: #fff;
backdrop-filter: blur(8px);
border: 1px solid rgba(210, 214, 222, 0.8);
pointer-events: none;
transform: translateX(-20px) scale(0.95);
transform-origin: left center;
margin-top: 0;
}
.sidebar-collapsed .treeview:hover .treeview-menu {
pointer-events: auto !important;
opacity: 1;
visibility: visible;
transform: translateX(0) scale(1);
}
.sidebar-collapsed .treeview-menu:hover {
/* Sidebar hover ouverte : sous-menus par clic (jQuery gère display) */
.sidebar-collapsed .main-sidebar:hover .treeview-menu {
position: static !important;
width: auto !important;
background: rgba(0,0,0,0.15) !important;
border: none !important;
box-shadow: none !important;
opacity: 1 !important;
visibility: visible !important;
transform: none !important;
pointer-events: auto !important;
transform: translateX(0) scale(1) !important;
}
.sidebar-collapsed .treeview-menu > li > a {
padding: 8px 12px;
color: #495057;
white-space: nowrap;
border-bottom: 1px solid rgba(210, 214, 222, 0.4);
transition: all 0.3s ease;
font-size: 12px;
font-weight: 500;
.sidebar-collapsed .main-sidebar:hover .treeview-menu > li > a {
padding: 8px 15px 8px 30px;
color: #b8c7ce;
display: block;
position: relative;
cursor: pointer;
text-decoration: none;
pointer-events: auto !important;
z-index: 10000;
}
.sidebar-collapsed .treeview-menu > li:last-child > a {
border-bottom: none;
border-radius: 0 0 4px 4px;
}
.sidebar-collapsed .treeview-menu > li:first-child > a {
border-radius: 4px 4px 0 0;
}
.sidebar-collapsed .treeview-menu > li > a:hover {
background: #3c8dbc;
.sidebar-collapsed .main-sidebar:hover .treeview-menu > li > a:hover {
color: #fff;
padding-left: 15px;
transform: translateX(2px);
}
.sidebar-collapsed .treeview-menu > li > a i {
margin-right: 6px;
width: 14px;
text-align: center;
font-size: 11px;
opacity: 0.7;
transition: all 0.3s ease;
}
.sidebar-collapsed .treeview-menu > li > a:hover i {
opacity: 1;
transform: scale(1.05);
background: rgba(255,255,255,0.1);
}
/* ============================================
@ -1167,11 +1237,6 @@ body {
left: 10px;
}
/* Treeview menu sur mobile */
.sidebar-collapsed .treeview-menu {
left: 70px;
width: 180px;
}
/* Charts containers */
.col-lg-6[style*="background-color: white"] {
@ -1563,9 +1628,7 @@ body {
.small-box:nth-child(4) { animation-delay: 0.4s; }
.small-box:nth-child(5) { animation-delay: 0.5s; }
.sidebar-collapsed .treeview:hover .treeview-menu {
animation: slideInFromSidebar 0.3s cubic-bezier(0.4, 0, 0.2, 1);
}
/* animation removed - using click-based submenus */
/* Désactiver les animations sur mobile pour de meilleures performances */
@media (max-width: 768px) {

View File

@ -1,25 +1,23 @@
<header class="main-header">
<!-- Logo -->
<a href="#" class="logo">
<span class="logo-mini"><b><img src="<?= base_url('assets/images/company_logo.jpg') ?>"></b></span>
<span class="logo-lg"><b>MotorBike</b></span>
<span class="logo-mini"><b><?php if (!empty($company_logo)): ?><img src="<?= base_url($company_logo) ?>?v=<?= time() ?>"><?php else: ?><img src="<?= base_url('assets/images/company_logo.jpg') ?>?v=<?= time() ?>" style="opacity:0.4;"><?php endif; ?></b></span>
<span class="logo-lg"><b><?= $company_name ?? 'MotorBike' ?></b></span>
</a>
<!-- Header Navbar -->
<nav class="navbar navbar-static-top" style="position: relative; height: 50px; background: #3c8dbc;">
<!-- Sidebar toggle button -->
<a href="#" class="sidebar-toggle" onclick="toggleSidebar()">
<span class="sr-only">Toggle navigation</span>
</a>
<a href="#" class="sidebar-toggle" onclick="toggleSidebar()"></a>
<!-- Navbar Right Menu -->
<ul class="navbar-nav" style="position: absolute; right: 15px; top: 50%; transform: translateY(-50%); margin: 0; padding: 0; list-style: none; display: flex; align-items: center; gap: 20px;">
<!-- Notifications -->
<li class="nav-item dropdown" style="position: relative;">
<i class="fa fa-bell" id="notificationIcon" style="font-size: 20px; cursor: pointer; color:white;" data-toggle="dropdown"></i>
<li class="nav-item" style="position: relative;">
<i class="fa fa-bell" id="notificationIcon" style="font-size: 20px; cursor: pointer; color:white;" onclick="toggleNotifPanel()"></i>
<span id="notificationCount" class="navbar-badge"></span>
<div class="dropdown-menu dropdown-menu-right notif-dropdown">
<div id="notifPanel" class="notif-dropdown" style="display:none; position:absolute; right:0; top:35px; z-index:9999;">
<div class="notif-panel-header">
<span class="notif-panel-title" id="notificationHeader">
<i class="fa fa-bell"></i> Notifications
@ -126,6 +124,7 @@
#notificationList {
overflow-y: auto;
flex: 1;
max-height: 460px;
padding: 8px;
background: #f8f9fa;
}
@ -234,6 +233,21 @@
</style>
<script>
function toggleNotifPanel() {
var panel = document.getElementById('notifPanel');
panel.style.display = panel.style.display === 'none' ? 'block' : 'none';
}
// Fermer le panel en cliquant en dehors
document.addEventListener('click', function(e) {
var panel = document.getElementById('notifPanel');
var icon = document.getElementById('notificationIcon');
var badge = document.getElementById('notificationCount');
if (panel.style.display === 'block' && !panel.contains(e.target) && e.target !== icon && e.target !== badge) {
panel.style.display = 'none';
}
});
function fetchNotifications() {
$.ajax({
url: '/notifications',
@ -359,4 +373,47 @@ $(document).on('click', '#markAllAsReadBtn', function(e) {
setInterval(fetchNotifications, 10000);
fetchNotifications();
function showMotoDetails(btn) {
var data = JSON.parse(btn.getAttribute('data-moto'));
document.getElementById('motoModalName').textContent = data.name || '-';
document.getElementById('motoModalSku').textContent = data.sku || '-';
document.getElementById('motoModalMoteur').textContent = data.moteur || '-';
document.getElementById('motoModalChasis').textContent = data.chasis || '-';
document.getElementById('motoModalPuissance').textContent= data.puissance || '-';
var imgEl = document.getElementById('motoModalImg');
if (data.image) {
imgEl.src = data.image;
imgEl.style.display = 'block';
} else {
imgEl.style.display = 'none';
}
$('#motoDetailModal').modal('show');
}
</script>
<!-- Modal détails moto -->
<div class="modal fade" id="motoDetailModal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header" style="background:#3c8dbc;color:#fff;">
<button type="button" class="close" data-dismiss="modal" style="color:#fff;opacity:1;">&times;</button>
<h4 class="modal-title"><i class="fa fa-motorcycle"></i> Détails de la moto</h4>
</div>
<div class="modal-body" style="text-align:center;">
<img id="motoModalImg" src="" alt="Photo moto"
style="max-width:100%;max-height:250px;object-fit:contain;border-radius:8px;margin-bottom:16px;border:1px solid #eee;">
<table class="table table-bordered table-condensed" style="text-align:left;margin-top:10px;">
<tr><th style="width:40%">Modèle</th><td id="motoModalName"></td></tr>
<tr><th> Série</th><td id="motoModalSku"></td></tr>
<tr><th> Moteur</th><td id="motoModalMoteur"></td></tr>
<tr><th>Châssis</th><td id="motoModalChasis"></td></tr>
<tr><th>Puissance</th><td id="motoModalPuissance"></td></tr>
</table>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
</div>
</div>
</div>
</div>

View File

@ -273,7 +273,6 @@
<li id="reportNav">
<a href="<?php echo base_url('reports/') ?>">
<i class="glyphicon glyphicon-stats"></i> <span>Rapports</span>
<span class="tooltip">Rapports</span>
</a>
</li>
<?php endif; ?>
@ -286,6 +285,18 @@
</li>
<?php endif; ?> -->
<?php
$session = session();
$currentUser = $session->get('user');
if (isset($currentUser['group_name']) && $currentUser['group_name'] === 'SuperAdmin'):
?>
<li id="mainActionLogNav">
<a href="<?php echo base_url('action-log') ?>">
<i class="fa fa-history"></i> <span>Historique des Actions</span>
</a>
</li>
<?php endif; ?>
<?php if (in_array('updateCompany', $user_permission)): ?>
<li id="companyNav"><a href="<?php echo base_url('company/') ?>"><i class="fa fa-building"></i> <span>Entreprise</span></a></li>
<?php endif; ?>

View File

@ -53,9 +53,7 @@
<th>Phone</th>
<th>Point de vente</th>
<th>Role</th>
<?php if (in_array('updateUser', $user_permission) || in_array('deleteUser', $user_permission)): ?>
<th>Action</th>
<?php endif; ?>
</tr>
</thead>