Browse Source

feat: afficher les infos complètes du moto dans les notifications de remise

- Remise.php : ajout méthode getFullProductInfoByDemandeId() avec JOIN orders_item/products/brands
- RemiseController.php : utilisation des infos complètes (modèle, marque, N° série, N° moteur, châssis, puissance) dans les notifications de validation/refus
- OrderController.php : enrichissement du message de notification lors de la création d'une demande de remise
- header_menu.php : refonte complète du design des notifications (cartes colorées par type, badge, horloge relative, point non-lu)
pull/1/head
Stephane 2 weeks ago
parent
commit
3c7585b3a2
  1. 2
      .vscode/sftp.json
  2. 117
      README.md
  3. 11
      app/Config/Kint.php
  4. 14
      app/Controllers/OrderController.php
  5. 17
      app/Controllers/RemiseController.php
  6. 2
      app/Helpers/alerts_helper.php
  7. 2
      app/Models/Orders.php
  8. 14
      app/Models/Remise.php
  9. 4
      app/Views/autres_encaissements/index.php
  10. 2
      app/Views/demande/index.php
  11. 4
      app/Views/historique/index.php
  12. 18
      app/Views/orders/index.php
  13. 2
      app/Views/recouvrement/recouvrement.php
  14. 2
      app/Views/reports/stockDetail.php
  15. 2
      app/Views/securite/index.php
  16. 12
      app/Views/templates/header.php
  17. 278
      app/Views/templates/header_menu.php
  18. 17
      composer.json
  19. 80
      public/index.php
  20. 75
      spark

2
.vscode/sftp.json

@ -6,7 +6,7 @@
"username": "motorbike", "username": "motorbike",
"remotePath": "/home/motorbike/public_html/", "remotePath": "/home/motorbike/public_html/",
"password": "IVrMDogT3XiBcrY", "password": "IVrMDogT3XiBcrY",
"uploadOnSave": false, "uploadOnSave": true,
"useTempFile": false, "useTempFile": false,
"openSsh": false "openSsh": false
} }

117
README.md

@ -0,0 +1,117 @@
# MOTORBiKE
Application web de gestion commerciale pour une entreprise de vente et maintenance de motos, developpee avec CodeIgniter 4.
## Fonctionnalites
- **Gestion des ventes / commandes** - creation, modification, suivi et impression de bons de livraison
- **Avances** - gestion des acomptes clients avec conversion automatique en commande et alertes d'echeance
- **Recouvrement** - suivi des paiements et creances
- **Caisse / Sortie caisse** - gestion des encaissements et decaissements avec export Excel/CSV
- **Produits** - catalogue avec attributs, categories, marques, images et import depuis Excel
- **Stocks** - affectation des produits par magasin/point de vente
- **Mecaniciens** - suivi des performances des techniciens
- **Utilisateurs & Groupes** - gestion des acces par roles et groupes de permissions
- **Magasins** - gestion multi-points de vente
- **Statistiques & Rapports** - tableaux de bord, rapports de ventes, de stock et de performances
- **Historique** - traçabilite des actions avec export
- **Notifications** - alertes en temps reel (echeances, etc.)
- **QR Code** - generation de QR codes produits
- **Securite** - validation de securite avec historique
## Stack technique
- **Framework** : CodeIgniter 4 (PHP 8.2+)
- **Base de donnees** : MySQL (via MySQLi)
- **Authentification** : JWT (`firebase/php-jwt`)
- **Export** : PhpSpreadsheet (`phpoffice/phpspreadsheet`)
- **Tests** : PHPUnit 9
## Prerequis
- PHP >= 8.2 avec les extensions : `curl`, `intl`, `json`, `mbstring`, `mysqli`
- MySQL >= 5.7 / MariaDB
- Composer
## Installation
```bash
# Cloner le depot
git clone <url-du-depot> motorbike
cd motorbike
# Installer les dependances
composer install
# Configurer l'environnement
cp .env.example .env
# Editer .env avec vos parametres de base de donnees et URL
# Executer les migrations
php spark migrate
# Lancer le serveur de developpement
php spark serve
```
## Configuration (.env)
```ini
CI_ENVIRONMENT = development
app.baseURL = 'http://localhost:8080/'
database.default.hostname = localhost
database.default.database = motorbike
database.default.username = <votre_utilisateur>
database.default.password = <votre_mot_de_passe>
database.default.DBDriver = MySQLi
database.default.port = 3306
```
## Structure du projet
```
app/
Config/ - Configuration (routes, filtres, base de donnees...)
Controllers/ - Controleurs de l'application
Database/ - Migrations et seeds
Filters/ - Filtres d'authentification (auth, loggedIn, publicCheck)
Models/ - Modeles de donnees
Views/ - Templates (dashboard, commandes, produits, rapports...)
public/
assets/ - CSS, JS, images
```
## Routes principales
| Chemin | Description |
|--------|-------------|
| `/` | Tableau de bord |
| `/login` | Authentification |
| `/orders` | Commandes / Ventes |
| `/avances` | Gestion des avances |
| `/products` | Catalogue produits |
| `/stores` | Magasins |
| `/recouvrement` | Recouvrement |
| `/sortieCaisse` | Sortie de caisse |
| `/reports` | Rapports |
| `/statistic` | Statistiques |
| `/users` | Utilisateurs |
| `/groups` | Groupes / Roles |
| `/brands` | Marques |
| `/category` | Categories |
| `/mecanicien` | Mecaniciens |
| `/historique` | Historique |
## Tests
```bash
composer test
# ou
php spark test
```
## Licence
MIT

