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

Merged
stephane merged 1 commits from feat/notification-remise-info-moto into master 2 weeks ago
  1. 2
      .vscode/sftp.json
  2. 117
      README.md
  3. 11
      app/Config/Kint.php
  4. 14
      app/Controllers/OrderController.php
  5. 19
      app/Controllers/RemiseController.php
  6. 2
      app/Helpers/alerts_helper.php
  7. 2
      app/Models/Orders.php
  8. 16
      app/Models/Remise.php
  9. 4
      app/Views/autres_encaissements/index.php
  10. 4
      app/Views/demande/index.php
  11. 6
      app/Views/historique/index.php
  12. 20
      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. 280
      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",
"remotePath": "/home/motorbike/public_html/",
"password": "IVrMDogT3XiBcrY",
"uploadOnSave": false,
"uploadOnSave": true,
"useTempFile": 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
{
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

14
app/Controllers/OrderController.php

@ -218,7 +218,7 @@ class OrderController extends AdminController
// ========================================
// 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) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
@ -594,12 +594,15 @@ class OrderController extends AdminController
}
$product_lines = [];
$product_info_lines = [];
foreach ($product_data_results as $product) {
if (isset($product['sku'], $product['price'])) {
$sku = $product['sku'];
$price = $product['price'];
$product_lines[] = "{$sku}:{$price}";
$product_lines[] = $product['sku'] . ':' . $product['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);
@ -623,7 +626,8 @@ class OrderController extends AdminController
$message = "💰 Nouvelle demande de remise : {$montantFormatted} Ar<br>" .
"Commande : {$bill_no}<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) {
foreach ($allStores as $store) {

19
app/Controllers/RemiseController.php

@ -129,15 +129,28 @@ class RemiseController extends AdminController
];
if ($Remise->updateRemise($id_demande, $data)) {
$remise_product = $Remise->getProductByDemandeId($id_demande);
$Notification = new NotificationController();
$ordersModel = new Orders();
$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);
$bill_no = $order_info['bill_no'] ?? '';
$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
$Stores = new Stores();

2
app/Helpers/alerts_helper.php

@ -46,7 +46,7 @@ function checkDeadlineAlerts()
log_message('error', "Aucun email DAF trouvé");
$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));
return;

2
app/Models/Orders.php

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

16
app/Models/Remise.php

@ -88,9 +88,23 @@ class Remise extends Model
$row = $this->select('product')
->where('id_demande', $id_demande)
->first();
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)
{

4
app/Views/autres_encaissements/index.php

@ -284,14 +284,13 @@
</h3>
</div>
<div class="box-body">
<div class="table-responsive">
<table id="historyTable" class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>ID</th>
<th>Type</th>
<th>Montant</th>
<th>Mode</th> <!-- ✅ NOUVELLE COLONNE -->
<th>Mode</th>
<th>Commentaire</th>
<th>Créé par</th>
<th>Magasin</th>
@ -303,7 +302,6 @@
<!-- Les données seront chargées par DataTables -->
</tbody>
</table>
</div>
</div>
</div>
</div>

4
app/Views/demande/index.php

@ -38,8 +38,7 @@
<h3 class="box-title">Gérer les remises</h3>
</div>
<div class="table-responsive">
<table id="manageTable" class="table table-bordered table-striped ">
<table id="manageTable" class="table table-bordered table-striped">
<thead>
<tr>
<th>#</th>
@ -53,7 +52,6 @@
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>

6
app/Views/historique/index.php

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

20
app/Views/orders/index.php

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

2
app/Views/recouvrement/recouvrement.php

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

2
app/Views/reports/stockDetail.php

@ -145,7 +145,6 @@
</div>
</div>
<div class="table-responsive">
<table id="export1"
class="table table-hover table-striped table-bordered">
<thead class="table-primary">
@ -161,7 +160,6 @@
<!-- DataTables will populate this -->
</tbody>
</table>
</div>
</div>
</div>

2
app/Views/securite/index.php

@ -239,7 +239,6 @@
</h3>
</div>
<div class="box-body">
<div class="table-responsive">
<table id="historyTable" class="table table-bordered table-striped table-hover">
<thead>
<tr>
@ -260,7 +259,6 @@
<!-- Les données seront chargées par DataTables -->
</tbody>
</table>
</div>
</div>
</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') ?>">
<link rel="stylesheet"
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 -->
<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') ?>">
@ -99,8 +101,14 @@
<!-- 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-bs/js/dataTables.bootstrap.min.js') ?>"></script>
<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>

280
app/Views/templates/header_menu.php

@ -18,18 +18,17 @@
<!-- 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>
<span id="notificationCount" class="badge badge-warning navbar-badge"></span>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right"
style="width: 400px; padding: 5%; max-height: 500px; overflow: auto; margin-right: 5px;">
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0 10px;">
<span class="dropdown-header" id="notificationHeader" style="padding: 0;">0 Notifications</span>
<button id="markAllAsReadBtn" class="btn btn-sm btn-primary" style="font-size: 12px; padding: 4px 10px;">
<i class="fa fa-check"></i> Marquer tout comme lu
<span id="notificationCount" class="navbar-badge"></span>
<div class="dropdown-menu dropdown-menu-right notif-dropdown">
<div class="notif-panel-header">
<span class="notif-panel-title" id="notificationHeader">
<i class="fa fa-bell"></i> Notifications
</span>
<button id="markAllAsReadBtn" style="display:none;">
<i class="fa fa-check-double"></i> Tout lire
</button>
</div>
<div class="dropdown-divider"></div>
<div id="notificationList"></div>
<div class="dropdown-divider"></div>
</div>
</li>
@ -59,43 +58,178 @@
<!-- Styles -->
<style>
/* Notifications non lues */
.notification_item.unread {
font-weight: bold;
background-color: #f0f8ff;
.navbar-badge {
position: absolute;
top: -8px;
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 {
color: #007bff;
.notif-panel-title {
font-size: 14px;
font-weight: 700;
letter-spacing: 0.5px;
display: flex;
align-items: center;
gap: 8px;
}
#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;
transition: all 0.2s;
}
#markAllAsReadBtn:hover {
background-color: #0056b3;
background: rgba(255,255,255,0.35);
}
/* Style pour le dropdown utilisateur */
.dropdown-item {
transition: background-color 0.2s;
/* Liste scrollable */
#notificationList {
overflow-y: auto;
flex: 1;
padding: 8px;
background: #f8f9fa;
}
.dropdown-item:hover {
background-color: #f5f5f5;
text-decoration: none;
#notificationList:empty::after {
content: 'Aucune notification';
display: block;
text-align: center;
padding: 30px;
color: #aaa;
font-size: 13px;
}
.navbar-badge {
position: absolute;
top: -8px;
right: -8px;
padding: 3px 6px;
border-radius: 10px;
background: #f39c12;
color: white;
font-size: 10px;
/* Carte de notification */
.notif-card {
display: flex;
align-items: stretch;
margin-bottom: 6px;
border-radius: 8px;
border: 1px solid #e8e8e8;
background: #fff;
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>
@ -110,30 +244,73 @@ function fetchNotifications() {
let notificationHTML = '';
data.forEach(notif => {
if (notif.is_read == 0) {
notificationCount++;
}
if (notif.is_read == 0) notificationCount++;
const href = '/' + notif.link.replace(/^\/+/, '');
const notifClass = notif.is_read == 0 ? "notification_item unread" : "notification_item";
const iconHTML = notif.is_read == 0 ? '<i class="fa fa-exclamation-circle icon-unread mr-9"></i>' : '';
const isUnread = notif.is_read == 0;
// 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 += `
<a href="${href}"
class="${notifClass}"
data-id="${notif.id}"
style="font-size: 15px; display: flex; align-items: center; justify-content: space-between;">
<span style="display: flex; align-items: center; gap:10px;">
${iconHTML} ${notif.message}
</span>
<span class="float-right text-muted text-sm">${notif.created_at}</span>
<a href="${href}" class="notif-card ${isUnread ? 'notif-unread' : ''}" data-id="${notif.id}">
<div class="notif-accent" style="background:${accentColor};"></div>
<div class="notif-body">
<div class="notif-header">
<span class="notif-type-badge" style="background:${badgeColor};">
<i class="fa ${typeIcon}"></i> ${typeLabel}
</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>
<div class="dropdown-divider"></div>
`;
});
if (notificationHTML === '') {
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) {
$('#notificationCount').text(notificationCount).show();
@ -143,22 +320,17 @@ function fetchNotifications() {
$('#markAllAsReadBtn').hide();
}
const items = document.querySelectorAll('.notification_item');
items.forEach(item => {
document.querySelectorAll('.notif-card').forEach(item => {
item.addEventListener('click', () => {
const notifId = item.dataset.id;
fetch("<?= base_url('notifications/markAsRead') ?>/" + notifId, {
method: "POST",
headers: { "Content-Type": "application/json" }
})
.then(response => response.json())
.then(data => {
console.log("Notification marked as read:", data);
})
.catch(error => console.error("Error:", error));
.then(r => r.json())
.catch(e => console.error(e));
});
});
},
error: function(err) {
console.error('Error fetching notifications:', err);

17
composer.json

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

80
public/index.php

@ -1,17 +1,34 @@
<?php
// Check PHP version.
$minPhpVersion = '7.4'; // If you update this, don't forget to update `spark`.
use CodeIgniter\Boot;
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, '<')) {
$message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
$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)
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR);
@ -29,59 +46,14 @@ if (getcwd() . DIRECTORY_SEPARATOR !== FCPATH) {
* 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.
require FCPATH . '../app/Config/Paths.php';
// ^^^ Change this line if you move your application folder
$paths = new Config\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();
$paths = new Paths();
// Save Config Cache
// $factoriesCache->save('config');
// ^^^ Uncomment this line if you want to use Config Caching.
// LOAD THE FRAMEWORK BOOTSTRAP FILE
require $paths->systemDirectory . '/Boot.php';
// Exits the application, setting the exit code for CLI-based applications
// that might be watching.
exit(EXIT_SUCCESS);
exit(Boot::bootWeb($paths));

75
spark

@ -4,35 +4,46 @@
/**
* 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
* 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
* 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
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");
}
// 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, '<')) {
$message = sprintf(
'Your PHP version must be %s or higher to run CodeIgniter. Current version: %s',
$minPhpVersion,
PHP_VERSION
PHP_VERSION,
);
exit($message);
@ -42,12 +53,11 @@ if (version_compare(PHP_VERSION, $minPhpVersion, '<')) {
error_reporting(E_ALL);
ini_set('display_errors', '1');
/**
* @var bool
*
* @deprecated No longer in use. `CodeIgniter` has `$context` property.
/*
*---------------------------------------------------------------
* SET THE CURRENT DIRECTORY
*---------------------------------------------------------------
*/
define('SPARKED', true);
// Path to the front controller
define('FCPATH', __DIR__ . DIRECTORY_SEPARATOR . 'public' . DIRECTORY_SEPARATOR);
@ -64,41 +74,14 @@ chdir(FCPATH);
* 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.
require FCPATH . '../app/Config/Paths.php';
// ^^^ Change this line if you move your application folder
$paths = new Config\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);
$paths = new Paths();
// fire off the command in the main framework.
$exit = $console->run();
// LOAD THE FRAMEWORK BOOTSTRAP FILE
require $paths->systemDirectory . '/Boot.php';
exit(is_int($exit) ? $exit : EXIT_SUCCESS);
exit(Boot::bootSpark($paths));

Loading…
Cancel
Save