11
app/Config/Kint.php

@ -20,6 +20,17 @@ use Kint\Renderer\Rich\ValuePluginInterface;
*/ */
class Kint extends BaseConfig class Kint extends BaseConfig
{ {
public function __construct()
{
parent::__construct();
// ini_get('xdebug.file_link_format') returns false when xdebug is not
// installed. Kint's init.php assigns this directly, making str_replace()
// throw a TypeError on PHP 8.2+. Reset to empty string when false.
if (\Kint\Kint::$file_link_format === false) {
\Kint\Kint::$file_link_format = '';
}
}
/* /*
|-------------------------------------------------------------------------- |--------------------------------------------------------------------------
| Global Settings | Global Settings

14
app/Controllers/OrderController.php

@ -218,7 +218,7 @@ class OrderController extends AdminController
// ======================================== // ========================================
// POUR DIRECTION OU DAF // POUR DIRECTION OU DAF
// ======================================== // ========================================
elseif($users['group_name'] == "Direction" || $users['group_name'] == "DAF" || $users['group_name'] == "SuperAdmin" ){ elseif(in_array($users['group_name'], ["Direction", "DAF", "SuperAdmin", "Administrator"])){
foreach ($data as $key => $value) { foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time'])); $date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
@ -594,12 +594,15 @@ class OrderController extends AdminController
} }
$product_lines = []; $product_lines = [];
$product_info_lines = [];
foreach ($product_data_results as $product) { foreach ($product_data_results as $product) {
if (isset($product['sku'], $product['price'])) { if (isset($product['sku'], $product['price'])) {
$sku = $product['sku']; $product_lines[] = $product['sku'] . ':' . $product['price'];
$price = $product['price'];
$product_lines[] = "{$sku}:{$price}";
} }
$product_info_lines[] = "• " . ($product['name'] ?? '-') .
" | N° Série : " . ($product['sku'] ?? '-') .
" | N° Moteur : " . ($product['numero_de_moteur'] ?? '-') .
" | Châssis : " . ($product['chasis'] ?? '-');
} }
$product_output = implode("\n", $product_lines); $product_output = implode("\n", $product_lines);
@ -623,7 +626,8 @@ class OrderController extends AdminController
$message = "💰 Nouvelle demande de remise : {$montantFormatted} Ar<br>" . $message = "💰 Nouvelle demande de remise : {$montantFormatted} Ar<br>" .
"Commande : {$bill_no}<br>" . "Commande : {$bill_no}<br>" .
"Store : " . $this->returnStore($users['store_id']) . "<br>" . "Store : " . $this->returnStore($users['store_id']) . "<br>" .
"Demandeur : {$users['firstname']} {$users['lastname']}"; "Demandeur : {$users['firstname']} {$users['lastname']}<br>" .
implode("<br>", $product_info_lines);
if (is_array($allStores) && count($allStores) > 0) { if (is_array($allStores) && count($allStores) > 0) {
foreach ($allStores as $store) { foreach ($allStores as $store) {

17
app/Controllers/RemiseController.php

@ -129,16 +129,29 @@ class RemiseController extends AdminController
]; ];
if ($Remise->updateRemise($id_demande, $data)) { if ($Remise->updateRemise($id_demande, $data)) {
$remise_product = $Remise->getProductByDemandeId($id_demande);
$Notification = new NotificationController(); $Notification = new NotificationController();
$ordersModel = new Orders(); $ordersModel = new Orders();
$order_id = $Remise->getOrderIdByDemandeId($id_demande); $order_id = $Remise->getOrderIdByDemandeId($id_demande);
// Récupérer les infos de la commande // Récupérer les infos de la commande
$order_info = $ordersModel->getOrdersData($order_id); $order_info = $ordersModel->getOrdersData($order_id);
$bill_no = $order_info['bill_no'] ?? ''; $bill_no = $order_info['bill_no'] ?? '';
$store_id = $order_info['store_id'] ?? 0; $store_id = $order_info['store_id'] ?? 0;
// Récupérer les infos complètes des motos
$products_info = $Remise->getFullProductInfoByDemandeId($id_demande);
$remise_product = '';
foreach ($products_info as $p) {
$remise_product .= "<br>• Modèle : " . ($p['name'] ?? '-');
$remise_product .= " | Marque : " . ($p['marque_name'] ?? '-');
$remise_product .= " | N° Série : " . ($p['sku'] ?? '-');
$remise_product .= " | N° Moteur : " . ($p['numero_de_moteur'] ?? '-');
$remise_product .= " | Châssis : " . ($p['chasis'] ?? '-');
if (!empty($p['puissance'])) {
$remise_product .= " | Puissance : " . $p['puissance'];
}
}
// ✅ RÉCUPÉRER TOUS LES STORES // ✅ RÉCUPÉRER TOUS LES STORES
$Stores = new Stores(); $Stores = new Stores();
$allStores = $Stores->getActiveStore(); $allStores = $Stores->getActiveStore();

2
app/Helpers/alerts_helper.php

@ -46,7 +46,7 @@ function checkDeadlineAlerts()
log_message('error', "Aucun email DAF trouvé"); log_message('error', "Aucun email DAF trouvé");
$db = \Config\Database::connect(); $db = \Config\Database::connect();
$allGroups = $db->query("SELECT DISTINCT group_name FROM groups")->getResult(); $allGroups = $db->query("SELECT DISTINCT group_name FROM `groups`")->getResult();
log_message('info', "Groupes disponibles: " . json_encode($allGroups)); log_message('info', "Groupes disponibles: " . json_encode($allGroups));
return; return;

2
app/Models/Orders.php

@ -131,7 +131,7 @@ class Orders extends Model
$groupName = $group['group_name'] ?? ''; $groupName = $group['group_name'] ?? '';
// Selon le rôle // Selon le rôle
if (in_array($groupName, ['Direction', 'SuperAdmin', 'DAF'], true)) { if (in_array($groupName, ['Direction', 'SuperAdmin', 'DAF', 'Administrator'], true)) {
return $builder return $builder
->orderBy('orders.id', 'DESC') ->orderBy('orders.id', 'DESC')
->get() ->get()

14
app/Models/Remise.php

@ -92,6 +92,20 @@ class Remise extends Model
return $row['product'] ?? null; return $row['product'] ?? null;
} }
public function getFullProductInfoByDemandeId(int $id_demande): array
{
$order_id = $this->getOrderIdByDemandeId($id_demande);
if (!$order_id) return [];
return $this->db->table('orders_item')
->select('products.name, products.sku, products.numero_de_moteur, products.chasis, products.puissance, brands.name as marque_name')
->join('products', 'products.id = orders_item.product_id', 'left')
->join('brands', 'brands.id = products.marque', 'left')
->where('orders_item.order_id', $order_id)
->get()
->getResultArray();
}
public function updateRemise1(int $id, $data) public function updateRemise1(int $id, $data)
{ {
$existing = $this->where('id_order', $id)->first(); $existing = $this->where('id_order', $id)->first();

4
app/Views/autres_encaissements/index.php

@ -284,14 +284,13 @@
</h3> </h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="table-responsive">
<table id="historyTable" class="table table-bordered table-striped table-hover"> <table id="historyTable" class="table table-bordered table-striped table-hover">
<thead> <thead>
<tr> <tr>
<th>ID</th> <th>ID</th>
<th>Type</th> <th>Type</th>
<th>Montant</th> <th>Montant</th>
<th>Mode</th> <!-- ✅ NOUVELLE COLONNE --> <th>Mode</th>
<th>Commentaire</th> <th>Commentaire</th>
<th>Créé par</th> <th>Créé par</th>
<th>Magasin</th> <th>Magasin</th>
@ -307,7 +306,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>

2
app/Views/demande/index.php

@ -38,7 +38,6 @@
<h3 class="box-title">Gérer les remises</h3> <h3 class="box-title">Gérer les remises</h3>
</div> </div>
<div class="table-responsive">
<table id="manageTable" class="table table-bordered table-striped"> <table id="manageTable" class="table table-bordered table-striped">
<thead> <thead>
<tr> <tr>
@ -56,7 +55,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>

4
app/Views/historique/index.php

@ -68,8 +68,7 @@
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="table-responsive"> <table id="historiqueTable" class="table table-bordered table-striped table-hover" style="width:100%;">
<table id="historiqueTable" class="table table-bordered table-striped table-hover nowrap" style="width:100%;">
<thead class="bg-light-blue"> <thead class="bg-light-blue">
<tr> <tr>
<th>Date</th> <th>Date</th>
@ -82,7 +81,6 @@
</thead> </thead>
<tbody></tbody> <tbody></tbody>
</table> </table>
</div>
<!-- Loader --> <!-- Loader -->
<div id="loading" style="display:none;text-align:center;margin:20px;"> <div id="loading" style="display:none;text-align:center;margin:20px;">

18
app/Views/orders/index.php

@ -46,11 +46,11 @@
<table id="manageTable" class="table table-bordered table-striped"> <table id="manageTable" class="table table-bordered table-striped">
<thead> <thead>
<tr> <tr>
<?php <?php
$session = session(); $session = session();
$users = $session->get('user'); $users = $session->get('user');
if ($users['group_name'] === 'SuperAdmin' || $users['group_name'] === "Direction" || $users['group_name'] === "DAF" ) { $groupName = $users['group_name'];
if (in_array($groupName, ['SuperAdmin', 'Direction', 'DAF', 'Administrator'])) {
?> ?>
<th>Facture n°</th> <th>Facture n°</th>
<th>Nom du client</th> <th>Nom du client</th>
@ -59,7 +59,6 @@
<th>Prix demandé</th> <th>Prix demandé</th>
<th>Prix de vente</th> <th>Prix de vente</th>
<th>Status</th> <th>Status</th>
<?php if ( <?php if (
in_array('updateOrder', $user_permission) in_array('updateOrder', $user_permission)
|| in_array('viewOrder', $user_permission) || in_array('viewOrder', $user_permission)
@ -67,14 +66,7 @@
) { ?> ) { ?>
<th>Action</th> <th>Action</th>
<?php } ?> <?php } ?>
<?php } ?> <?php } elseif ($groupName === 'SECURITE') { ?>
<?php
$session = session();
$users = $session->get('user');
// Interface spécifique pour SECURITE
if ($users['group_name'] === 'SECURITE') {
?>
<th>Nom du produit</th> <th>Nom du produit</th>
<th>Commerciale</th> <th>Commerciale</th>
<th>Date et Heure</th> <th>Date et Heure</th>
@ -87,9 +79,7 @@
) { ?> ) { ?>
<th>Action</th> <th>Action</th>
<?php } ?> <?php } ?>
<?php } elseif ($users['group_name'] === 'COMMERCIALE' || $users['group_name'] === 'Caissière' || $users['group_name'] === "Cheffe d'Agence") { <?php } else { // COMMERCIALE, Caissière, Cheffe d'Agence, et tous les autres rôles ?>
// Interface pour les autres rôles (COMMERCIALE, Caissière, Cheffe d'Agence)
?>
<th>Nom du produit</th> <th>Nom du produit</th>
<th>Commerciale</th> <th>Commerciale</th>
<th>Date et Heure</th> <th>Date et Heure</th>

2
app/Views/recouvrement/recouvrement.php

@ -19,7 +19,6 @@
</div> </div>
<!-- Tableau des recouvrements --> <!-- Tableau des recouvrements -->
<div class="table-responsive">
<table id="recouvrement_table" class="table table-hover table-bordered"> <table id="recouvrement_table" class="table table-hover table-bordered">
<thead> <thead>
<tr> <tr>
@ -32,7 +31,6 @@
</tr> </tr>
</thead> </thead>
</table> </table>
</div>
<!-- Résumé des paiements --> <!-- Résumé des paiements -->
<div class="row text-center mt-4"> <div class="row text-center mt-4">

2
app/Views/reports/stockDetail.php

@ -145,7 +145,6 @@
</div> </div>
</div> </div>
<div class="table-responsive">
<table id="export1" <table id="export1"
class="table table-hover table-striped table-bordered"> class="table table-hover table-striped table-bordered">
<thead class="table-primary"> <thead class="table-primary">
@ -163,7 +162,6 @@
</table> </table>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> <!-- End row --> </div> <!-- End row -->

2
app/Views/securite/index.php

@ -239,7 +239,6 @@
</h3> </h3>
</div> </div>
<div class="box-body"> <div class="box-body">
<div class="table-responsive">
<table id="historyTable" class="table table-bordered table-striped table-hover"> <table id="historyTable" class="table table-bordered table-striped table-hover">
<thead> <thead>
<tr> <tr>
@ -264,7 +263,6 @@
</div> </div>
</div> </div>
</div> </div>
</div>
</section> </section>
</div> </div>

12
app/Views/templates/header.php

@ -33,6 +33,8 @@
href="<?php echo base_url('assets/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') ?>"> href="<?php echo base_url('assets/plugins/bootstrap-wysihtml5/bootstrap3-wysihtml5.min.css') ?>">
<link rel="stylesheet" <link rel="stylesheet"
href="<?php echo base_url('assets/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css') ?>"> href="<?php echo base_url('assets/bower_components/datatables.net-bs/css/dataTables.bootstrap.min.css') ?>">
<!-- DataTables Responsive -->
<link rel="stylesheet" href="https://cdn.datatables.net/responsive/2.5.0/css/responsive.bootstrap.min.css">
<!-- Select2 --> <!-- Select2 -->
<link rel="stylesheet" href="<?php echo base_url('assets/bower_components/select2/dist/css/select2.min.css') ?>"> <link rel="stylesheet" href="<?php echo base_url('assets/bower_components/select2/dist/css/select2.min.css') ?>">
<link rel="stylesheet" href="<?php echo base_url('assets/plugins/fileinput/fileinput.min.css') ?>"> <link rel="stylesheet" href="<?php echo base_url('assets/plugins/fileinput/fileinput.min.css') ?>">
@ -99,8 +101,14 @@
<!-- DataTables --> <!-- DataTables -->
<script src="<?php echo base_url('assets/bower_components/datatables.net/js/jquery.dataTables.min.js') ?>"></script> <script src="<?php echo base_url('assets/bower_components/datatables.net/js/jquery.dataTables.min.js') ?>"></script>
<script <script src="<?php echo base_url('assets/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js') ?>"></script>
src="<?php echo base_url('assets/bower_components/datatables.net-bs/js/dataTables.bootstrap.min.js') ?>"></script> <!-- DataTables Responsive -->
<script src="https://cdn.datatables.net/responsive/2.5.0/js/dataTables.responsive.min.js"></script>
<script src="https://cdn.datatables.net/responsive/2.5.0/js/responsive.bootstrap.min.js"></script>
<script>
// Enable responsive for all DataTables globally
$.extend(true, $.fn.dataTable.defaults, { responsive: true });
</script>

278
app/Views/templates/header_menu.php

@ -18,18 +18,17 @@
<!-- Notifications --> <!-- Notifications -->
<li class="nav-item dropdown" style="position: relative;"> <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> <i class="fa fa-bell" id="notificationIcon" style="font-size: 20px; cursor: pointer; color:white;" data-toggle="dropdown"></i>
<span id="notificationCount" class="badge badge-warning navbar-badge"></span> <span id="notificationCount" class="navbar-badge"></span>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right" <div class="dropdown-menu dropdown-menu-right notif-dropdown">
style="width: 400px; padding: 5%; max-height: 500px; overflow: auto; margin-right: 5px;"> <div class="notif-panel-header">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0 10px;"> <span class="notif-panel-title" id="notificationHeader">
<span class="dropdown-header" id="notificationHeader" style="padding: 0;">0 Notifications</span> <i class="fa fa-bell"></i> Notifications
<button id="markAllAsReadBtn" class="btn btn-sm btn-primary" style="font-size: 12px; padding: 4px 10px;"> </span>
<i class="fa fa-check"></i> Marquer tout comme lu <button id="markAllAsReadBtn" style="display:none;">
<i class="fa fa-check-double"></i> Tout lire
</button> </button>
</div> </div>
<div class="dropdown-divider"></div>
<div id="notificationList"></div> <div id="notificationList"></div>
<div class="dropdown-divider"></div>
</div> </div>
</li> </li>
@ -59,43 +58,178 @@
<!-- Styles --> <!-- Styles -->
<style> <style>
/* Notifications non lues */ .navbar-badge {
.notification_item.unread { position: absolute;
font-weight: bold; top: -8px;
background-color: #f0f8ff; right: -8px;
padding: 3px 6px;
border-radius: 10px;
background: #e74c3c;
color: white;
font-size: 10px;
font-weight: 700;
line-height: 1;
min-width: 18px;
text-align: center;
}
/* Dropdown container */
.notif-dropdown {
width: 420px;
padding: 0;
max-height: 520px;
display: flex;
flex-direction: column;
border-radius: 12px;
overflow: hidden;
box-shadow: 0 8px 30px rgba(0,0,0,0.15);
border: 1px solid rgba(0,0,0,0.08);
}
/* Header du panel */
.notif-panel-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 14px 16px;
background: linear-gradient(135deg, #2c3e50, #3498db);
color: white;
flex-shrink: 0;
} }
.icon-unread { .notif-panel-title {
color: #007bff; font-size: 14px;
font-weight: 700;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
} }
#markAllAsReadBtn { #markAllAsReadBtn {
background: rgba(255,255,255,0.2);
border: 1px solid rgba(255,255,255,0.4);
color: white;
font-size: 11px;
padding: 4px 10px;
border-radius: 20px;
cursor: pointer;
white-space: nowrap; white-space: nowrap;
transition: all 0.2s;
} }
#markAllAsReadBtn:hover { #markAllAsReadBtn:hover {
background-color: #0056b3; background: rgba(255,255,255,0.35);
} }
/* Style pour le dropdown utilisateur */ /* Liste scrollable */
.dropdown-item { #notificationList {
transition: background-color 0.2s; overflow-y: auto;
flex: 1;
padding: 8px;
background: #f8f9fa;
} }
.dropdown-item:hover { #notificationList:empty::after {
background-color: #f5f5f5; content: 'Aucune notification';
text-decoration: none; display: block;
text-align: center;
padding: 30px;
color: #aaa;
font-size: 13px;
} }
.navbar-badge { /* Carte de notification */
position: absolute; .notif-card {
top: -8px; display: flex;
right: -8px; align-items: stretch;
padding: 3px 6px; margin-bottom: 6px;
border-radius: 10px; border-radius: 8px;
background: #f39c12; border: 1px solid #e8e8e8;
color: white; background: #fff;
font-size: 10px; text-decoration: none !important;
color: inherit !important;
overflow: hidden;
position: relative;
transition: box-shadow 0.2s, transform 0.1s;
cursor: pointer;
}
.notif-card:hover {
box-shadow: 0 3px 12px rgba(0,0,0,0.1);
transform: translateY(-1px);
text-decoration: none !important;
color: inherit !important;
}
.notif-card.notif-unread {
background: #f0f7ff;
border-color: #b8d9f8;
}
/* Barre colorée à gauche */
.notif-accent {
width: 5px;
flex-shrink: 0;
}
/* Corps de la notif */
.notif-body {
flex: 1;
padding: 10px 12px;
min-width: 0;
}
/* En-tête : type + heure */
.notif-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 5px;
gap: 8px;
}
.notif-type-badge {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
padding: 2px 8px;
border-radius: 20px;
color: #fff;
white-space: nowrap;
flex-shrink: 0;
}
.notif-time {
font-size: 11px;
color: #999;
white-space: nowrap;
flex-shrink: 0;
}
/* Contenu du message */
.notif-message {
font-size: 12.5px;
color: #444;
line-height: 1.55;
word-break: break-word;
}
.notif-card.notif-unread .notif-message {
color: #1a1a2e;
font-weight: 500;
}
/* Point bleu "non lu" */
.notif-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #3498db;
flex-shrink: 0;
align-self: flex-start;
margin: 12px 10px 0 0;
} }
</style> </style>
@ -110,30 +244,73 @@ function fetchNotifications() {
let notificationHTML = ''; let notificationHTML = '';
data.forEach(notif => { data.forEach(notif => {
if (notif.is_read == 0) { if (notif.is_read == 0) notificationCount++;
notificationCount++;
}
const href = '/' + notif.link.replace(/^\/+/, ''); const href = '/' + notif.link.replace(/^\/+/, '');
const notifClass = notif.is_read == 0 ? "notification_item unread" : "notification_item"; const isUnread = notif.is_read == 0;
const iconHTML = notif.is_read == 0 ? '<i class="fa fa-exclamation-circle icon-unread mr-9"></i>' : '';
// Détecter le type à partir du début du message
let accentColor = '#7f8c8d';
let badgeColor = '#7f8c8d';
let typeLabel = 'Info';
let typeIcon = 'fa-bell';
const msg = notif.message || '';
if (msg.includes('💰') || msg.toLowerCase().includes('remise')) {
accentColor = '#f39c12'; badgeColor = '#e67e22'; typeLabel = 'Remise'; typeIcon = 'fa-tag';
} else if (msg.includes('📦') || msg.toLowerCase().includes('commande')) {
accentColor = '#3498db'; badgeColor = '#2980b9'; typeLabel = 'Commande'; typeIcon = 'fa-shopping-cart';
} else if (msg.includes('✅') || msg.toLowerCase().includes('acceptée') || msg.toLowerCase().includes('validée')) {
accentColor = '#27ae60'; badgeColor = '#219a52'; typeLabel = 'Accepté'; typeIcon = 'fa-check-circle';
} else if (msg.includes('❌') || msg.toLowerCase().includes('refusée')) {
accentColor = '#e74c3c'; badgeColor = '#c0392b'; typeLabel = 'Refusé'; typeIcon = 'fa-times-circle';
} else if (msg.toLowerCase().includes('livraison') || msg.toLowerCase().includes('remis')) {
accentColor = '#9b59b6'; badgeColor = '#8e44ad'; typeLabel = 'Livraison'; typeIcon = 'fa-truck';
}
// Formater la date lisiblement
let dateDisplay = notif.created_at || '';
try {
const d = new Date(notif.created_at);
if (!isNaN(d)) {
const now = new Date();
const diffMs = now - d;
const diffMn = Math.floor(diffMs / 60000);
const diffH = Math.floor(diffMn / 60);
if (diffMn < 1) dateDisplay = 'À l\'instant';
else if (diffMn < 60) dateDisplay = diffMn + ' min';
else if (diffH < 24) dateDisplay = diffH + 'h';
else dateDisplay = d.toLocaleDateString('fr-FR', {day:'2-digit', month:'2-digit'});
}
} catch(e) {}
notificationHTML += ` notificationHTML += `
<a href="${href}" <a href="${href}" class="notif-card ${isUnread ? 'notif-unread' : ''}" data-id="${notif.id}">
class="${notifClass}" <div class="notif-accent" style="background:${accentColor};"></div>
data-id="${notif.id}" <div class="notif-body">
style="font-size: 15px; display: flex; align-items: center; justify-content: space-between;"> <div class="notif-header">
<span style="display: flex; align-items: center; gap:10px;"> <span class="notif-type-badge" style="background:${badgeColor};">
${iconHTML} ${notif.message} <i class="fa ${typeIcon}"></i> ${typeLabel}
</span> </span>
<span class="float-right text-muted text-sm">${notif.created_at}</span> <span class="notif-time"><i class="fa fa-clock-o"></i> ${dateDisplay}</span>
</div>
<div class="notif-message">${msg}</div>
</div>
${isUnread ? '<div class="notif-dot"></div>' : ''}
</a> </a>
<div class="dropdown-divider"></div>
`; `;
}); });
if (notificationHTML === '') {
notificationHTML = '';
}
$('#notificationList').html(notificationHTML); $('#notificationList').html(notificationHTML);
$('#notificationHeader').text(data.length + ' Notifications');
const unreadLabel = notificationCount > 0
? `<i class="fa fa-bell"></i> ${notificationCount} non lue${notificationCount > 1 ? 's' : ''}`
: `<i class="fa fa-bell"></i> Notifications`;
$('#notificationHeader').html(unreadLabel);
if (notificationCount > 0) { if (notificationCount > 0) {
$('#notificationCount').text(notificationCount).show(); $('#notificationCount').text(notificationCount).show();
@ -143,22 +320,17 @@ function fetchNotifications() {
$('#markAllAsReadBtn').hide(); $('#markAllAsReadBtn').hide();
} }
const items = document.querySelectorAll('.notification_item'); document.querySelectorAll('.notif-card').forEach(item => {
items.forEach(item => {
item.addEventListener('click', () => { item.addEventListener('click', () => {
const notifId = item.dataset.id; const notifId = item.dataset.id;
fetch("<?= base_url('notifications/markAsRead') ?>/" + notifId, { fetch("<?= base_url('notifications/markAsRead') ?>/" + notifId, {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" }
}) })
.then(response => response.json()) .then(r => r.json())
.then(data => { .catch(e => console.error(e));
console.log("Notification marked as read:", data);
})
.catch(error => console.error("Error:", error));
}); });
}); });
}, },
error: function(err) { error: function(err) {
console.error('Error fetching notifications:', err); console.error('Error fetching notifications:', err);

17
composer.json

@ -1,17 +1,15 @@
{ {
"name": "codeigniter4/framework", "name": "app/motorbike",
"type": "project", "type": "project",
"description": "The CodeIgniter framework v4", "description": "MOTORBiKE - Application de gestion commerciale",
"homepage": "https://codeigniter.com",
"license": "MIT", "license": "MIT",
"require": { "require": {
"php": "^7.4 || ^8.0", "php": "^8.2",
"codeigniter4/framework": "^4.3",
"ext-curl": "*", "ext-curl": "*",
"ext-intl": "*", "ext-intl": "*",
"ext-json": "*", "ext-json": "*",
"ext-mbstring": "*", "ext-mbstring": "*",
"laminas/laminas-escaper": "^2.9",
"psr/log": "^1.1",
"firebase/php-jwt": "^6.11", "firebase/php-jwt": "^6.11",
"kint-php/kint": "5.0", "kint-php/kint": "5.0",
"phpoffice/phpspreadsheet": "^5.0" "phpoffice/phpspreadsheet": "^5.0"
@ -46,7 +44,7 @@
}, },
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"CodeIgniter\\": "system/" "App\\": "app/"
}, },
"exclude-from-classmap": [ "exclude-from-classmap": [
"**/Database/Migrations/**" "**/Database/Migrations/**"
@ -56,6 +54,11 @@
"test": "phpunit" "test": "phpunit"
}, },
"config": {
"audit": {
"block-insecure": false
}
},
"support": { "support": {
"forum": "http://forum.codeigniter.com/", "forum": "http://forum.codeigniter.com/",
"source": "https://github.com/codeigniter4/CodeIgniter4", "source": "https://github.com/codeigniter4/CodeIgniter4",

80
public/index.php

@ -1,17 +1,34 @@
<?php <?php
// Check PHP version. use CodeIgniter\Boot;
$minPhpVersion = '7.4'; // If you update this, don't forget to update `spark`. use Config\Paths;
/*
*---------------------------------------------------------------
* CHECK PHP VERSION
*---------------------------------------------------------------
*/
$minPhpVersion = '8.2'; // If you update this, don't forget to update `spark`.
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
$message = sprintf( $message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s', 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
$minPhpVersion, $minPhpVersion,
PHP_VERSION PHP_VERSION,
); );
exit($message); header('HTTP/1.1 503 Service Unavailable.', true, 503);
echo $message;
exit(1);
} }
/*
*---------------------------------------------------------------
* SET THE CURRENT DIRECTORY
*---------------------------------------------------------------
*/
// Path to the front controller (this file) // Path to the front controller (this file)
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR); define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR);
@ -29,59 +46,14 @@ if (getcwd() . DIRECTORY_SEPARATOR !== FCPATH) {
* and fires up an environment-specific bootstrapping. * and fires up an environment-specific bootstrapping.
*/ */
// Load our paths config file // LOAD OUR PATHS CONFIG FILE
// This is the line that might need to be changed, depending on your folder structure. // This is the line that might need to be changed, depending on your folder structure.
require FCPATH . '../app/Config/Paths.php'; require FCPATH . '../app/Config/Paths.php';
// ^^^ Change this line if you move your application folder // ^^^ Change this line if you move your application folder
$paths = new Config\Paths(); $paths = new Paths();
// Location of the framework bootstrap file.
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
// Define ENVIRONMENT
if (! defined('ENVIRONMENT')) {
define('ENVIRONMENT', env('CI_ENVIRONMENT', 'development'));
}
// Load Config Cache
// $factoriesCache = new \CodeIgniter\Cache\FactoriesCache();
// $factoriesCache->load('config');
// ^^^ Uncomment these lines if you want to use Config Caching.
/*
* ---------------------------------------------------------------
* GRAB OUR CODEIGNITER INSTANCE
* ---------------------------------------------------------------
*
* The CodeIgniter class contains the core functionality to make
* the application run, and does all the dirty work to get
* the pieces all working together.
*/
$app = Config\Services::codeigniter();
$app->initialize();
$context = is_cli() ? 'php-cli' : 'web';
$app->setContext($context);
/*
*---------------------------------------------------------------
* LAUNCH THE APPLICATION
*---------------------------------------------------------------
* Now that everything is set up, it's time to actually fire
* up the engines and make this app do its thang.
*/
$app->run();
// Save Config Cache // LOAD THE FRAMEWORK BOOTSTRAP FILE
// $factoriesCache->save('config'); require $paths->systemDirectory . '/Boot.php';
// ^^^ Uncomment this line if you want to use Config Caching.
// Exits the application, setting the exit code for CLI-based applications exit(Boot::bootWeb($paths));
// that might be watching.
exit(EXIT_SUCCESS);

75
spark

@ -4,35 +4,46 @@
/** /**
* This file is part of CodeIgniter 4 framework. * This file is part of CodeIgniter 4 framework.
* *
* (c) CodeIgniter Foundation <Conseil@codeigniter.com> * (c) CodeIgniter Foundation <admin@codeigniter.com>
* *
* For the full copyright and license information, please view * For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code. * the LICENSE file that was distributed with this source code.
*/ */
use CodeIgniter\Boot;
use Config\Paths;
/* /*
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* CodeIgniter command-line tools * CODEIGNITER COMMAND-LINE TOOLS
* -------------------------------------------------------------------- * --------------------------------------------------------------------
* The main entry point into the CLI system and allows you to run * The main entry point into the CLI system and allows you to run
* commands and perform maintenance on your application. * commands and perform maintenance on your application.
* */
* Because CodeIgniter can handle CLI requests as just another web request
* this class mainly acts as a passthru to the framework itself. /*
*---------------------------------------------------------------
* CHECK SERVER API
*---------------------------------------------------------------
*/ */
// Refuse to run when called from php-cgi // Refuse to run when called from php-cgi
if (strpos(PHP_SAPI, 'cgi') === 0) { if (str_starts_with(PHP_SAPI, 'cgi')) {
exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n"); exit("The cli tool is not supported when running php-cgi. It needs php-cli to function!\n\n");
} }
// Check PHP version. /*
$minPhpVersion = '7.4'; // If you update this, don't forget to update `public/index.php`. *---------------------------------------------------------------
* CHECK PHP VERSION
*---------------------------------------------------------------
*/
$minPhpVersion = '8.2'; // If you update this, don't forget to update `public/index.php`.
if (version_compare(PHP_VERSION, $minPhpVersion, '<')) { if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
$message = sprintf( $message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s', 'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
$minPhpVersion, $minPhpVersion,
PHP_VERSION PHP_VERSION,
); );
exit($message); exit($message);
@ -42,12 +53,11 @@ if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
error_reporting(E_ALL); error_reporting(E_ALL);
ini_set('display_errors', '1'); ini_set('display_errors', '1');
/** /*
* @var bool *---------------------------------------------------------------
* * SET THE CURRENT DIRECTORY
* @deprecated No longer in use. `CodeIgniter` has `$context` property. *---------------------------------------------------------------
*/ */
define('SPARKED', true);
// Path to the front controller // Path to the front controller
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR); define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
@ -64,41 +74,14 @@ chdir(FCPATH);
* and fires up an environment-specific bootstrapping. * and fires up an environment-specific bootstrapping.
*/ */
// Load our paths config file // LOAD OUR PATHS CONFIG FILE
// This is the line that might need to be changed, depending on your folder structure. // This is the line that might need to be changed, depending on your folder structure.
require FCPATH . '../app/Config/Paths.php'; require FCPATH . '../app/Config/Paths.php';
// ^^^ Change this line if you move your application folder // ^^^ Change this line if you move your application folder
$paths = new Config\Paths(); $paths = new Paths();
// Location of the framework bootstrap file.
require rtrim($paths->systemDirectory, '\\/ ') . DIRECTORY_SEPARATOR . 'bootstrap.php';
// Load environment settings from .env files into $_SERVER and $_ENV
require_once SYSTEMPATH . 'Config/DotEnv.php';
(new CodeIgniter\Config\DotEnv(ROOTPATH))->load();
// Define ENVIRONMENT
if (! defined('ENVIRONMENT')) {
define('ENVIRONMENT', env('CI_ENVIRONMENT', 'production'));
}
// Grab our CodeIgniter
$app = Config\Services::codeigniter();
$app->initialize();
// Grab our Console
$console = new CodeIgniter\CLI\Console();
// Show basic information before we do anything else.
if (is_int($suppress = array_search('--no-header', $_SERVER['argv'], true))) {
unset($_SERVER['argv'][$suppress]); // @codeCoverageIgnore
$suppress = true;
}
$console->showHeader($suppress);
// fire off the command in the main framework. // LOAD THE FRAMEWORK BOOTSTRAP FILE
$exit = $console->run(); require $paths->systemDirectory . '/Boot.php';
exit(is_int($exit) ? $exit : EXIT_SUCCESS); exit(Boot::bootSpark($paths));

Loading…
Cancel
Save