Browse Source

21122025

master
Sarobidy22 2 months ago
parent
commit
b1ef2963b6
  1. 73
      app/Config/Routes.php
  2. 571
      app/Controllers/AutresEncaissementsController.php
  3. 255
      app/Controllers/AvanceController.php
  4. 38
      app/Controllers/Dashboard.php
  5. 199
      app/Controllers/MecanicienController.php
  6. 930
      app/Controllers/OrderController.php
  7. 237
      app/Controllers/ReportController.php
  8. 17
      app/Controllers/SecuriteController.php
  9. 446
      app/Controllers/SortieCaisseController.php
  10. 302
      app/Models/AutresEncaissements.php
  11. 165
      app/Models/Avance.php
  12. 2
      app/Models/OrderItems.php
  13. 6
      app/Models/Orders.php
  14. 34
      app/Models/SortieCaisse.php
  15. 25
      app/Models/Users.php
  16. 978
      app/Views/autres_encaissements/index.php
  17. 1120
      app/Views/avances/avance.php
  18. 492
      app/Views/dashboard.php
  19. 28
      app/Views/groups/edit.php
  20. 571
      app/Views/mecanicien/index.php
  21. 217
      app/Views/orders/createbyid.php
  22. 319
      app/Views/orders/edit.php
  23. 25
      app/Views/orders/index.php
  24. 797
      app/Views/securite/index.php
  25. 9
      app/Views/templates/side_menubar.php

73
app/Config/Routes.php

@ -217,18 +217,56 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
/**
* route for the reports
*/
$routes->group('/reports', function ($routes) {
$routes->group('reports', function ($routes) {
$routes->get('/', [ReportController::class, 'index']);
$routes->post('/', [ReportController::class, 'index']);
$routes->get('detail/stock', [ReportController::class, 'stockDetail']);
$routes->get('detail/fetctData/(:num)', [ReportController::class, 'fetchProductSodled']);
$routes->get('detail/fetctDataStock/(:num)', [ReportController::class, 'fetchProductStock']);
$routes->get('detail/fetctDataStock2/(:num)', [ReportController::class, 'fetchProductStock2']);
$routes->get('detail/performance', [ReportController::class, 'performancedetail']);
// Corrections fetct → fetch
$routes->get('detail/fetchData/(:num)', [ReportController::class, 'fetchProductSold/$1']);
$routes->get('detail/fetchDataStock/(:num)', [ReportController::class, 'fetchProductStock/$1']);
$routes->get('detail/fetchDataStock2/(:num)', [ReportController::class, 'fetchProductStock2/$1']);
$routes->get('detail/performance', [ReportController::class, 'performanceDetail']);
$routes->get('detail/fetchPerformances', [ReportController::class, 'fetchPerformances']);
$routes->get('detail/fetchCaissierPerformances', [ReportController::class, 'fetchCaissierPerformances']);
// Sécurité (correction du chemin et du contrôleur)
$routes->get('detail/fetchSecuritePerformances', [ReportController::class, 'fetchSecuritePerformances']);
$routes->get('detail/getSecuriteValidationDetails/(:num)', [ReportController::class, 'getSecuriteValidationDetails']);
$routes->get('securite-history', [ReportController::class, 'securiteHistory'], ['filter' => 'auth']);
});
//autres encaissement
$routes->group('encaissements', ['filter' => 'auth'], function($routes) {
// Page principale
$routes->get('/', 'AutresEncaissementsController::index');
// Créer un encaissement
$routes->post('create', 'AutresEncaissementsController::create');
// Récupérer les données pour DataTables
$routes->get('fetch', 'AutresEncaissementsController::fetchEncaissements');
// Détails d'un encaissement
$routes->get('details/(:num)', 'AutresEncaissementsController::getDetails/$1');
// Récupérer un encaissement pour édition
$routes->get('getEncaissement/(:num)', 'AutresEncaissementsController::getEncaissement/$1');
// Modifier un encaissement
$routes->post('update/(:num)', 'AutresEncaissementsController::update/$1');
// Supprimer un encaissement
$routes->post('delete/(:num)', 'AutresEncaissementsController::delete/$1');
// Statistiques
$routes->get('statistics', 'AutresEncaissementsController::getStatistics');
});
/**
* route for the company
@ -307,41 +345,48 @@ $routes->group('/sortieCaisse', function ($routes) {
// avance
// ✅ DANS app/Config/Routes.php
$routes->group('/avances', function ($routes) {
$routes->group('avances', function ($routes) {
// Page principale
$routes->get('/', [AvanceController::class, 'index']);
// Routes pour récupérer les données (GET)
// ✅ CORRECTION : Utiliser POST pour fetchPendingValidation (cohérent avec DataTables AJAX)
$routes->get('fetchPendingValidation', [AvanceController::class, 'fetchPendingValidation']);
$routes->post('fetchPendingValidation', [AvanceController::class, 'fetchPendingValidation']);
// GET : Récupération des données
$routes->get('fetchAvanceData', [AvanceController::class, 'fetchAvanceData']);
$routes->get('fetchAvanceBecameOrder', [AvanceController::class, 'fetchAvanceBecameOrder']);
$routes->get('fetchExpiredAvance', [AvanceController::class, 'fetchExpiredAvance']);
// Routes pour une avance spécifique
// GET : Avance spécifique
$routes->get('fetchSingleAvance/(:num)', [AvanceController::class, 'fetchSingleAvance/$1']);
$routes->get('getInvoicePreview/(:num)', [AvanceController::class, 'getInvoicePreview/$1']);
$routes->get('getFullInvoiceForPrint/(:num)', [AvanceController::class, 'getFullInvoiceForPrint/$1']);
$routes->get('printInvoice/(:num)', [AvanceController::class, 'printInvoice/$1']);
// Routes POST pour modifications
// POST : Création / Modifications
$routes->post('createAvance', [AvanceController::class, 'createAvance']);
$routes->post('updateAvance', [AvanceController::class, 'updateAvance']);
$routes->post('deleteAvance', [AvanceController::class, 'removeAvance']);
$routes->post('notifyPrintInvoice', [AvanceController::class, 'notifyPrintInvoice']);
$routes->post('processExpiredAvances', [AvanceController::class, 'processExpiredAvances']);
// ✅ CORRECTION : Routes pour paiement et conversion
// Paiement
$routes->post('payAvance', [AvanceController::class, 'payAvance']);
// ✅ AJOUT : Routes GET ET POST pour la conversion manuelle
// Conversion manuelle
$routes->get('checkAndConvertCompleted', [AvanceController::class, 'checkAndConvertCompleted']);
$routes->post('checkAndConvertCompleted', [AvanceController::class, 'checkAndConvertCompleted']);
// Route pour forcer la conversion d'une avance spécifique
// Forcer conversion
$routes->get('forceConvertToOrder/(:num)', [AvanceController::class, 'forceConvertToOrder/$1']);
// Route CRON (optionnel)
// CRON (alertes deadline)
$routes->get('checkDeadlineAlerts', [AvanceController::class, 'checkDeadlineAlerts']);
// Validation d'avance
$routes->get('validateAvance', [AvanceController::class, 'validateAvance']);
$routes->post('validateAvance', [AvanceController::class, 'validateAvance']);
});
// historique

571
app/Controllers/AutresEncaissementsController.php

@ -0,0 +1,571 @@
<?php
namespace App\Controllers;
use App\Models\AutresEncaissements;
use App\Models\Stores;
use App\Models\Users;
class AutresEncaissementsController extends AdminController
{
public function __construct()
{
parent::__construct();
}
private $pageTitle = 'Autres Encaissements';
/**
* Page principale - Liste des encaissements
*/
public function index()
{
$this->verifyRole('viewEncaissement');
$session = session();
$user = $session->get('user');
$data['page_title'] = $this->pageTitle;
$data['user_role'] = $user['group_name'];
$data['user_permission'] = $this->permission;
// Récupérer les magasins pour le filtre
$storeModel = new Stores();
$data['stores'] = $storeModel->getActiveStore();
return $this->render_template('autres_encaissements/index', $data);
}
/**
* Ajouter un encaissement
*/
public function create()
{
if ($this->request->getMethod() !== 'post') {
return $this->response->setJSON([
'success' => false,
'messages' => 'Méthode non autorisée'
]);
}
try {
$this->verifyRole('createEncaissement');
$session = session();
$user = $session->get('user');
// Validation des données
$validation = \Config\Services::validation();
$validation->setRules([
'type_encaissement' => 'required',
'montant' => 'required|decimal',
'mode_paiement' => 'required|in_list[Espèces,MVola,Virement Bancaire]',
'commentaire' => 'permit_empty|string'
]);
if (!$validation->withRequest($this->request)->run()) {
return $this->response->setJSON([
'success' => false,
'messages' => $validation->getErrors()
]);
}
$encaissementModel = new AutresEncaissements();
$Notification = new NotificationController();
$typeEncaissement = $this->request->getPost('type_encaissement');
$autreType = $this->request->getPost('autre_type');
// Si "Autre" est sélectionné, utiliser le champ texte
$finalType = ($typeEncaissement === 'Autre' && !empty($autreType))
? $autreType
: $typeEncaissement;
$data = [
'type_encaissement' => $finalType,
'autre_type' => $autreType,
'montant' => $this->request->getPost('montant'),
'mode_paiement' => $this->request->getPost('mode_paiement'),
'commentaire' => $this->request->getPost('commentaire'),
'user_id' => $user['id'],
'store_id' => $user['store_id']
];
if ($encaissementId = $encaissementModel->insert($data)) {
// ✅ Récupérer tous les stores pour les notifications
$db = \Config\Database::connect();
$storesQuery = $db->table('stores')->select('id')->get();
$allStores = $storesQuery->getResultArray();
$montantFormate = number_format((float)$this->request->getPost('montant'), 0, ',', ' ');
$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 pour DAF
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'encaissements'
);
// 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");
return $this->response->setJSON([
'success' => true,
'messages' => 'Encaissement enregistré avec succès'
]);
}
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur lors de l\'enregistrement'
]);
} catch (\Exception $e) {
log_message('error', "Erreur création encaissement: " . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'messages' => 'Une erreur interne est survenue: ' . $e->getMessage()
]);
}
}
/**
* Modifier un encaissement
*/
public function update($id)
{
try {
$this->verifyRole('updateEncaissement');
$session = session();
$user = $session->get('user');
$encaissementModel = new AutresEncaissements();
$Notification = new NotificationController();
// Récupérer l'ancien encaissement pour comparaison
$oldEncaissement = $encaissementModel->find($id);
if (!$oldEncaissement) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Encaissement non trouvé'
]);
}
$typeEncaissement = $this->request->getPost('type_encaissement');
$autreType = $this->request->getPost('autre_type');
$finalType = ($typeEncaissement === 'Autre' && !empty($autreType))
? $autreType
: $typeEncaissement;
$data = [
'type_encaissement' => $finalType,
'autre_type' => $autreType,
'montant' => $this->request->getPost('montant'),
'mode_paiement' => $this->request->getPost('mode_paiement'),
'commentaire' => $this->request->getPost('commentaire')
];
if ($encaissementModel->update($id, $data)) {
// ✅ Envoyer notification de modification à tous les stores
$db = \Config\Database::connect();
$storesQuery = $db->table('stores')->select('id')->get();
$allStores = $storesQuery->getResultArray();
$montantFormate = number_format((float)$this->request->getPost('montant'), 0, ',', ' ');
$ancienMontant = number_format((float)$oldEncaissement['montant'], 0, ',', ' ');
$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'
);
}
log_message('info', "✅ Encaissement {$id} modifié - Notifications envoyées");
return $this->response->setJSON([
'success' => true,
'messages' => 'Encaissement modifié avec succès'
]);
}
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur lors de la modification'
]);
} catch (\Exception $e) {
log_message('error', 'Erreur dans update(): ' . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur serveur: ' . $e->getMessage()
]);
}
}
/**
* Supprimer un encaissement
*/
public function delete($id)
{
try {
$this->verifyRole('deleteEncaissement');
$session = session();
$user = $session->get('user');
$encaissementModel = new AutresEncaissements();
$Notification = new NotificationController();
// Récupérer l'encaissement avant suppression pour la notification
$encaissement = $encaissementModel->find($id);
if (!$encaissement) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Encaissement non trouvé'
]);
}
if ($encaissementModel->delete($id)) {
// ✅ Envoyer notification de suppression à tous les stores
$db = \Config\Database::connect();
$storesQuery = $db->table('stores')->select('id')->get();
$allStores = $storesQuery->getResultArray();
$montantFormate = number_format((float)$encaissement['montant'], 0, ',', ' ');
$type = $encaissement['type_encaissement'];
$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'
);
}
log_message('info', "✅ Encaissement {$id} supprimé - Notifications envoyées");
return $this->response->setJSON([
'success' => true,
'messages' => 'Encaissement supprimé avec succès'
]);
}
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur lors de la suppression'
]);
} catch (\Exception $e) {
log_message('error', 'Erreur dans delete(): ' . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur serveur: ' . $e->getMessage()
]);
}
}
/**
* Récupérer les données pour DataTables
*/
public function fetchEncaissements()
{
try {
$this->verifyRole('viewEncaissement');
$session = session();
$user = $session->get('user');
$encaissementModel = new AutresEncaissements();
// Récupérer les filtres
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$storeId = $this->request->getGet('store_id');
// Si l'utilisateur n'est pas admin, limiter à son magasin
if (!in_array($user['group_name'], ['SuperAdmin', 'DAF', 'Direction'])) {
$storeId = $user['store_id'];
}
$encaissements = $encaissementModel->getEncaissementsWithDetails($storeId);
// Filtrer par dates
if ($startDate || $endDate) {
$encaissements = array_filter($encaissements, function($item) use ($startDate, $endDate) {
$itemDate = date('Y-m-d', strtotime($item['created_at']));
if ($startDate && $itemDate < $startDate) {
return false;
}
if ($endDate && $itemDate > $endDate) {
return false;
}
return true;
});
}
$result = [];
foreach ($encaissements as $item) {
// Construire les boutons avec permissions
$buttons = '';
if (in_array('viewEncaissement', $this->permission)) {
$buttons .= '<button class="btn btn-info btn-sm" onclick="viewDetails(' . $item['id'] . ')">
<i class="fa fa-eye"></i> Détails
</button> ';
}
if (in_array('updateEncaissement', $this->permission)) {
$buttons .= '<button class="btn btn-warning btn-sm" onclick="editEncaissement(' . $item['id'] . ')">
<i class="fa fa-edit"></i>
</button> ';
}
if (in_array('deleteEncaissement', $this->permission)) {
$buttons .= '<button class="btn btn-danger btn-sm" onclick="deleteEncaissement(' . $item['id'] . ')">
<i class="fa fa-trash"></i>
</button>';
}
// ✅ Badge coloré pour le mode de paiement
$badgeClass = match($item['mode_paiement']) {
'Espèces' => 'success',
'MVola' => 'warning',
'Virement Bancaire' => 'info',
default => 'default'
};
$modePaiementBadge = '<span class="label label-' . $badgeClass . '">' . $item['mode_paiement'] . '</span>';
$result[] = [
$item['id'],
$item['type_encaissement'],
number_format($item['montant'], 0, '.', ' ') . ' Ar',
$modePaiementBadge, // ✅ NOUVELLE COLONNE
$item['commentaire'] ?: 'Aucun',
$item['user_name'] ?: 'N/A',
$item['store_name'] ?: 'N/A',
date('d/m/Y H:i', strtotime($item['created_at'])),
$buttons
];
}
return $this->response->setJSON(['data' => $result]);
} catch (\Exception $e) {
log_message('error', 'Erreur dans fetchEncaissements(): ' . $e->getMessage());
return $this->response->setJSON(['data' => []]);
}
}
/**
* Récupérer les détails d'un encaissement
*/
public function getDetails($id)
{
try {
$this->verifyRole('viewEncaissement');
$encaissementModel = new AutresEncaissements();
$encaissement = $encaissementModel->getEncaissementById($id);
if ($encaissement) {
return $this->response->setJSON([
'success' => true,
'data' => $encaissement
]);
}
return $this->response->setJSON([
'success' => false,
'message' => 'Encaissement non trouvé'
]);
} catch (\Exception $e) {
log_message('error', 'Erreur dans getDetails(): ' . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'message' => 'Erreur serveur'
]);
}
}
/**
* Récupérer un encaissement pour édition
*/
public function getEncaissement($id)
{
try {
$this->verifyRole('updateEncaissement');
$encaissementModel = new AutresEncaissements();
$encaissement = $encaissementModel->find($id);
if ($encaissement) {
return $this->response->setJSON([
'success' => true,
'data' => $encaissement
]);
}
return $this->response->setJSON([
'success' => false,
'message' => 'Encaissement non trouvé'
]);
} catch (\Exception $e) {
log_message('error', 'Erreur dans getEncaissement(): ' . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'message' => 'Erreur serveur'
]);
}
}
/**
* Récupérer les statistiques
*/
public function getStatistics()
{
try {
$this->verifyRole('viewEncaissement');
$session = session();
$user = $session->get('user');
$encaissementModel = new AutresEncaissements();
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$storeId = $this->request->getGet('store_id');
// ✅ LOG pour débogage
log_message('info', "📊 getStatistics appelé - Dates: {$startDate} à {$endDate}, Store: {$storeId}");
// Si l'utilisateur n'est pas admin, limiter à son magasin
if (!in_array($user['group_name'], ['SuperAdmin', 'DAF', 'Direction'])) {
$storeId = $user['store_id'];
}
// ✅ IMPORTANT : Convertir $storeId en NULL si vide
$storeIdFilter = (!empty($storeId) && $storeId !== '') ? (int)$storeId : null;
// Récupérer les totaux
$totalMontant = $encaissementModel->getTotalEncaissements($storeIdFilter, $startDate, $endDate);
$totalCount = $encaissementModel->getTotalCount($storeIdFilter, $startDate, $endDate);
$todayCount = $encaissementModel->getTodayCount($storeIdFilter);
// Récupérer les totaux par mode de paiement
$totauxParMode = $encaissementModel->getTotalEncaissementsByMode($storeIdFilter, $startDate, $endDate);
// Statistiques par type
$statsByType = $encaissementModel->getStatsByType($storeIdFilter, $startDate, $endDate);
// ✅ LOG pour vérifier les valeurs
log_message('info', "✅ Résultats - Total: {$totalMontant}, Count: {$totalCount}, Today: {$todayCount}");
return $this->response->setJSON([
'success' => true,
'total_montant' => number_format($totalMontant, 0, ',', ' ') . ' Ar',
'total_count' => $totalCount,
'today_count' => $todayCount,
'total_espece' => number_format($totauxParMode['total_espece'], 0, ',', ' ') . ' Ar',
'total_mvola' => number_format($totauxParMode['total_mvola'], 0, ',', ' ') . ' Ar',
'total_virement' => number_format($totauxParMode['total_virement'], 0, ',', ' ') . ' Ar',
'stats_by_type' => $statsByType,
// ✅ Ajouter ces infos pour débogage
'debug' => [
'store_id' => $storeIdFilter,
'start_date' => $startDate,
'end_date' => $endDate
]
]);
} catch (\Exception $e) {
log_message('error', '❌ Erreur dans getStatistics(): ' . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'total_montant' => '0 Ar',
'total_count' => 0,
'today_count' => 0,
'total_espece' => '0 Ar',
'total_mvola' => '0 Ar',
'total_virement' => '0 Ar',
'stats_by_type' => [],
'error' => $e->getMessage()
]);
}
}
}

255
app/Controllers/AvanceController.php

@ -43,10 +43,11 @@ class AvanceController extends AdminController
{
return in_array($user['group_name'], ['Caissière']);
}
/**
* Modifier la méthode buildActionButtons pour ajouter l'icône œil pour la Direction
*/
private function buildActionButtons($value, $isAdmin, $isOwner, $isCaissier = false)
private function buildActionButtons($value, $isAdmin, $isOwner, $isCaissier = false, $isCommercial = false)
{
$session = session();
$users = $session->get('user');
@ -54,28 +55,58 @@ private function buildActionButtons($value, $isAdmin, $isOwner, $isCaissier = fa
$buttons = '';
// ✅ Bouton Voir pour Caissière (toujours visible)
if ($isCaissier && in_array('viewAvance', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-info" onclick="viewFunc(' . $value['avance_id'] . ')" title="Voir et imprimer la facture" data-user-role="caissiere">'
. '<i class="fa fa-eye"></i></button> ';
// ✅ Badge de statut validation (pour le commercial uniquement)
if ($isCommercial && $value['validated'] == 0) {
$buttons .= '<span class="badge badge-warning" style="margin-right: 5px;">En attente validation</span>';
}
// ✅ Bouton Voir pour Direction (toujours visible, sans impression)
if ($isDirection && in_array('viewAvance', $this->permission) && !$isCaissier) {
$buttons .= '<button type="button" class="btn btn-info" onclick="viewFunc(' . $value['avance_id'] . ')" title="Voir la facture" data-user-role="direction">'
// ✅ BOUTON VOIR - Toujours visible pour tout le monde
if (in_array('viewAvance', $this->permission)) {
$title = $isCaissier ? 'Voir et imprimer la facture' : 'Voir la facture';
$buttons .= '<button type="button" class="btn btn-info" onclick="viewFunc(' . $value['avance_id'] . ')" title="' . $title . '">'
. '<i class="fa fa-eye"></i></button> ';
}
// ✅ MODIFIÉ : Bouton Modifier - Le caissier peut maintenant modifier toutes les avances
if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['avance_id'] . ')" title="Modifier">'
. '<i class="fa fa-pencil"></i></button> ';
// ✅ BOUTON MODIFIER
// Commercial : peut modifier uniquement ses avances non validées
// Caissière : peut modifier toutes les avances validées de son store
// Admin : peut tout modifier
if (in_array('updateAvance', $this->permission)) {
$canEdit = false;
if ($isAdmin) {
$canEdit = true; // Admin peut tout modifier
} elseif ($isCommercial && $isOwner && $value['validated'] == 0) {
$canEdit = true; // Commercial modifie ses avances non validées
} elseif ($isCaissier && $value['validated'] == 1) {
$canEdit = true; // Caissière modifie les avances validées
}
if ($canEdit) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['avance_id'] . ')" title="Modifier">'
. '<i class="fa fa-pencil"></i></button> ';
}
}
// ✅ MODIFIÉ : Bouton Supprimer - Le caissier peut maintenant supprimer toutes les avances
if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) {
$buttons .= '<button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['avance_id'] . ',' . $value['product_id'] . ')" title="Supprimer">'
. '<i class="fa fa-trash"></i></button> ';
// ✅ BOUTON SUPPRIMER
// Commercial : peut supprimer uniquement ses avances non validées
// Caissière : peut supprimer toutes les avances (validées ou non) de son store
// Admin : peut tout supprimer
if (in_array('deleteAvance', $this->permission)) {
$canDelete = false;
if ($isAdmin) {
$canDelete = true; // Admin peut tout supprimer
} elseif ($isCommercial && $isOwner && $value['validated'] == 0) {
$canDelete = true; // Commercial supprime ses avances non validées
} elseif ($isCaissier) {
$canDelete = true; // Caissière peut tout supprimer dans son store
}
if ($canDelete) {
$buttons .= '<button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['avance_id'] . ',' . ($value['product_id'] ?? 0) . ')" title="Supprimer">'
. '<i class="fa fa-trash"></i></button> ';
}
}
return $buttons;
@ -85,10 +116,14 @@ private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCai
{
$date_time = date('d-m-Y h:i a', strtotime($value['avance_date']));
// ✅ Afficher product_name si disponible, sinon récupérer depuis la BDD
$productName = !empty($value['product_name'])
? $value['product_name']
: $product->getProductNameById($value['product_id']);
// ✅ Gestion sécurisée du nom du produit
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);
}
if ($isAdmin) {
return [
@ -134,8 +169,14 @@ private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData')
foreach ($data as $key => $value) {
$isOwner = $users['id'] === $value['user_id'];
// ✅ MODIFIÉ : Passer $isCaissier aux boutons d'action
$buttons = $this->buildActionButtons($value, $isAdmin, $isOwner, $isCaissier);
// ✅ Passer tous les indicateurs de rôle
$buttons = $this->buildActionButtons(
$value,
$isAdmin,
$isOwner,
$isCaissier,
$isCommerciale
);
$row = $this->buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons);
@ -147,19 +188,169 @@ private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData')
return $this->response->setJSON($result);
}
public function fetchAvanceData()
{
return $this->fetchAvanceDataGeneric('getIncompleteAvances');
}
public function fetchAvanceData()
{
return $this->fetchAvanceDataGeneric('getIncompleteAvances');
}
public function fetchAvanceBecameOrder()
{
return $this->fetchAvanceDataGeneric('getCompletedAvances');
}
public function fetchAvanceBecameOrder()
{
return $this->fetchAvanceDataGeneric('getCompletedAvances');
}
public function fetchExpiredAvance()
{
return $this->fetchAvanceDataGeneric('getAllAvanceData2');
}
public function fetchPendingValidation()
{
$this->verifyRole('viewAvance');
$session = session();
$users = $session->get('user');
log_message('info', '🔍 fetchPendingValidation appelée');
log_message('info', "🔍 Rôle utilisateur : {$users['group_name']}");
public function fetchExpiredAvance()
$isCaissier = $this->isCaissier($users);
if (!$isCaissier) {
log_message('warning', "⚠️ Utilisateur {$users['id']} n'est pas caissière");
return $this->response->setJSON(['data' => []]);
}
log_message('info', "✅ Utilisateur {$users['id']} est caissière du store {$users['store_id']}");
$Avance = new Avance();
$Products = new Products();
$pendingAvances = $Avance->getPendingValidationAvances($users['store_id']);
log_message('info', "🔍 Nombre d'avances en attente trouvées : " . count($pendingAvances));
$result = ['data' => []];
foreach ($pendingAvances as $avance) {
// Gestion sécurisée du nom du produit
if ($avance['type_avance'] === 'mere') {
$productName = $avance['product_name'] ?? 'Produit sur mer';
} else {
$productName = $Products->getProductNameById($avance['product_id'] ?? 0);
}
$date_time = date('d-m-Y h:i a', strtotime($avance['avance_date']));
// ✅ MODIFICATION : Boutons sans le bouton "Voir" (œil)
$actions = '<button type="button" class="btn btn-success" onclick="validateAvanceFunc(' . $avance['avance_id'] . ')" title="Valider cette avance">' .
'<i class="fa fa-check"></i> Valider</button> ';
// ✅ PAS de bouton "Voir" pour les avances non validées
// $actions .= '<button type="button" class="btn btn-info" onclick="viewFunc(' . $avance['avance_id'] . ')" title="Voir">' .
// '<i class="fa fa-eye"></i></button> ';
$actions .= '<button type="button" class="btn btn-danger" onclick="removeFunc(' . $avance['avance_id'] . ',' . ($avance['product_id'] ?? 0) . ')" title="Refuser/Supprimer">' .
'<i class="fa fa-times"></i> Refuser</button>';
$result['data'][] = [
$avance['avance_id'],
$avance['customer_name'],
$avance['customer_phone'],
$productName,
number_format((int)$avance['gross_amount'], 0, ',', ' '),
number_format((int)$avance['avance_amount'], 0, ',', ' '),
$date_time,
$actions // ✅ Sans le bouton "Voir"
];
}
log_message('info', "✅ Retour de " . count($result['data']) . " avances en attente");
return $this->response->setJSON($result);
}
/**
* ✅ MÉTHODE VALIDATION (déjà correcte, on la garde)
*/
public function validateAvance()
{
return $this->fetchAvanceDataGeneric('getAllAvanceData2');
$this->verifyRole('updateAvance');
$session = session();
$users = $session->get('user');
if (!$this->isCaissier($users)) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Seule la caissière peut valider les avances'
]);
}
$avance_id = $this->request->getPost('avance_id');
if (!$avance_id) {
return $this->response->setJSON([
'success' => false,
'messages' => 'ID avance manquant'
]);
}
$Avance = new Avance();
$avance = $Avance->find($avance_id);
if (!$avance) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Avance introuvable'
]);
}
if ($avance['store_id'] !== $users['store_id']) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Vous ne pouvez valider que les avances de votre magasin'
]);
}
if ($Avance->validateAvance($avance_id, $users['id'])) {
$Notification = new NotificationController();
$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 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'
);
return $this->response->setJSON([
'success' => true,
'messages' => 'Avance validée avec succès ! Le montant a été ajouté à la caisse.'
]);
} else {
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur lors de la validation de l\'avance'
]);
}
}

38
app/Controllers/Dashboard.php

@ -10,6 +10,7 @@ use App\Models\Stores;
use App\Models\Users;
use App\Models\Recouvrement;
use App\Models\SortieCaisse;
use App\Models\AutresEncaissements; // ✅ AJOUT
class Dashboard extends AdminController
{
@ -38,6 +39,21 @@ class Dashboard extends AdminController
$sortieCaisse = new SortieCaisse();
$total_sortie_caisse = $sortieCaisse->getTotalSortieCaisse();
// ✅ AJOUT : Récupérer les autres encaissements PAR MODE DE PAIEMENT
$autresEncaissementsModel = new AutresEncaissements();
// Déterminer si l'utilisateur est admin pour filtrer par store
$isAdmin = in_array($user_id['group_name'], ['DAF', 'Direction', 'SuperAdmin']);
$storeIdFilter = $isAdmin ? null : $user_id['store_id'];
// Récupérer les totaux des autres encaissements par mode de paiement
$totauxAutresEncaissements = $autresEncaissementsModel->getTotalEncaissementsByMode($storeIdFilter);
$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;
// === EXTRACTION DES SORTIES PAR MODE DE PAIEMENT ===
$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;
@ -69,11 +85,11 @@ class Dashboard extends AdminController
$es_avances = isset($paymentDataAvance->total_espece) ? (float) $paymentDataAvance->total_espece : 0;
$vb_avances = isset($paymentDataAvance->total_virement_bancaire) ? (float) $paymentDataAvance->total_virement_bancaire : 0;
// === COMBINAISON ORDERS + AVANCES (BRUT = CE QUE LA CAISSIÈRE A ENCAISSÉ) ===
$total_mvola_brut = $mv1_orders + $mv2_orders + $mv_avances;
$total_espece_brut = $es1_orders + $es2_orders + $es_avances;
$total_vb_brut = $vb1_orders + $vb2_orders + $vb_avances;
$total_brut = $total_orders + $total_avances;
// === COMBINAISON ORDERS + AVANCES + AUTRES ENCAISSEMENTS (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;
// === AJUSTEMENTS AVEC RECOUVREMENTS ET SORTIES ===
$total_mvola_final = $total_mvola_brut - $me - $mb + $bm - $total_sortie_mvola;
@ -102,6 +118,12 @@ class Dashboard extends AdminController
'total_orders_only' => $total_orders,
'total_avances' => $total_avances,
// ✅ AJOUT : Détails des autres encaissements
'total_autres_encaissements' => $total_autres_encaissements,
'total_autres_enc_espece' => $total_autres_enc_espece,
'total_autres_enc_mvola' => $total_autres_enc_mvola,
'total_autres_enc_virement' => $total_autres_enc_virement,
// ✅ Détails des sorties
'total_sorties' => $total_sortie_global,
'total_sortie_espece' => $total_sortie_espece,
@ -125,7 +147,7 @@ class Dashboard extends AdminController
'total_espece_orders' => $es1_orders + $es2_orders,
'total_vb_orders' => $vb1_orders + $vb2_orders,
// ✅ Montants bruts
// ✅ Montants bruts (AVEC autres encaissements)
'total_brut' => $total_brut,
'total_mvola_brut' => $total_mvola_brut,
'total_espece_brut' => $total_espece_brut,
@ -136,8 +158,6 @@ class Dashboard extends AdminController
$data['total_products'] = $productModel->countProductsByUserStore();
// === ✅ Récupérer le nom du store pour l'affichage ===
$isAdmin = in_array($user_id['group_name'], ['DAF', 'Direction','SuperAdmin']);
if (!$isAdmin && !empty($user_id['store_id']) && $user_id['store_id'] != 0) {
$store = $storeModel->getStoresData($user_id['store_id']);
$data['store_name'] = $store['name'] ?? 'Votre magasin';
@ -236,8 +256,6 @@ class Dashboard extends AdminController
// ✅ AJOUT POUR CAISSIER : Passer les données de performance
if ($user_id['group_name'] == "Caissière") {
$data['isCaissier'] = true;
// Pas besoin de données supplémentaires car fetchCaissierPerformances
// récupère déjà les données via AJAX
}
if ($user_id['group_name'] == "MECANICIEN") {

199
app/Controllers/MecanicienController.php

@ -5,6 +5,7 @@ namespace App\Controllers;
use App\Models\Mecanicien;
use App\Models\Products;
use App\Models\Users;
use App\Models\stores;
class MecanicienController extends AdminController
{
@ -21,133 +22,91 @@ class MecanicienController extends AdminController
$session = session();
$user_id = $session->get('user');
// if($user_id CONTAINS MECANICIEN)
// is mecanicien true
$data['id'] = $user_id['id'];
$data['user_role'] = $user_id['group_name'];
$Products = new Products();
$Users = new Users();
$Stores = new Stores();
$data['moto'] = $Products->getActiveProductData();
$data['users'] = $Users->getUsers();
$data['stores'] = $Stores->getActiveStore();
return $this->render_template('mecanicien/index', $data);
}
public function fetchmecanicienSingle($id)
{
// die(var_dump($id));
if ($id) {
$Mecanicien = new Mecanicien();
$data = $Mecanicien->getReparationSingle($id);
echo json_encode($data);
}
}
/**
* ✅ MODIFIÉ : Utilise mecanic_id au lieu de store_id
*/
public function fetchMecanicien()
{
$Mecanicien = new Mecanicien();
$session = session();
$user_id = $session->get('user');
// ✅ RÉCUPÉRER LES PARAMÈTRES DE FILTRE
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$mecanicId = $this->request->getGet('mecanic_id'); // ✅ CHANGÉ: store_id → mecanic_id
log_message('debug', '=== FILTRES RÉPARATIONS ===');
log_message('debug', 'startDate: ' . ($startDate ?? 'vide'));
log_message('debug', 'endDate: ' . ($endDate ?? 'vide'));
log_message('debug', 'mecanic_id: ' . ($mecanicId ?? 'vide'));
$data['id'] = $user_id['id'];
$reparation = $Mecanicien->getReparation($data['id']);
$result = ['data' => []];
function strReparation($repastatus)
{
$reparation = '';
if ($repastatus == 1) {
$reparation = 'En cours de réparation';
} else if ($repastatus == 2) {
$reparation = 'Réparer';
} else {
$reparation = 'Non réparer';
}
// ✅ UTILISER LA MÉTHODE AVEC FILTRES (mecanicId au lieu de storeId)
$reparation = $Mecanicien->getReparationWithFilters($data['id'], $startDate, $endDate, $mecanicId);
return $reparation;
}
$result = ['data' => []];
// Iterate through the data
foreach ($reparation as $key => $repa) {
// Action buttons
$buttons = '';
// dd($repa['reparationsID']);
// Check permissions for updating the store
if (in_array('updateMecanicien', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $repa['reparationsID'] . ')" data-toggle="modal" data-target="#editModal"><i class="fa fa-pencil"></i></button>';
}
// Check permissions for deleting the store
if (in_array('deleteMecanicien', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $repa['reparationsID'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
$buttons .= ' <button type="button" class="btn btn-default" onclick="removeFunc(' . $repa['reparationsID'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
}
$image = '<img src="' . base_url('assets/images/product_image/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$image = '<img src="' . base_url('assets/images/products/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$produit = $repa['name'] . ' (' . $repa['sku'] . ')';
// Status display
$status = strReparation($repa['reparation_statut']);
$username = $repa['username'];
$observation = $repa['reparation_observation'];
$date_debut = date("d/m/Y", strtotime($repa['reparation_debut']));
$date_fin = date("d/m/Y", strtotime($repa['reparation_fin']));
// Add the row data
$result['data'][$key] = [
$image,
$produit,
$username,
$status,
$observation,
$date_debut,
$date_fin,
$buttons
];
}
// Return data in JSON format
return $this->response->setJSON($result);
}
public function fetchMecanicien_1(int $id)
{
$Mecanicien = new Mecanicien();
$reparation = $Mecanicien->getReparation($id);
$result = ['data' => []];
// die(var_dump($reparation));
// Iterate through the data
foreach ($reparation as $key => $repa) {
// Action buttons
$buttons = '';
// dd($repa['reparationsID']);
// Check permissions for updating the store
if (in_array('updateMecanicien', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $repa['reparationsID'] . ')" data-toggle="modal" data-target="#editModal"><i class="fa fa-pencil"></i></button>';
}
// Check permissions for deleting the store
if (in_array('deleteMecanicien', $this->permission)) {
$buttons .= ' <button type="button" class="btn btn-danger" onclick="removeFunc(' . $repa['reparationsID'] . ')" data-toggle="modal" data-target="#removeModal"><i class="fa fa-trash"></i></button>';
$status = '';
if ($repa['reparation_statut'] == 1) {
$status = '<span class="label label-warning">En cours</span>';
} elseif ($repa['reparation_statut'] == 2) {
$status = '<span class="label label-success">Réparer</span>';
} else {
$status = '<span class="label label-danger">Non réparer</span>';
}
$image = '<img src="' . base_url('assets/images/product_image/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$produit = $repa['name'];
// Status display
$status = $repa['reparation_statut'];
$username = $repa['username'];
$username = $repa['firstname'] . ' ' . $repa['lastname'];
$observation = $repa['reparation_observation'];
$date_debut = date("d/m/Y", strtotime($repa['reparation_debut']));
$date_fin = date("d/m/Y", strtotime($repa['reparation_fin']));
// Add the row data
$storeName = $repa['store_name'] ?? 'N/A';
$result['data'][$key] = [
$image,
$produit,
$username,
$status,
$username, // Index 2 - Mécanicien
$storeName, // Index 3 - Magasin
$status, // Index 4 - Statut (utilisé pour les stats)
$observation,
$date_debut,
$date_fin,
@ -155,7 +114,8 @@ class MecanicienController extends AdminController
];
}
// Return data in JSON format
log_message('debug', '📊 Nombre de réparations trouvées: ' . count($result['data']));
return $this->response->setJSON($result);
}
@ -184,9 +144,11 @@ class MecanicienController extends AdminController
'date_fin' => $this->request->getPost('date_fin'),
];
// Run validation
if ($validation->run($validationData)) {
// // Prepare data
$productModel = new \App\Models\Products();
$product = $productModel->find($this->request->getPost('motos'));
$storeId = $product['store_id'] ?? null;
$data = [
'user_id' => $this->request->getPost('mecano'),
'produit_id' => $this->request->getPost('motos'),
@ -194,9 +156,9 @@ class MecanicienController extends AdminController
'reparation_statut' => $this->request->getPost('statut'),
'reparation_debut' => $this->request->getPost('date_debut'),
'reparation_fin' => $this->request->getPost('date_fin'),
'store_id' => $storeId,
];
// Load the model and create the store
$Mecanicien = new Mecanicien();
if ($Mecanicien->createRepation($data)) {
$response['success'] = true;
@ -206,7 +168,6 @@ class MecanicienController extends AdminController
$response['messages'] = 'Erreur de base de données';
}
} else {
// Validation failed, return error messages
$response['success'] = false;
$response['messages'] = $validation->getErrors();
}
@ -229,7 +190,7 @@ class MecanicienController extends AdminController
$response['messages'] = "Supprimé avec succès";
} else {
$response['success'] = false;
$response['messages'] = "Erreur dans la base de données lors de la suppression des informations sur la marque";
$response['messages'] = "Erreur dans la base de données lors de la suppression";
}
} else {
$response['success'] = false;
@ -239,42 +200,38 @@ class MecanicienController extends AdminController
return $this->response->setJSON($response);
}
public function update(int $id)
{
$this->verifyRole('updateMecanicien');
$response = [];
if ($id) {
// Set validation rules
$validation = \Config\Services::validation();
$validation->setRules([
'motos_edit' => 'required',
'motos' => 'required',
'mecano' => 'required',
'statut_edit' => 'required',
'observation_edit' => 'required',
'date_debut_edit' => 'required',
'date_fin_edit' => 'required',
'statut' => 'required',
'observation' => 'required',
'date_debut' => 'required',
'date_fin' => 'required',
]);
$statutList = [
"1" => "En cours de réparation",
"2" => "Reparé",
"3" => "Non reparé"
];
$statut = $this->request->getPost('statut');
$validationData = [
'motos_edit' => $this->request->getPost('motos'),
'motos' => $this->request->getPost('motos'),
'mecano' => $this->request->getPost('mecano'),
'statut_edit' => $statutList[$statut],
'observation_edit' => $this->request->getPost('observation'),
'date_debut_edit' => $this->request->getPost('date_debut'),
'date_fin_edit' => $this->request->getPost('date_fin'),
'statut' => $this->request->getPost('statut'),
'observation' => $this->request->getPost('observation'),
'date_debut' => $this->request->getPost('date_debut'),
'date_fin' => $this->request->getPost('date_fin'),
];
$Mecanicien = new Mecanicien();
if ($validation->run($validationData)) {
$productModel = new \App\Models\Products();
$product = $productModel->find($this->request->getPost('motos'));
$storeId = $product['store_id'] ?? null;
$data = [
'user_id' => $this->request->getPost('mecano'),
@ -283,9 +240,8 @@ class MecanicienController extends AdminController
'reparation_observation' => $this->request->getPost('observation'),
'reparation_debut' => $this->request->getPost('date_debut'),
'reparation_fin' => $this->request->getPost('date_fin'),
'store_id' => $storeId,
];
// echo '<pre>';
// die(var_dump($data));
if ($Mecanicien->updateReparation($data, $id)) {
$response['success'] = true;
@ -295,7 +251,6 @@ class MecanicienController extends AdminController
$response['messages'] = 'Erreur dans la base de données';
}
} else {
// Validation failed, return error messages
$response['success'] = false;
$response['messages'] = $validation->getErrors();
}
@ -307,6 +262,9 @@ class MecanicienController extends AdminController
return $this->response->setJSON($response);
}
/**
* ✅ MODIFIÉ : Utilise mecanic_id au lieu de pvente
*/
public function fetchMecanicienPerformances()
{
$Mecanicien = new Mecanicien();
@ -316,58 +274,63 @@ class MecanicienController extends AdminController
// ✅ RÉCUPÉRER LES PARAMÈTRES DE FILTRE
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$pvente = $this->request->getGet('pvente');
$mecanicId = $this->request->getGet('mecanic_id'); // ✅ CHANGÉ: pvente → mecanic_id
// Log pour débogage
log_message('debug', 'Filtres Mécanicien reçus - startDate: ' . $startDate . ', endDate: ' . $endDate . ', pvente: ' . $pvente);
log_message('debug', '=== FILTRES PERFORMANCES MÉCANICIEN ===');
log_message('debug', 'startDate: ' . ($startDate ?? 'vide'));
log_message('debug', 'endDate: ' . ($endDate ?? 'vide'));
log_message('debug', 'mecanic_id: ' . ($mecanicId ?? 'vide'));
$data['id'] = $users['id'];
// ✅ PASSER LES FILTRES AU MODÈLE
$reparation = $Mecanicien->getReparationWithFilters($data['id'], $startDate, $endDate, $pvente);
// ✅ PASSER LES FILTRES AU MODÈLE (mecanicId au lieu de pvente)
$reparation = $Mecanicien->getReparationWithFilters($data['id'], $startDate, $endDate, $mecanicId);
$result = ['data' => []];
if($users['group_name'] == "SuperAdmin" || $users['group_name'] == "Direction" || $users['group_name'] == "DAF"){
foreach ($reparation as $key => $repa) {
$image = '<img src="' . base_url('assets/images/product_image/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$produit = esc($repa['name']);
$first_name = esc($repa['firstname']);
$last_name = esc($repa['lastname']);
$image = '<img src="' . base_url('assets/images/products/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$produit = esc($repa['name']);
$first_name = esc($repa['firstname']);
$last_name = esc($repa['lastname']);
$user_name = $first_name . ' ' . $last_name;
$date_debut = date("d/m/Y", strtotime($repa['reparation_debut']));
$date_fin = date("d/m/Y", strtotime($repa['reparation_fin']));
$storeName = $repa['store_name'] ?? 'N/A';
// Add the row data
$result['data'][$key] = [
$user_name,
$image,
$produit,
$repa['sku'],
$storeName,
$date_debut,
$date_fin,
];
}
} else {
foreach ($reparation as $key => $repa) {
$image = '<img src="' . base_url('assets/images/product_image/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$image = '<img src="' . base_url('assets/images/products/' . $repa['image']) . '" alt="' . $repa['name'] . '" class="img-circle" width="50" height="50" />';
$produit = $repa['name'];
$username = $repa['username'];
$date_debut = date("d/m/Y", strtotime($repa['reparation_debut']));
$date_fin = date("d/m/Y", strtotime($repa['reparation_fin']));
$storeName = $repa['store_name'] ?? 'N/A';
// Add the row data
$result['data'][$key] = [
$image,
$produit,
$repa['sku'],
$storeName,
$date_debut,
$date_fin,
];
}
}
log_message('debug', '📊 Nombre de réparations (performances) trouvées: ' . count($result['data']));
return $this->response->setJSON($result);
}
}

930
app/Controllers/OrderController.php

File diff suppressed because it is too large

237
app/Controllers/ReportController.php

@ -8,6 +8,9 @@ use App\Models\Stores;
use App\Models\Reports;
use App\Models\Products;
use App\Models\OrderItems;
use App\Models\Brands;
use App\Models\Users;
class ReportController extends AdminController
{
@ -36,6 +39,7 @@ class ReportController extends AdminController
$parking_data = $Reports->getOrderData($today_year);
$data['report_years'] = $Reports->getOrderYear();
// Process the parking data and calculate total amounts
$final_parking_data = [];
foreach ($parking_data as $month => $orders) {
@ -403,4 +407,237 @@ class ReportController extends AdminController
return $this->response->setJSON($result);
}
public function fetchSecuritePerformances()
{
$session = session();
$user = $session->get('user');
// ✅ RÉCUPÉRER LES PARAMÈTRES DE FILTRAGE
$startDate = $this->request->getGet('startDate');
$endDate = $this->request->getGet('endDate');
$storeFilter = $this->request->getGet('store_id');
// ✅ DEBUG
log_message('debug', '=== FILTRES REÇUS ===');
log_message('debug', 'startDate: ' . ($startDate ?? 'vide'));
log_message('debug', 'endDate: ' . ($endDate ?? 'vide'));
log_message('debug', 'store_id: ' . ($storeFilter ?? 'vide'));
// Charger les modèles
$orderModel = new \App\Models\Orders();
$orderItemModel = new \App\Models\OrderItems();
$productModel = new \App\Models\Products();
$brandModel = new \App\Models\Brands();
$userModel = new \App\Models\Users();
$storeModel = new \App\Models\Stores();
// Récupérer le store de l'utilisateur connecté
$userStore = null;
if (in_array($user['group_name'], ['SECURITE', 'Cheffe d\'Agence'])) {
$currentUser = $userModel->find($user['id']);
$userStore = $currentUser['store_id'] ?? null;
}
// ✅ CONSTRUCTION DE LA REQUÊTE AVEC FILTRES
$builder = $orderModel->builder();
$builder->select('
orders.id,
orders.bill_no,
orders.customer_name,
orders.customer_phone,
orders.customer_address,
orders.customer_cin,
orders.delivered_at,
orders.store_id,
CONCAT(deliverer.firstname, " ", deliverer.lastname) as agent_name,
deliverer.id as agent_id
')
->join('users as deliverer', 'deliverer.id = orders.delivered_by', 'left')
->where('orders.paid_status', 3)
->where('orders.delivered_by IS NOT NULL', null, false);
// ✅ FILTRER PAR STORE
// Si l'utilisateur a un store assigné, on filtre automatiquement par ce store
if ($userStore) {
$builder->where('orders.store_id', $userStore);
log_message('debug', '✅ Filtre userStore appliqué: ' . $userStore);
}
// Sinon, si un store est sélectionné dans le filtre, on l'applique
elseif (!empty($storeFilter)) {
$builder->where('orders.store_id', $storeFilter);
log_message('debug', '✅ Filtre storeFilter appliqué: ' . $storeFilter);
}
// ✅ FILTRAGE PAR DATE DE DÉBUT
if (!empty($startDate)) {
$builder->where('DATE(orders.delivered_at) >=', $startDate);
log_message('debug', '✅ Filtre startDate appliqué: ' . $startDate);
}
// ✅ FILTRAGE PAR DATE DE FIN
if (!empty($endDate)) {
$builder->where('DATE(orders.delivered_at) <=', $endDate);
log_message('debug', '✅ Filtre endDate appliqué: ' . $endDate);
}
$builder->orderBy('orders.delivered_at', 'DESC');
// ✅ DEBUG : Afficher la requête SQL
$sql = $builder->getCompiledSelect(false);
log_message('debug', '📋 SQL GÉNÉRÉ: ' . $sql);
$orders = $builder->get()->getResultArray();
log_message('debug', '📊 Nombre de commandes trouvées: ' . count($orders));
$result = [];
foreach ($orders as $order) {
$orderItems = $orderItemModel->where('order_id', $order['id'])->findAll();
foreach ($orderItems as $item) {
$qty = isset($item['qty']) ? (int)$item['qty'] : 1;
$product = $productModel->find($item['product_id']);
if ($product) {
$brand = $brandModel->find($product['marque']);
$brandName = $brand['name'] ?? 'Aucune marque';
$store = $storeModel->find($order['store_id']);
$storeName = $store['name'] ?? 'N/A';
$agentName = $order['agent_name'] ?? 'N/A';
$imageUrl = base_url('assets/images/products/' . $product['image']);
$validationDate = $order['delivered_at'] ?
date('d/m/Y H:i', strtotime($order['delivered_at'])) :
'N/A';
$result[] = [
$imageUrl, // 0 - Image
$order['bill_no'] ?? 'N/A', // 1 - N° Facture
$product['name'], // 2 - Désignation
$product['sku'], // 3 - UGS
$brandName, // 4 - Marque
$order['customer_name'], // 5 - Client
$agentName, // 6 - Agent Sécurité
$storeName, // 7 - Magasin
$validationDate, // 8 - Date Validation
'<span class="badge badge-success"><i class="fa fa-check"></i> VALIDÉE</span>', // 9
$qty, // 10 - Quantité
$product['sku'] ?? 'N/A', // 11
$order['customer_phone'] ?? 'N/A', // 12
$order['customer_address'] ?? 'N/A', // 13
$order['customer_cin'] ?? 'N/A', // 14
$product['numero_de_moteur'] ?? 'N/A', // 15
$product['chasis'] ?? 'N/A' // 16
];
}
}
}
log_message('debug', '✅ Nombre de lignes retournées: ' . count($result));
return $this->response->setJSON(['data' => $result]);
}
// ============================================
// MÉTHODE POUR RÉCUPÉRER LES DÉTAILS D'UNE VALIDATION
// ============================================
public function getSecuriteValidationDetails($orderId)
{
$session = session();
$user = $session->get('user');
$orderModel = new \App\Models\Orders();
$orderItemModel = new \App\Models\OrderItems();
$productModel = new \App\Models\Products();
$brandModel = new \App\Models\Brands();
$storeModel = new \App\Models\Stores();
$userModel = new \App\Models\Users();
$builder = $orderModel->builder();
$builder->select('
orders.*,
CONCAT(deliverer.firstname, " ", deliverer.lastname) as agent_name
')
->join('users as deliverer', 'deliverer.id = orders.delivered_by', 'left')
->where('orders.id', $orderId);
$order = $builder->get()->getRowArray();
if (!$order) {
return $this->response->setJSON(['error' => 'Commande non trouvée']);
}
// Récupérer le magasin
$store = $storeModel->find($order['store_id']);
$storeName = $store['name'] ?? 'N/A';
// Récupérer le premier item
$orderItem = $orderItemModel->where('order_id', $orderId)->first();
$product = null;
$brandName = 'Aucune marque';
$imageUrl = '';
$qty = 1;
if ($orderItem) {
// ✅ RÉCUPÉRER LA QUANTITÉ
$qty = isset($orderItem['qty']) ? (int)$orderItem['qty'] : 1;
$product = $productModel->find($orderItem['product_id']);
if ($product) {
$brand = $brandModel->find($product['marque']);
$brandName = $brand['name'] ?? 'Aucune marque';
$imageUrl = base_url('assets/images/products/' . $product['image']);
}
}
$agentName = $order['agent_name'] ?? 'N/A';
$validationDate = $order['delivered_at'] ?
date('d/m/Y H:i', strtotime($order['delivered_at'])) :
'N/A';
$result = [
$imageUrl, // 0
$order['bill_no'] ?? 'N/A', // 1
$product['name'] ?? 'N/A', // 2
$product['sku'] ?? 'N/A', // 3
$brandName, // 4
$order['customer_name'], // 5
$agentName, // 6
$storeName, // 7
$validationDate, // 8
'<span class="badge badge-success"><i class="fa fa-check"></i> VALIDÉE</span>', // 9
$qty, // 10 - ✅ QUANTITÉ
$product['sku'] ?? 'N/A', // 11
$order['customer_phone'] ?? 'N/A', // 12
$order['customer_address'] ?? 'N/A', // 13
$order['customer_cin'] ?? 'N/A', // 14
$product['numero_de_moteur'] ?? 'N/A', // 15
$product['chasis'] ?? 'N/A' // 16
];
return $this->response->setJSON($result);
}
public function securiteHistory()
{
$this->verifyRole('viewReports');
$session = session();
$user = $session->get('user');
$data['page_title'] = 'Historique des Validations Sécurité';
$data['user_role'] = $user['group_name']; // ✅ AJOUT DE LA VARIABLE user_role
// ✅ RÉCUPÉRER TOUS LES MAGASINS ACTIFS (comme dans AutresEncaissements)
$storeModel = new \App\Models\Stores();
$data['stores'] = $storeModel->getActiveStore();
log_message('debug', '📋 Nombre de magasins trouvés: ' . count($data['stores']));
log_message('debug', '👤 Rôle utilisateur: ' . $user['group_name']);
return $this->render_template('reports/securite_history', $data);
}
}

17
app/Controllers/SecuriteController.php

@ -17,14 +17,23 @@ class SecuriteController extends AdminController
private $pageTitle = 'Validation sortie motos';
public function index()
public function index() // ou validateSecurite() ou autre nom
{
$this->verifyRole('viewSecurite');
$data['page_title'] = $this->pageTitle;
$this->verifyRole('viewSecurite'); // ou autre permission
$session = session();
$user = $session->get('user');
$data['page_title'] = 'Validation Sécurité';
$data['user_role'] = $user['group_name']; // ✅ AJOUTER CETTE LIGNE
$data['user_permission'] = $this->permission;
// ✅ RÉCUPÉRER LES MAGASINS
$storeModel = new \App\Models\Stores();
$data['stores'] = $storeModel->getActiveStore();
return $this->render_template('securite/index', $data);
}
public function fetchSecuriteData()
{
$securiteModel = new Securite();

446
app/Controllers/SortieCaisseController.php

@ -415,7 +415,6 @@ class SortieCaisseController extends AdminController
*/
public function markAsPaid($id_sortie)
{
// Vérifier que l'utilisateur est une caissière
$session = session();
$users = $session->get('user');
@ -447,7 +446,114 @@ public function markAsPaid($id_sortie)
]);
}
// Mettre à jour le statut
// ✅ NOUVEAU: VÉRIFICATION DES FONDS DISPONIBLES
$orders = new Orders();
$Recouvrement = new Recouvrement();
$paymentData = $orders->getPaymentModes();
$totalRecouvrement = $Recouvrement->getTotalRecouvrements();
$total_sortie_caisse = $SortieCaisse->getTotalSortieCaisse();
// EXTRACTION DES TOTAUX (uniquement les décaissements déjà payés)
$total_sortie_espece = 0;
$total_sortie_mvola = 0;
$total_sortie_virement = 0;
if (is_object($total_sortie_caisse)) {
$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
$total_recouvrement_me = 0;
$total_recouvrement_be = 0;
$total_recouvrement_bm = 0;
$total_recouvrement_mb = 0;
if (is_object($totalRecouvrement)) {
$total_recouvrement_me = isset($totalRecouvrement->me) ? (float) $totalRecouvrement->me : 0;
$total_recouvrement_be = isset($totalRecouvrement->be) ? (float) $totalRecouvrement->be : 0;
$total_recouvrement_bm = isset($totalRecouvrement->bm) ? (float) $totalRecouvrement->bm : 0;
$total_recouvrement_mb = isset($totalRecouvrement->mb) ? (float) $totalRecouvrement->mb : 0;
}
// Orders
$total_espece1 = 0;
$total_espece2 = 0;
$total_mvola1 = 0;
$total_mvola2 = 0;
$total_virement1 = 0;
$total_virement2 = 0;
if (is_object($paymentData)) {
$total_espece1 = isset($paymentData->total_espece1) ? (float) $paymentData->total_espece1 : 0;
$total_espece2 = isset($paymentData->total_espece2) ? (float) $paymentData->total_espece2 : 0;
$total_mvola1 = isset($paymentData->total_mvola1) ? (float) $paymentData->total_mvola1 : 0;
$total_mvola2 = isset($paymentData->total_mvola2) ? (float) $paymentData->total_mvola2 : 0;
$total_virement1 = isset($paymentData->total_virement_bancaire1) ? (float) $paymentData->total_virement_bancaire1 : 0;
$total_virement2 = isset($paymentData->total_virement_bancaire2) ? (float) $paymentData->total_virement_bancaire2 : 0;
}
// CALCUL DES SOLDES DISPONIBLES
$total_espece_disponible = $total_espece1 +
$total_espece2 +
$total_recouvrement_me +
$total_recouvrement_be -
$total_sortie_espece;
$total_mvola_disponible = $total_mvola1 +
$total_mvola2 -
$total_recouvrement_me -
$total_recouvrement_mb +
$total_recouvrement_bm -
$total_sortie_mvola;
$total_virement_disponible = $total_virement1 +
$total_virement2 -
$total_recouvrement_be -
$total_recouvrement_bm +
$total_recouvrement_mb -
$total_sortie_virement;
// Vérifier selon le mode de paiement
$montant_retire = (float) $decaissement['montant_retire'];
$mode_paiement = $decaissement['mode_paiement'];
$fonds_disponible = 0;
$mode_paiement_label = '';
switch ($mode_paiement) {
case 'En espèce':
$fonds_disponible = $total_espece_disponible;
$mode_paiement_label = 'en espèce';
break;
case 'MVOLA':
$fonds_disponible = $total_mvola_disponible;
$mode_paiement_label = 'MVOLA';
break;
case 'Virement Bancaire':
$fonds_disponible = $total_virement_disponible;
$mode_paiement_label = 'virement bancaire';
break;
}
// ✅ VÉRIFICATION: Fonds suffisants ?
if ($montant_retire > $fonds_disponible) {
return $this->response->setJSON([
'success' => false,
'messages' => '❌ Paiement impossible — fonds ' . $mode_paiement_label . ' insuffisants.<br>' .
'<strong>Disponible:</strong> ' . number_format($fonds_disponible, 0, ',', ' ') . ' Ar<br>' .
'<strong>Demandé:</strong> ' . number_format($montant_retire, 0, ',', ' ') . ' Ar<br><br>' .
'<em>Soldes actuels:</em><br>' .
'• Espèce: ' . number_format($total_espece_disponible, 0, ',', ' ') . ' Ar<br>' .
'• MVOLA: ' . number_format($total_mvola_disponible, 0, ',', ' ') . ' Ar<br>' .
'• Virement: ' . number_format($total_virement_disponible, 0, ',', ' ') . ' Ar'
]);
}
// ✅ Fonds suffisants, on procède au paiement
$data = [
'statut' => 'Payé',
'date_paiement_effectif' => date('Y-m-d H:i:s')
@ -456,60 +562,39 @@ public function markAsPaid($id_sortie)
$result = $SortieCaisse->updateSortieCaisse($id_sortie, $data);
if ($result) {
// Créer une notification pour TOUS les DAF, Direction et SuperAdmin
// Créer une notification pour TOUS les DAF, Direction et SuperAdmin
try {
if (class_exists('App\Controllers\NotificationController')) {
$Notification = new NotificationController();
$montant = number_format($decaissement['montant_retire'], 0, ',', ' ');
$message = "💰 Décaissement payé - " . $montant . " Ar<br>" .
$message = "💰 Décaissement payé - " . $montant . " Ar (" . $mode_paiement . ")<br>" .
"Motif: " . $decaissement['motif'] . "<br>" .
"Caissière: " . $users['firstname'] . ' ' . $users['lastname'] . "<br>" .
"Store: " . $this->returnStoreName($decaissement['store_id']);
"Store: " . $this->returnStoreName($decaissement['store_id']) . "<br>" .
"Nouveau solde " . $mode_paiement_label . ": " . number_format($fonds_disponible - $montant_retire, 0, ',', ' ') . " Ar";
// ✅ Récupérer TOUS les stores
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
// ✅ 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'],
'sortieCaisse'
);
// Notifier DAF
$Notification->createNotification(
$message,
"DAF",
(int)$store['id'],
'sortieCaisse'
);
// Notifier SuperAdmin
$Notification->createNotification(
$message,
"SuperAdmin",
(int)$store['id'],
'sortieCaisse'
);
$Notification->createNotification($message, "Direction", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "DAF", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'sortieCaisse');
}
}
}
} catch (\Exception $e) {
// Logger l'erreur mais continuer
log_message('error', 'Erreur notification markAsPaid: ' . $e->getMessage());
}
return $this->response->setJSON([
'success' => true,
'messages' => '✅ Décaissement marqué comme <strong>PAYÉ</strong><br>' .
'Tous les Direction, DAF et SuperAdmin ont été notifiés.<br>' .
'Montant: ' . number_format($decaissement['montant_retire'], 0, ',', ' ') . ' Ar'
'Montant: ' . number_format($decaissement['montant_retire'], 0, ',', ' ') . ' Ar (' . $mode_paiement . ')<br>' .
'Nouveau solde ' . $mode_paiement_label . ': ' . number_format($fonds_disponible - $montant_retire, 0, ',', ' ') . ' Ar<br>' .
'<em>Tous les Direction, DAF et SuperAdmin ont été notifiés.</em>'
]);
} else {
@ -676,117 +761,8 @@ public function markAsPaid($id_sortie)
]);
}
// RÉCUPÉRATION DES DONNÉES FINANCIÈRES
$orders = new Orders();
$Recouvrement = new Recouvrement();
$sortieCaisse = new SortieCaisse();
$paymentData = $orders->getPaymentModes();
$totalRecouvrement = $Recouvrement->getTotalRecouvrements();
$total_sortie_caisse = $sortieCaisse->getTotalSortieCaisse();
// EXTRACTION DES TOTAUX DES SORTIES PAR MODE DE PAIEMENT
$total_sortie_espece = 0;
$total_sortie_mvola = 0;
$total_sortie_virement = 0;
if (is_object($total_sortie_caisse)) {
$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
$total_recouvrement_me = 0;
$total_recouvrement_be = 0;
$total_recouvrement_bm = 0;
$total_recouvrement_mb = 0;
if (is_object($totalRecouvrement)) {
$total_recouvrement_me = isset($totalRecouvrement->me) ? (float) $totalRecouvrement->me : 0;
$total_recouvrement_be = isset($totalRecouvrement->be) ? (float) $totalRecouvrement->be : 0;
$total_recouvrement_bm = isset($totalRecouvrement->bm) ? (float) $totalRecouvrement->bm : 0;
$total_recouvrement_mb = isset($totalRecouvrement->mb) ? (float) $totalRecouvrement->mb : 0;
}
// Orders
$total_espece1 = 0;
$total_espece2 = 0;
$total_mvola1 = 0;
$total_mvola2 = 0;
$total_virement1 = 0;
$total_virement2 = 0;
if (is_object($paymentData)) {
$total_espece1 = isset($paymentData->total_espece1) ? (float) $paymentData->total_espece1 : 0;
$total_espece2 = isset($paymentData->total_espece2) ? (float) $paymentData->total_espece2 : 0;
$total_mvola1 = isset($paymentData->total_mvola1) ? (float) $paymentData->total_mvola1 : 0;
$total_mvola2 = isset($paymentData->total_mvola2) ? (float) $paymentData->total_mvola2 : 0;
$total_virement1 = isset($paymentData->total_virement_bancaire1) ? (float) $paymentData->total_virement_bancaire1 : 0;
$total_virement2 = isset($paymentData->total_virement_bancaire2) ? (float) $paymentData->total_virement_bancaire2 : 0;
}
// CALCUL DES SOLDES DISPONIBLES PAR MODE DE PAIEMENT
$total_espece_disponible = $total_espece1 +
$total_espece2 +
$total_recouvrement_me +
$total_recouvrement_be -
$total_sortie_espece;
$total_mvola_disponible = $total_mvola1 +
$total_mvola2 -
$total_recouvrement_me -
$total_recouvrement_mb +
$total_recouvrement_bm -
$total_sortie_mvola;
$total_virement_disponible = $total_virement1 +
$total_virement2 -
$total_recouvrement_be -
$total_recouvrement_bm +
$total_recouvrement_mb -
$total_sortie_virement;
// VÉRIFICATION SELON LE MODE DE PAIEMENT CHOISI
$fonds_disponible = 0;
$mode_paiement_label = '';
switch ($mode_paiement) {
case 'En espèce':
$fonds_disponible = $total_espece_disponible;
$mode_paiement_label = 'en espèce';
break;
case 'MVOLA':
$fonds_disponible = $total_mvola_disponible;
$mode_paiement_label = 'MVOLA';
break;
case 'Virement Bancaire':
$fonds_disponible = $total_virement_disponible;
$mode_paiement_label = 'virement bancaire';
break;
default:
return $this->response->setJSON([
'success' => false,
'messages' => 'Mode de paiement invalide'
]);
}
// Vérification des fonds
if ($montant_retire > $fonds_disponible) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Décaissement échoué — fonds ' . $mode_paiement_label . ' insuffisants.<br>' .
'<strong>Disponible:</strong> ' . number_format($fonds_disponible, 0, ',', ' ') . ' Ar<br>' .
'<strong>Demandé:</strong> ' . number_format($montant_retire, 0, ',', ' ') . ' Ar<br><br>' .
'<em>Soldes actuels:</em><br>' .
'• Espèce: ' . number_format($total_espece_disponible, 0, ',', ' ') . ' Ar<br>' .
'• MVOLA: ' . number_format($total_mvola_disponible, 0, ',', ' ') . ' Ar<br>' .
'• Virement: ' . number_format($total_virement_disponible, 0, ',', ' ') . ' Ar'
]);
}
// ✅ SUPPRESSION: Plus besoin de vérifier les fonds disponibles à la création
// La vérification se fera au moment du paiement effectif par la caissière
// PRÉPARATION DES DONNÉES
$motif = $this->request->getPost('motif_select');
@ -870,7 +846,7 @@ public function markAsPaid($id_sortie)
$result = $model->addSortieCaisse($data);
if ($result) {
// Notification pour TOUS les Direction, DAF et SuperAdmin
// Notification pour TOUS les Direction, DAF et SuperAdmin
try {
if (class_exists('App\Controllers\NotificationController')) {
$Notification = new NotificationController();
@ -880,47 +856,26 @@ public function markAsPaid($id_sortie)
"Store: " . $this->returnStoreName($user['store_id']) . "<br>" .
"Demandeur: " . $user['firstname'] . ' ' . $user['lastname'];
// ✅ Récupérer TOUS les stores
$Stores = new Stores();
$allStores = $Stores->getActiveStore();
// ✅ 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'],
'sortieCaisse'
);
$Notification->createNotification(
$message,
"DAF",
(int)$store['id'],
'sortieCaisse'
);
$Notification->createNotification(
$message,
"SuperAdmin",
(int)$store['id'],
'sortieCaisse'
);
$Notification->createNotification($message, "Direction", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "DAF", (int)$store['id'], 'sortieCaisse');
$Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'sortieCaisse');
}
}
}
} catch (\Exception $e) {
log_message('error', 'Erreur notification createSortieCaisse: ' . $e->getMessage());
// Continue même si la notification échoue
}
return $this->response->setJSON([
'success' => true,
'messages' => 'Décaissement de ' . number_format($montant_retire, 0, ',', ' ') . ' Ar créé avec succès<br>' .
'Mode de paiement: ' . $mode_paiement . '<br>' .
'Nouveau solde ' . $mode_paiement_label . ': ' . number_format($fonds_disponible - $montant_retire, 0, ',', ' ') . ' Ar<br>' .
'<em>Notification envoyée à tous les Direction, DAF et SuperAdmin</em>'
'<em>En attente de validation</em>'
]);
} else {
@ -952,8 +907,6 @@ public function markAsPaid($id_sortie)
'nom_edit' => 'required',
'fonction_edit' => 'required',
'date_demande_edit' => 'required'
// Suppression des règles de validation pour les champs qui seront en hidden
// La preuve d'achat devient facultative
]);
if (!$validation->withRequest($this->request)->run()) {
@ -967,7 +920,6 @@ public function markAsPaid($id_sortie)
$session = session();
$users = $session->get('user');
// Récupérer le décaissement actuel
$sortieCaisse = new SortieCaisse();
$currentSortie = $sortieCaisse->getSortieCaisseSingle($id_sortie);
@ -978,14 +930,18 @@ public function markAsPaid($id_sortie)
]);
}
// Nettoyage et conversion du montant
// ✅ EMPÊCHER LA MODIFICATION SI DÉJÀ PAYÉ
if ($currentSortie['statut'] === 'Payé') {
return $this->response->setJSON([
'success' => false,
'messages' => 'Impossible de modifier un décaissement déjà payé.'
]);
}
$montant_retire_raw = $this->request->getPost('montant_retire_edit');
$montant_retire = (float) str_replace([' ', ','], ['', '.'], $montant_retire_raw);
// Récupérer le motif (disabled dans le form, donc on le récupère du champ hidden)
$motif = $this->request->getPost('motif_select_edit') ?: $this->request->getPost('motif_select_edit_hidden');
// Récupérer le mode de paiement (maintenant éditable, donc directement du formulaire)
$mode_paiement = $this->request->getPost('mode_paiement_edit');
if ($montant_retire <= 0) {
@ -995,130 +951,8 @@ public function markAsPaid($id_sortie)
]);
}
// RÉCUPÉRATION DES DONNÉES FINANCIÈRES
$orders = new Orders();
$Recouvrement = new Recouvrement();
$paymentData = $orders->getPaymentModes();
$totalRecouvrement = $Recouvrement->getTotalRecouvrements();
$total_sortie_caisse = $sortieCaisse->getTotalSortieCaisse();
// EXTRACTION DES TOTAUX DES SORTIES PAR MODE DE PAIEMENT
$total_sortie_espece = 0;
$total_sortie_mvola = 0;
$total_sortie_virement = 0;
if (is_object($total_sortie_caisse)) {
$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;
}
// IMPORTANT : Ajouter le montant actuel du décaissement aux sorties
// pour avoir le solde réel avant modification
if ($currentSortie['statut'] === 'Valider') {
switch ($currentSortie['mode_paiement']) {
case 'En espèce':
$total_sortie_espece -= (float) $currentSortie['montant_retire'];
break;
case 'MVOLA':
$total_sortie_mvola -= (float) $currentSortie['montant_retire'];
break;
case 'Virement Bancaire':
$total_sortie_virement -= (float) $currentSortie['montant_retire'];
break;
}
}
// Recouvrements
$total_recouvrement_me = 0;
$total_recouvrement_be = 0;
$total_recouvrement_bm = 0;
$total_recouvrement_mb = 0;
if (is_object($totalRecouvrement)) {
$total_recouvrement_me = isset($totalRecouvrement->me) ? (float) $totalRecouvrement->me : 0;
$total_recouvrement_be = isset($totalRecouvrement->be) ? (float) $totalRecouvrement->be : 0;
$total_recouvrement_bm = isset($totalRecouvrement->bm) ? (float) $totalRecouvrement->bm : 0;
$total_recouvrement_mb = isset($totalRecouvrement->mb) ? (float) $totalRecouvrement->mb : 0;
}
// Orders
$total_espece1 = 0;
$total_espece2 = 0;
$total_mvola1 = 0;
$total_mvola2 = 0;
$total_virement1 = 0;
$total_virement2 = 0;
if (is_object($paymentData)) {
$total_espece1 = isset($paymentData->total_espece1) ? (float) $paymentData->total_espece1 : 0;
$total_espece2 = isset($paymentData->total_espece2) ? (float) $paymentData->total_espece2 : 0;
$total_mvola1 = isset($paymentData->total_mvola1) ? (float) $paymentData->total_mvola1 : 0;
$total_mvola2 = isset($paymentData->total_mvola2) ? (float) $paymentData->total_mvola2 : 0;
$total_virement1 = isset($paymentData->total_virement_bancaire1) ? (float) $paymentData->total_virement_bancaire1 : 0;
$total_virement2 = isset($paymentData->total_virement_bancaire2) ? (float) $paymentData->total_virement_bancaire2 : 0;
}
// CALCUL DES SOLDES DISPONIBLES
$total_espece_disponible = $total_espece1 +
$total_espece2 +
$total_recouvrement_me +
$total_recouvrement_be -
$total_sortie_espece;
$total_mvola_disponible = $total_mvola1 +
$total_mvola2 -
$total_recouvrement_me -
$total_recouvrement_mb +
$total_recouvrement_bm -
$total_sortie_mvola;
$total_virement_disponible = $total_virement1 +
$total_virement2 -
$total_recouvrement_be -
$total_recouvrement_bm +
$total_recouvrement_mb -
$total_sortie_virement;
// VÉRIFICATION SELON LE MODE DE PAIEMENT CHOISI
$fonds_disponible = 0;
$mode_paiement_label = '';
switch ($mode_paiement) {
case 'En espèce':
$fonds_disponible = $total_espece_disponible;
$mode_paiement_label = 'en espèce';
break;
case 'MVOLA':
$fonds_disponible = $total_mvola_disponible;
$mode_paiement_label = 'MVOLA';
break;
case 'Virement Bancaire':
$fonds_disponible = $total_virement_disponible;
$mode_paiement_label = 'virement bancaire';
break;
default:
return $this->response->setJSON([
'success' => false,
'messages' => 'Mode de paiement invalide'
]);
}
// Vérification des fonds
if ($montant_retire > $fonds_disponible) {
return $this->response->setJSON([
'success' => false,
'messages' => 'Modification échouée — fonds ' . $mode_paiement_label . ' insuffisants.<br>' .
'<strong>Disponible:</strong> ' . number_format($fonds_disponible, 0, ',', ' ') . ' Ar<br>' .
'<strong>Demandé:</strong> ' . number_format($montant_retire, 0, ',', ' ') . ' Ar'
]);
}
// PRÉPARATION DES DONNÉES
// ✅ SUPPRESSION: Plus de vérification des fonds à la modification
// La vérification se fera uniquement au moment du paiement
$data = [
'montant_retire' => $montant_retire,
@ -1140,7 +974,6 @@ public function markAsPaid($id_sortie)
'reference' => $this->request->getPost('reference_edit') ?? ''
];
// Mapping source_fond et initiateur
if (isset($this->mapping[$motif])) {
$data['source_fond'] = $this->mapping[$motif]['source_fond'];
$data['initiateur_demande'] = $this->mapping[$motif]['initiateur_demande'];
@ -1149,7 +982,6 @@ public function markAsPaid($id_sortie)
$data['initiateur_demande'] = 'Caissière';
}
// Gestion du fichier (FACULTATIF)
$preuveFile = $this->request->getFile('sortie_preuve_edit');
if ($preuveFile && $preuveFile->isValid() && !$preuveFile->hasMoved()) {
$newName = $preuveFile->getRandomName();
@ -1163,9 +995,7 @@ public function markAsPaid($id_sortie)
$data['preuve_achat'] = $newName;
$data['mime_type'] = $preuveFile->getClientMimeType();
}
// Si aucun nouveau fichier n'est uploadé, on garde l'ancien (pas de modification)
// MISE À JOUR EN BASE
if ($sortieCaisse->updateSortieCaisse($id_sortie, $data)) {
return $this->response->setJSON([
'success' => true,
@ -1223,15 +1053,15 @@ public function markAsPaid($id_sortie)
switch ($statut) {
case "Valider":
$message = "✅ Votre décaissement a été validé par la Direction<br>Store: " . $this->returnStoreName($store_id);
$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');
break;
case "Refuser":
$message = "❌ Votre décaissement a été refusé par la Direction<br>Store: " . $this->returnStoreName($store_id) . "<br>Raison: " . $this->request->getPost('admin_raison');
$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');
break;
case "En attente":
$message = "⏳ Votre décaissement a été mis en attente par la Direction<br>Store: " . $this->returnStoreName($store_id);
$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');
break;
}

302
app/Models/AutresEncaissements.php

@ -0,0 +1,302 @@
<?php
namespace App\Models;
use CodeIgniter\Model;
class AutresEncaissements extends Model
{
protected $table = 'autres_encaissements';
protected $primaryKey = 'id';
protected $allowedFields = [
'type_encaissement',
'autre_type',
'montant',
'mode_paiement',
'commentaire',
'user_id',
'store_id',
'created_at',
'updated_at'
];
protected $useTimestamps = true;
protected $createdField = 'created_at';
protected $updatedField = 'updated_at';
/**
* Récupérer tous les encaissements avec les infos utilisateur et magasin
*
* @param int|null $storeId ID du magasin (optionnel)
* @return array
*/
public function getEncaissementsWithDetails($storeId = null)
{
$builder = $this->db->table($this->table);
$builder->select('
autres_encaissements.*,
CONCAT(users.firstname, " ", users.lastname) as user_name,
users.email as user_email,
stores.name as store_name
');
$builder->join('users', 'users.id = autres_encaissements.user_id', 'left');
$builder->join('stores', 'stores.id = autres_encaissements.store_id', 'left');
if ($storeId) {
$builder->where('autres_encaissements.store_id', $storeId);
}
$builder->orderBy('autres_encaissements.created_at', 'DESC');
return $builder->get()->getResultArray();
}
/**
* Récupérer un encaissement par ID avec détails
*
* @param int $id ID de l'encaissement
* @return array|null
*/
public function getTotalEncaissementsByMode($storeId = null, $startDate = null, $endDate = null)
{
$builder = $this->db->table($this->table);
$builder->select('
SUM(CASE WHEN mode_paiement = "Espèces" THEN montant ELSE 0 END) as total_espece,
SUM(CASE WHEN mode_paiement = "MVola" THEN montant ELSE 0 END) as total_mvola,
SUM(CASE WHEN mode_paiement = "Virement Bancaire" THEN montant ELSE 0 END) as total_virement
');
if ($storeId) {
$builder->where('store_id', $storeId);
}
if ($startDate) {
$builder->where('DATE(created_at) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(created_at) <=', $endDate);
}
$result = $builder->get()->getRow();
return [
'total_espece' => $result ? (float)$result->total_espece : 0,
'total_mvola' => $result ? (float)$result->total_mvola : 0,
'total_virement' => $result ? (float)$result->total_virement : 0,
];
}
public function getEncaissementById($id)
{
$builder = $this->db->table($this->table);
$builder->select('
autres_encaissements.*,
CONCAT(users.firstname, " ", users.lastname) as user_name,
users.email as user_email,
stores.name as store_name
');
$builder->join('users', 'users.id = autres_encaissements.user_id', 'left');
$builder->join('stores', 'stores.id = autres_encaissements.store_id', 'left');
$builder->where('autres_encaissements.id', $id);
return $builder->get()->getRowArray();
}
/**
* Statistiques par type d'encaissement
*
* @param int|null $storeId ID du magasin
* @param string|null $startDate Date de début (Y-m-d)
* @param string|null $endDate Date de fin (Y-m-d)
* @return array
*/
public function getStatsByType($storeId = null, $startDate = null, $endDate = null)
{
$builder = $this->db->table($this->table);
// ✅ SÉLECTION DES CHAMPS (sans commentaire dans le SELECT)
$builder->select('
type_encaissement,
mode_paiement,
COUNT(*) as total_count,
SUM(montant) as total_montant
');
if ($storeId) {
$builder->where('store_id', $storeId);
}
if ($startDate) {
$builder->where('DATE(created_at) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(created_at) <=', $endDate);
}
// ✅ GROUPER PAR type_encaissement ET mode_paiement
$builder->groupBy('type_encaissement, mode_paiement');
return $builder->get()->getResultArray();
}
/**
* Total des encaissements
*
* @param int|null $storeId ID du magasin
* @param string|null $startDate Date de début (Y-m-d)
* @param string|null $endDate Date de fin (Y-m-d)
* @return float
*/
public function getTotalEncaissements($storeId = null, $startDate = null, $endDate = null)
{
$builder = $this->db->table($this->table);
$builder->selectSum('montant', 'total');
if ($storeId) {
$builder->where('store_id', $storeId);
}
if ($startDate) {
$builder->where('DATE(created_at) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(created_at) <=', $endDate);
}
$result = $builder->get()->getRow();
return $result ? (float)$result->total : 0;
}
/**
* Nombre d'encaissements aujourd'hui
*
* @param int|null $storeId ID du magasin
* @return int
*/
public function getTodayCount($storeId = null)
{
$builder = $this->db->table($this->table);
$builder->where('DATE(created_at)', date('Y-m-d'));
if ($storeId) {
$builder->where('store_id', $storeId);
}
return $builder->countAllResults();
}
/**
* Nombre total d'encaissements
*
* @param int|null $storeId ID du magasin
* @param string|null $startDate Date de début (Y-m-d)
* @param string|null $endDate Date de fin (Y-m-d)
* @return int
*/
public function getTotalCount($storeId = null, $startDate = null, $endDate = null)
{
$builder = $this->db->table($this->table);
if ($storeId) {
$builder->where('store_id', $storeId);
}
if ($startDate) {
$builder->where('DATE(created_at) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(created_at) <=', $endDate);
}
return $builder->countAllResults();
}
/**
* Encaissements récents (7 derniers jours)
*
* @param int|null $storeId ID du magasin
* @param int $limit Nombre de résultats
* @return array
*/
public function getRecentEncaissements($storeId = null, $limit = 10)
{
$builder = $this->db->table($this->table);
$builder->select('
autres_encaissements.*,
CONCAT(users.firstname, " ", users.lastname) as user_name,
stores.name as store_name
');
$builder->join('users', 'users.id = autres_encaissements.user_id', 'left');
$builder->join('stores', 'stores.id = autres_encaissements.store_id', 'left');
if ($storeId) {
$builder->where('autres_encaissements.store_id', $storeId);
}
$builder->where('DATE(autres_encaissements.created_at) >=', date('Y-m-d', strtotime('-7 days')));
$builder->orderBy('autres_encaissements.created_at', 'DESC');
$builder->limit($limit);
return $builder->get()->getResultArray();
}
/**
* Encaissements par utilisateur
*
* @param int $userId ID de l'utilisateur
* @param string|null $startDate Date de début
* @param string|null $endDate Date de fin
* @return array
*/
public function getEncaissementsByUser($userId, $startDate = null, $endDate = null)
{
$builder = $this->db->table($this->table);
$builder->where('user_id', $userId);
if ($startDate) {
$builder->where('DATE(created_at) >=', $startDate);
}
if ($endDate) {
$builder->where('DATE(created_at) <=', $endDate);
}
$builder->orderBy('created_at', 'DESC');
return $builder->get()->getResultArray();
}
/**
* Recherche d'encaissements
*
* @param string $keyword Mot-clé de recherche
* @param int|null $storeId ID du magasin
* @return array
*/
public function searchEncaissements($keyword, $storeId = null)
{
$builder = $this->db->table($this->table);
$builder->select('
autres_encaissements.*,
CONCAT(users.firstname, " ", users.lastname) as user_name,
stores.name as store_name
');
$builder->join('users', 'users.id = autres_encaissements.user_id', 'left');
$builder->join('stores', 'stores.id = autres_encaissements.store_id', 'left');
$builder->groupStart()
->like('autres_encaissements.type_encaissement', $keyword)
->orLike('autres_encaissements.commentaire', $keyword)
->orLike('CONCAT(users.firstname, " ", users.lastname)', $keyword)
->groupEnd();
if ($storeId) {
$builder->where('autres_encaissements.store_id', $storeId);
}
$builder->orderBy('autres_encaissements.created_at', 'DESC');
return $builder->get()->getResultArray();
}
}

165
app/Models/Avance.php

@ -11,7 +11,8 @@ class Avance extends Model {
'avance_amount', 'avance_date','user_id',
'customer_name', 'customer_address', 'customer_phone', 'customer_cin',
'gross_amount','amount_due','product_id','is_order','active','store_id',
'type_avance','type_payment', 'deadline','commentaire','product_name'
'type_avance','type_payment', 'deadline','commentaire','product_name',
'validated', 'validated_by', 'validated_at'
];
public function createAvance(array $data) {
@ -210,44 +211,34 @@ class Avance extends Model {
}
// ✅ CORRECTION PRINCIPALE : getPaymentModesAvance pour la caissière
public function getPaymentModesAvance()
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction','DAF']);
try {
$builder = $this->db->table('avances')
->select('
SUM(avance_amount) AS total,
SUM(CASE WHEN LOWER(type_payment) = "mvola" THEN avance_amount ELSE 0 END) AS total_mvola,
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('active', 1)
->where('is_order', 0); // ✅ Exclure les avances devenues orders
// ✅ CORRECTION : Ajouter le filtre store_id pour la caissière
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']);
}
$result = $builder->get()->getRowObject();
// ✅ MODIFICATION : Ne compter QUE les avances VALIDÉES
public function getPaymentModesAvance()
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction','DAF']);
// ✅ Gérer le cas où il n'y a pas de résultats
if (!$result) {
return (object) [
'total' => 0,
'total_mvola' => 0,
'total_espece' => 0,
'total_virement_bancaire' => 0
];
}
try {
$builder = $this->db->table('avances')
->select('
SUM(avance_amount) AS total,
SUM(CASE WHEN LOWER(type_payment) = "mvola" THEN avance_amount ELSE 0 END) AS total_mvola,
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('active', 1)
->where('is_order', 0);
// ✅ Filtre par store pour non-admin
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']);
}
return $result;
$result = $builder->get()->getRowObject();
} catch (\Exception $e) {
log_message('error', 'Erreur getPaymentModesAvance: ' . $e->getMessage());
// ✅ Gérer le cas où il n'y a pas de résultats
if (!$result) {
return (object) [
'total' => 0,
'total_mvola' => 0,
@ -255,7 +246,19 @@ class Avance extends Model {
'total_virement_bancaire' => 0
];
}
return $result;
} catch (\Exception $e) {
log_message('error', 'Erreur getPaymentModesAvance: ' . $e->getMessage());
return (object) [
'total' => 0,
'total_mvola' => 0,
'total_espece' => 0,
'total_virement_bancaire' => 0
];
}
}
public function getAllAvanceData1(int $id=null) {
$session = session();
@ -403,13 +406,26 @@ class Avance extends Model {
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction', 'DAF']);
$isCommercial = in_array($users['group_name'], ['COMMERCIALE']);
$isCaissier = in_array($users['group_name'], ['Caissière']);
$builder = $this->where('is_order', 0)
->where('active', 1)
->where('amount_due >', 0);
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']);
// ✅ LOGIQUE PAR RÔLE
if ($isCommercial) {
// Commercial voit TOUTES ses avances (validées ET non validées)
$builder->where('user_id', $users['id']);
} elseif ($isCaissier) {
// Caissière voit UNIQUEMENT les avances validées de son store
$builder->where('validated', 1)
->where('store_id', $users['store_id']);
} elseif ($isAdmin) {
// Admin voit tout (pas de filtre supplémentaire)
} else {
// Autres rôles : ne rien afficher
$builder->where('1', '0'); // Condition toujours fausse
}
if ($id) {
@ -418,19 +434,25 @@ class Avance extends Model {
return $builder->orderBy('avance_date', 'DESC')->findAll();
}
public function getCompletedAvances(int $id = null)
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction', 'DAF']);
$isCommercial = in_array($users['group_name'], ['COMMERCIALE']);
$isCaissier = in_array($users['group_name'], ['Caissière']);
$builder = $this->where('is_order', 0)
->where('active', 1)
->where('amount_due', 0);
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']);
if ($isCommercial) {
$builder->where('user_id', $users['id']);
} elseif ($isCaissier) {
$builder->where('validated', 1)
->where('store_id', $users['store_id']);
} elseif (!$isAdmin) {
$builder->where('1', '0');
}
if ($id) {
@ -440,6 +462,67 @@ class Avance extends Model {
return $builder->orderBy('avance_date', 'DESC')->findAll();
}
/**
* ✅ NOUVELLE : getPendingValidationAvances
*/
public function getPendingValidationAvances(int $store_id = null)
{
try {
$builder = $this->where('validated', 0)
->where('active', 1)
->where('is_order', 0);
if ($store_id) {
$builder->where('store_id', $store_id);
}
return $builder->orderBy('avance_date', 'DESC')->findAll();
} catch (\Exception $e) {
log_message('error', 'Erreur getPendingValidationAvances: ' . $e->getMessage());
return [];
}
}
/**
* ✅ VALIDATION D'UNE AVANCE
*/
public function validateAvance(int $avance_id, int $caissiere_id): bool
{
try {
$avance = $this->find($avance_id);
if (!$avance) {
log_message('error', "Avance {$avance_id} introuvable");
return false;
}
if ($avance['validated'] == 1) {
log_message('warning', "Avance {$avance_id} déjà validée");
return false;
}
$updateResult = $this->update($avance_id, [
'validated' => 1,
'validated_by' => $caissiere_id,
'validated_at' => date('Y-m-d H:i:s')
]);
if ($updateResult) {
log_message('info', "✅ Avance {$avance_id} validée par caissière {$caissiere_id}");
// Vérifier si conversion nécessaire
$this->autoCheckAndConvert($avance_id);
}
return $updateResult;
} catch (\Exception $e) {
log_message('error', "Erreur validation avance {$avance_id}: " . $e->getMessage());
return false;
}
}
public function markAsPrinted($avance_id)
{
try {

2
app/Models/OrderItems.php

@ -8,7 +8,7 @@ use DateTime;
class OrderItems extends Model
{
protected $table = 'orders_item';
protected $allowedFields = ['order_id', 'product_id', 'puissance', 'rate', 'amount'];
protected $allowedFields = ['order_id', 'product_id', 'puissance', 'rate', 'amount','qty'];
public function insertOrderItem($data)
{

6
app/Models/Orders.php

@ -212,11 +212,10 @@ class Orders extends Model
'order_id' => $order_id,
'product_id' => $post[$x],
'rate' => $data['rate_value'][$x],
'qty' => 1,
'qty' => isset($data['qty'][$x]) ? (int)$data['qty'][$x] : 1,
'amount' => $data['amount_value'][$x],
'puissance' => $puissances[$x] ?? 1,
];
$orderItemModel->insert($items);
// ✅ CORRECTION : Marquer product_sold = 1 dès la création
@ -281,6 +280,7 @@ class Orders extends Model
'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],
];
@ -462,7 +462,7 @@ class Orders extends Model
ELSE 0
END) AS total_virement_bancaire2
')
->whereIn('orders.paid_status', [1, 2, 3]); // ← CHANGEZ CETTE LIGNE
->whereIn('orders.paid_status', [1, 3]);
if (!$isAdmin) {
$baseQuery->where('orders.store_id', $users['store_id']);

34
app/Models/SortieCaisse.php

@ -72,11 +72,11 @@ class SortieCaisse extends Model
->findAll();
}
// ✅ CAISSIÈRE : Voir uniquement SES décaissements
// ✅ CAISSIÈRE : Voir TOUS les décaissements de SON STORE (pas seulement les siens)
if($users["group_name"] === "Caissière"){
return $this
->select('*')
->where('user_id', $users['id'])
->where('store_id', $users['store_id']) // ✅ CHANGEMENT ICI
->orderBy('date_retrait', 'DESC')
->findAll();
}
@ -112,11 +112,11 @@ class SortieCaisse extends Model
->findAll();
}
// ✅ CAISSIÈRE : Voir uniquement SES décaissements
// ✅ CAISSIÈRE : Voir TOUS les décaissements de SON STORE
if($users["group_name"] === "Caissière"){
return $this
->select('*')
->where('user_id', $users['id'])
->where('store_id', $users['store_id']) // ✅ CHANGEMENT ICI
->orderBy('date_retrait', 'DESC')
->findAll();
}
@ -162,7 +162,6 @@ class SortieCaisse extends Model
->first();
return $reparation;
}
/**
* ✅ MODIFICATION : DAF, Direction, SuperAdmin voient TOUS les totaux
*/
@ -176,13 +175,13 @@ class SortieCaisse extends Model
if ($isAdmin) {
try {
return $this->select('
SUM(CASE WHEN mode_paiement = "En espèce" THEN montant_retire ELSE 0 END) AS total_espece,
SUM(CASE WHEN mode_paiement = "MVOLA" THEN montant_retire ELSE 0 END) AS total_mvola,
SUM(CASE WHEN mode_paiement = "Virement Bancaire" THEN montant_retire ELSE 0 END) AS total_virement,
SUM(montant_retire) AS mr
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
')
// ✅ SUPPRESSION DU FILTRE PAR STORE
->whereIn('statut', ['Valider', 'Payé'])
// ✅ CHANGEMENT : Uniquement statut = "Payé" (plus "Valider")
->where('statut', 'Payé')
->get()
->getRowObject();
} catch (\Exception $e) {
@ -195,16 +194,17 @@ class SortieCaisse extends Model
];
}
} else {
// ✅ CAISSIÈRE : Uniquement son store
// ✅ CAISSIÈRE : Uniquement son store ET statut "Payé"
try {
return $this->select('
SUM(CASE WHEN mode_paiement = "En espèce" THEN montant_retire ELSE 0 END) AS total_espece,
SUM(CASE WHEN mode_paiement = "MVOLA" THEN montant_retire ELSE 0 END) AS total_mvola,
SUM(CASE WHEN mode_paiement = "Virement Bancaire" THEN montant_retire ELSE 0 END) AS total_virement,
SUM(montant_retire) AS mr
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'])
->whereIn('statut', ['Valider', 'Payé'])
// ✅ CHANGEMENT : Uniquement statut = "Payé" (plus "Valider")
->where('statut', 'Payé')
->get()
->getRowObject();
} catch (\Exception $e) {

25
app/Models/Users.php

@ -64,6 +64,31 @@ class Users extends Model
->where('groups.group_name', 'COMMERCIALE')
->findAll(); // Get all matching users
}
public function getSecurityAgents()
{
return $this->select('users.id, users.firstname, users.lastname, CONCAT(users.firstname, " ", users.lastname) as full_name')
->join('user_group', 'user_group.user_id = users.id', 'left')
->join('groups', 'groups.id = user_group.group_id', 'left')
->where('groups.group_name', 'SECURITE')
->where('users.active', 1) // Uniquement les utilisateurs actifs
->orderBy('users.firstname', 'ASC')
->findAll();
}
/**
* ✅ MÉTHODE GÉNÉRIQUE : Récupérer les utilisateurs par groupe
*/
public function getUsersByGroupName($groupName)
{
return $this->select('users.id, users.firstname, users.lastname, users.email, users.store_id, CONCAT(users.firstname, " ", users.lastname) as full_name, groups.group_name')
->join('user_group', 'user_group.user_id = users.id', 'left')
->join('groups', 'groups.id = user_group.group_id', 'left')
->where('groups.group_name', strtoupper($groupName))
->where('users.active', 1)
->orderBy('users.firstname', 'ASC')
->findAll();
}
/**
* get grouped user by id
* @param mixed $userId

978
app/Views/autres_encaissements/index.php

@ -0,0 +1,978 @@
<!-- application/Views/autres_encaissements/index.php -->
<style>
.encaissement-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
margin: 20px 0;
color: white;
padding: 30px;
}
.encaissement-header h1 {
margin: 0;
font-size: 28px;
font-weight: 700;
color : white;
}
.stat-card {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 12px;
padding: 25px;
transition: all 0.3s ease;
border-left: 5px solid #2196f3;
text-align: center;
margin-bottom: 20px;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 32px;
font-weight: 800;
color: #1976d2;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
color: #1565c0;
letter-spacing: 0.5px;
}
.form-section {
background: white;
border-radius: 12px;
padding: 30px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
margin-bottom: 30px;
}
.form-section h3 {
color: #667eea;
font-weight: 700;
margin-bottom: 25px;
padding-bottom: 15px;
border-bottom: 3px solid #667eea;
}
.form-control:focus {
border-color: #667eea;
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
}
.btn-primary-custom {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border: none;
padding: 12px 30px;
font-weight: 600;
border-radius: 8px;
transition: all 0.3s ease;
color: white;
}
.btn-primary-custom:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
color: white;
}
#historyTable thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
#historyTable thead th {
border: none !important;
font-weight: 600;
text-transform: uppercase;
font-size: 13px;
letter-spacing: 0.5px;
}
#historyTable tbody tr {
transition: all 0.2s ease;
}
#historyTable tbody tr:hover {
background-color: #f5f5f5;
transform: scale(1.01);
}
.filter-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
#autreTypeGroup {
display: none;
}
.alert-notification {
position: fixed;
top: 20px;
right: 20px;
z-index: 9999;
min-width: 300px;
}
</style>
<div class="content-wrapper">
<!-- Content Header -->
<section class="content-header">
<div class="encaissement-header">
<h1>
<i class="fa fa-money"></i> Autres Encaissements
</h1>
<p style="margin: 10px 0 0 0; font-size: 16px; opacity: 0.9;">
Gestion des encaissements divers (Plastification, Duplicata, etc.)
</p>
</div>
</section>
<!-- Main content -->
<section class="content">
<div id="messages"></div>
<!-- Statistiques -->
<div class="row">
<div class="col-md-4">
<div class="stat-card">
<i class="fa fa-money" style="font-size: 40px; color: #2196f3;"></i>
<div class="stat-number" id="totalMontant">0 Ar</div>
<div class="stat-label">Total Encaissements</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card">
<i class="fa fa-calendar" style="font-size: 40px; color: #2196f3;"></i>
<div class="stat-number" id="todayCount">0</div>
<div class="stat-label">Encaissements Aujourd'hui</div>
</div>
</div>
</div>
<!-- Formulaire d'ajout -->
<?php if (in_array('createEncaissement', $user_permission)): ?>
<div class="row">
<div class="col-md-12">
<div class="form-section">
<h3><i class="fa fa-plus-circle"></i> Ajouter un Encaissement</h3>
<form id="formEncaissement">
<div class="row">
<div class="col-md-3">
<div class="form-group">
<label for="typeEncaissement">Type d'Encaissement <span class="text-danger">*</span></label>
<select class="form-control" id="typeEncaissement" name="type_encaissement" required>
<option value="">-- Sélectionner --</option>
<option value="Plastification">Plastification</option>
<option value="Duplicata">Duplicata</option>
<option value="Retour Décaissement">Retour Décaissement</option>
<option value="Vente de Pièce">Vente de Pièce</option>
<option value="Autre">Autre (Spécifier)</option>
</select>
</div>
</div>
<div class="col-md-3" id="autreTypeGroup">
<div class="form-group">
<label for="autreType">Spécifier le type <span class="text-danger">*</span></label>
<input type="text" class="form-control" id="autreType" name="autre_type" placeholder="Ex: Frais de dossier">
</div>
</div>
<div class="col-md-3">
<div class="form-group">
<label for="montant">Montant (Ar) <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="montant" name="montant" placeholder="Ex: 50000" required min="0" step="0.01">
</div>
</div>
<!-- ✅ NOUVEAU CHAMP MODE DE PAIEMENT -->
<div class="col-md-3">
<div class="form-group">
<label for="modePaiement">Mode de Paiement <span class="text-danger">*</span></label>
<select class="form-control" id="modePaiement" name="mode_paiement" required>
<option value="">-- Sélectionner --</option>
<option value="Espèces">Espèces</option>
<option value="MVola">MVola</option>
<option value="Virement Bancaire">Virement Bancaire</option>
</select>
</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="form-group">
<label for="commentaire">Commentaire</label>
<textarea class="form-control" id="commentaire" name="commentaire" rows="3" placeholder="Ajouter un commentaire (optionnel)"></textarea>
</div>
</div>
</div>
<div class="text-right">
<button type="reset" class="btn btn-default">
<i class="fa fa-refresh"></i> Réinitialiser
</button>
<button type="submit" class="btn btn-primary">
<i class="fa fa-save"></i> Enregistrer l'Encaissement
</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<!-- Filtres -->
<div class="row">
<div class="col-md-12">
<div class="filter-section">
<div class="row">
<div class="col-md-3">
<label for="filterStartDate">Date de début</label>
<input type="date" id="filterStartDate" class="form-control">
</div>
<div class="col-md-3">
<label for="filterEndDate">Date de fin</label>
<input type="date" id="filterEndDate" class="form-control">
</div>
<?php if (in_array($user_role, ['SuperAdmin', 'DAF', 'Direction'])): ?>
<div class="col-md-3">
<label for="filterStore">Magasin</label>
<select id="filterStore" class="form-control">
<option value="">Tous les magasins</option>
<?php foreach ($stores as $store): ?>
<option value="<?= $store['id'] ?>"><?= $store['name'] ?></option>
<?php endforeach; ?>
</select>
</div>
<?php endif; ?>
<div class="col-md-3">
<label>&nbsp;</label><br>
<button id="btnFilter" class="btn btn-primary">
<i class="fa fa-filter"></i> Filtrer
</button>
<button id="btnReset" class="btn btn-warning">
<i class="fa fa-refresh"></i> Réinitialiser
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tableau historique -->
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-history"></i> Historique des Encaissements
</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>Commentaire</th>
<th>Créé par</th>
<th>Magasin</th>
<th>Date</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
<!-- Les données seront chargées par DataTables -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Modal Détails -->
<div class="modal fade" id="detailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<button type="button" class="close" data-dismiss="modal" style="color: white;">&times;</button>
<h4 class="modal-title">
<i class="fa fa-info-circle"></i> Détails de l'Encaissement
</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-6">
<h4 style="border-bottom: 2px solid #667eea; padding-bottom: 10px; margin-bottom: 20px;">
<i class="fa fa-money"></i> Informations Encaissement
</h4>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>ID:</strong></div>
<div class="col-xs-7"><span id="detailId"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Type:</strong></div>
<div class="col-xs-7"><span id="detailType" style="font-weight: 600; color: #667eea;"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Montant:</strong></div>
<div class="col-xs-7"><span id="detailMontant" style="font-size: 22px; font-weight: bold; color: #2196f3;"></span></div>
</div>
<!-- ✅ NOUVEAU CHAMP -->
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Mode de paiement:</strong></div>
<div class="col-xs-7"><span id="detailMode"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Date:</strong></div>
<div class="col-xs-7"><span id="detailDate"></span></div>
</div>
</div>
<div class="col-md-6">
<h4 style="border-bottom: 2px solid #667eea; padding-bottom: 10px; margin-bottom: 20px;">
<i class="fa fa-user"></i> Informations Utilisateur
</h4>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Créé par:</strong></div>
<div class="col-xs-7"><span id="detailUser"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Email:</strong></div>
<div class="col-xs-7"><span id="detailEmail"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-5"><strong>Magasin:</strong></div>
<div class="col-xs-7"><span id="detailStore"></span></div>
</div>
</div>
</div>
<div class="row" style="margin-top: 20px;">
<div class="col-md-12">
<h4 style="border-bottom: 2px solid #667eea; padding-bottom: 10px; margin-bottom: 20px;">
<i class="fa fa-comment"></i> Commentaire
</h4>
<p id="detailCommentaire" style="padding: 15px; background: #f8f9fa; border-radius: 8px; min-height: 60px;"></p>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fa fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
<!-- Modal Édition -->
<?php if (in_array('updateEncaissement', $user_permission)): ?>
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<button type="button" class="close" data-dismiss="modal" style="color: white;">&times;</button>
<h4 class="modal-title">
<i class="fa fa-edit"></i> Modifier l'Encaissement
</h4>
</div>
<form id="formEditEncaissement">
<div class="modal-body">
<input type="hidden" id="editId" name="id">
<div class="form-group">
<label for="editTypeEncaissement">Type d'Encaissement <span class="text-danger">*</span></label>
<select class="form-control" id="editTypeEncaissement" name="type_encaissement" required>
<option value="">-- Sélectionner --</option>
<option value="Plastification">Plastification</option>
<option value="Duplicata">Duplicata</option>
<option value="Retour Décaissement">Retour Décaissement</option>
<option value="Vente de Pièce">Vente de Pièce</option>
<option value="Autre">Autre (Spécifier)</option>
</select>
</div>
<div class="form-group" id="editAutreTypeGroup" style="display: none;">
<label for="editAutreType">Spécifier le type</label>
<input type="text" class="form-control" id="editAutreType" name="autre_type" placeholder="Ex: Frais de dossier">
</div>
<div class="form-group">
<label for="editMontant">Montant (Ar) <span class="text-danger">*</span></label>
<input type="number" class="form-control" id="editMontant" name="montant" required min="0" step="0.01">
</div>
<!-- ✅ NOUVEAU CHAMP MODE DE PAIEMENT -->
<div class="form-group">
<label for="editModePaiement">Mode de Paiement <span class="text-danger">*</span></label>
<select class="form-control" id="editModePaiement" name="mode_paiement" required>
<option value="">-- Sélectionner --</option>
<option value="Espèces">Espèces</option>
<option value="MVola">MVola</option>
<option value="Virement Bancaire">Virement Bancaire</option>
</select>
</div>
<div class="form-group">
<label for="editCommentaire">Commentaire</label>
<textarea class="form-control" id="editCommentaire" name="commentaire" rows="3"></textarea>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fa fa-times"></i> Annuler
</button>
<button type="submit" class="btn btn-primary-custom">
<i class="fa fa-save"></i> Enregistrer les modifications
</button>
</div>
</form>
</div>
</div>
</div>
<?php endif; ?>
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<script>
// ✅ DÉCLARER LA FONCTION EN DEHORS DU $(function) POUR LA RENDRE GLOBALE
function loadStatisticsForFilteredPeriod(startDate, endDate, store_id) {
console.log('📊 [STATS] Début chargement statistiques');
console.log('📊 [STATS] Paramètres:', {
startDate: startDate,
endDate: endDate,
store_id: store_id
});
$.ajax({
url: '<?= base_url('encaissements/statistics') ?>',
type: 'GET',
data: {
startDate: startDate || '',
endDate: endDate || '',
store_id: store_id || ''
},
beforeSend: function() {
console.log('📊 [STATS] Requête envoyée...');
// Afficher un loader
$('#totalMontant').html('<i class="fa fa-spinner fa-spin"></i>');
$('#todayCount').html('<i class="fa fa-spinner fa-spin"></i>');
},
success: function(response) {
console.log('✅ [STATS] Réponse reçue:', response);
if (response.success) {
console.log('✅ [STATS] Mise à jour des valeurs:', {
total_montant: response.total_montant,
today_count: response.today_count,
total_count: response.total_count
});
// ✅ MISE À JOUR DES CARTES
$('#totalMontant').text(response.total_montant);
$('#todayCount').text(response.today_count);
// Si tu ajoutes une carte pour total_count
if ($('#totalCount').length) {
$('#totalCount').text(response.total_count);
}
// ✅ Si tu as des éléments pour afficher les totaux par mode
if ($('#totalEspece').length) {
$('#totalEspece').text(response.total_espece);
}
if ($('#totalMvola').length) {
$('#totalMvola').text(response.total_mvola);
}
if ($('#totalVirement').length) {
$('#totalVirement').text(response.total_virement);
}
console.log('✅ [STATS] DOM mis à jour avec succès');
// Afficher les infos de debug si disponibles
if (response.debug) {
console.log('🔍 [DEBUG]', response.debug);
}
} else {
console.error('❌ [STATS] Erreur: response.success = false', response);
$('#totalMontant').text('Erreur');
$('#todayCount').text('Erreur');
}
},
error: function(xhr, status, error) {
console.error('❌ [STATS] Erreur AJAX complète:', {
status: status,
error: error,
responseText: xhr.responseText,
statusCode: xhr.status
});
// Essayer de parser la réponse
try {
var errorResponse = JSON.parse(xhr.responseText);
console.error('❌ [STATS] Détails erreur:', errorResponse);
} catch(e) {
console.error('❌ [STATS] Impossible de parser la réponse');
}
$('#totalMontant').text('0 Ar');
$('#todayCount').text('0');
}
});
}
$(function() {
console.log('🚀 [INIT] Initialisation de la page...');
$("#encaissementNav").addClass('active');
// Afficher/masquer le champ "Autre type" - Formulaire principal
$('#typeEncaissement').on('change', function() {
if ($(this).val() === 'Autre') {
$('#autreTypeGroup').slideDown();
$('#autreType').prop('required', true);
} else {
$('#autreTypeGroup').slideUp();
$('#autreType').prop('required', false).val('');
}
});
// Afficher/masquer le champ "Autre type" - Modal édition
$('#editTypeEncaissement').on('change', function() {
if ($(this).val() === 'Autre') {
$('#editAutreTypeGroup').slideDown();
$('#editAutreType').prop('required', true);
} else {
$('#editAutreTypeGroup').slideUp();
$('#editAutreType').prop('required', false).val('');
}
});
// Configuration DataTable en français
$.extend(true, $.fn.dataTable.defaults, {
language: {
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sLengthMenu: "Afficher _MENU_ éléments",
sInfo: "Affichage de _START_ à _END_ sur _TOTAL_ éléments",
sInfoEmpty: "Aucun élément à afficher",
sInfoFiltered: "(filtré de _MAX_ éléments au total)",
sLoadingRecords: "Chargement en cours...",
sZeroRecords: "Aucun encaissement trouvé",
sEmptyTable: "Aucune donnée disponible",
oPaginate: {
sFirst: "Premier",
sPrevious: "Précédent",
sNext: "Suivant",
sLast: "Dernier"
}
}
});
// ✅ INITIALISER LES DATES À AUJOURD'HUI PAR DÉFAUT
var today = new Date().toISOString().split('T')[0];
$('#filterStartDate').val(today);
$('#filterEndDate').val(today);
console.log('📅 [INIT] Dates initialisées:', {
startDate: $('#filterStartDate').val(),
endDate: $('#filterEndDate').val()
});
// ✅ FONCTION : CHARGER LES STATISTIQUES DU MOIS EN COURS
function loadStatisticsForCurrentMonth() {
console.log('📊 [INIT] Chargement des statistiques du mois en cours...');
var now = new Date();
var firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
var lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
var startDate = firstDay.toISOString().split('T')[0];
var endDate = lastDay.toISOString().split('T')[0];
var store_id = $('#filterStore').val() || '';
console.log('📅 [INIT] Période du mois:', {
firstDay: startDate,
lastDay: endDate,
store_id: store_id
});
// ✅ Appeler la fonction globale
loadStatisticsForFilteredPeriod(startDate, endDate, store_id);
}
// ✅ INITIALISATION DATATABLE
var historyTable = $('#historyTable').DataTable({
ajax: {
url: '<?= base_url('encaissements/fetch') ?>',
type: 'GET',
data: function(d) {
d.startDate = $('#filterStartDate').val();
d.endDate = $('#filterEndDate').val();
d.store_id = $('#filterStore').val();
console.log('📤 [DATATABLE] Filtres envoyés:', {
startDate: d.startDate,
endDate: d.endDate,
store_id: d.store_id
});
},
dataSrc: function(json) {
console.log('📥 [DATATABLE] Données reçues:', json.data.length, 'lignes');
return json.data;
},
error: function(xhr, error, code) {
console.error('❌ [DATATABLE] Erreur:', error);
showAlert('danger', 'Erreur lors du chargement des données');
}
},
columns: [
{ data: 0 }, // ID
{ data: 1 }, // Type
{ data: 2 }, // Montant
{ data: 3 }, // Mode de paiement
{ data: 4 }, // Commentaire
{ data: 5 }, // Utilisateur
{ data: 6 }, // Magasin
{ data: 7 }, // Date
{ data: 8, orderable: false } // Actions
],
order: [[7, 'desc']],
pageLength: 10,
lengthMenu: [[5, 10, 25, 50, -1], [5, 10, 25, 50, "Tout"]]
});
console.log('✅ [INIT] DataTable initialisée');
// ✅ CHARGER LES STATISTIQUES DU MOIS AU DÉMARRAGE
console.log('🚀 [INIT] Appel de loadStatisticsForCurrentMonth()');
loadStatisticsForCurrentMonth();
// ✅ BOUTON FILTRER - RECHARGE TABLEAU + STATISTIQUES
$('#btnFilter').on('click', function() {
console.log('🔍 [FILTER] Bouton Filtrer cliqué');
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
var store_id = $('#filterStore').val();
console.log('📋 [FILTER] Filtres appliqués:', {
startDate: startDate,
endDate: endDate,
store_id: store_id
});
// ✅ RECHARGER LE TABLEAU
historyTable.ajax.reload();
// ✅ RECHARGER LES STATISTIQUES SELON LA PÉRIODE FILTRÉE
loadStatisticsForFilteredPeriod(startDate, endDate, store_id);
});
// ✅ BOUTON RÉINITIALISER
$('#btnReset').on('click', function() {
console.log('🔄 [RESET] Réinitialisation des filtres');
// ✅ RÉINITIALISER À LA DATE DU JOUR
var today = new Date().toISOString().split('T')[0];
$('#filterStartDate').val(today);
$('#filterEndDate').val(today);
$('#filterStore').val('');
console.log('📅 [RESET] Nouvelles dates:', {
startDate: today,
endDate: today
});
// ✅ RECHARGER LE TABLEAU
historyTable.ajax.reload();
// ✅ RECHARGER LES STATISTIQUES DU MOIS EN COURS
loadStatisticsForCurrentMonth();
});
// ✅ CHANGEMENT DE MAGASIN - RECHARGER AUTOMATIQUEMENT
$('#filterStore').on('change', function() {
var selectedStore = $(this).val();
console.log('🏪 [STORE] Changement de magasin:', selectedStore || 'Tous');
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
// ✅ RECHARGER LE TABLEAU
historyTable.ajax.reload();
// ✅ RECHARGER LES STATISTIQUES
loadStatisticsForFilteredPeriod(startDate, endDate, selectedStore);
});
// ✅ FORMULAIRE D'AJOUT
$('#formEncaissement').on('submit', function(e) {
e.preventDefault();
console.log('➕ [CREATE] Soumission du formulaire d\'ajout');
$.ajax({
url: '<?= base_url('encaissements/create') ?>',
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
beforeSend: function() {
$('button[type="submit"]').prop('disabled', true)
.html('<i class="fa fa-spinner fa-spin"></i> Enregistrement...');
},
success: function(response) {
console.log('✅ [CREATE] Réponse:', response);
if (response.success) {
showAlert('success', response.messages);
$('#formEncaissement')[0].reset();
$('#autreTypeGroup').hide();
historyTable.ajax.reload(null, false);
// ✅ RECHARGER LES STATISTIQUES APRÈS AJOUT
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
var store_id = $('#filterStore').val();
loadStatisticsForFilteredPeriod(startDate, endDate, store_id);
} else {
var errorMsg = typeof response.messages === 'object'
? Object.values(response.messages).join('<br>')
: response.messages;
showAlert('danger', errorMsg);
}
},
error: function(xhr) {
console.error('❌ [CREATE] Erreur:', xhr);
var errorMsg = 'Erreur lors de l\'enregistrement';
try {
var response = JSON.parse(xhr.responseText);
if (response.messages) {
errorMsg = typeof response.messages === 'object'
? Object.values(response.messages).join('<br>')
: response.messages;
}
} catch(e) {
errorMsg += ': ' + xhr.statusText;
}
showAlert('danger', errorMsg);
},
complete: function() {
$('button[type="submit"]').prop('disabled', false)
.html('<i class="fa fa-save"></i> Enregistrer l\'Encaissement');
}
});
});
// ✅ FORMULAIRE D'ÉDITION
$('#formEditEncaissement').on('submit', function(e) {
e.preventDefault();
var id = $('#editId').val();
console.log('✏️ [UPDATE] Soumission du formulaire d\'édition, ID:', id);
$.ajax({
url: '<?= base_url('encaissements/update/') ?>' + id,
type: 'POST',
data: $(this).serialize(),
dataType: 'json',
success: function(response) {
console.log('✅ [UPDATE] Réponse:', response);
if (response.success) {
$('#editModal').modal('hide');
showAlert('success', response.messages);
historyTable.ajax.reload(null, false);
// ✅ RECHARGER LES STATISTIQUES APRÈS MODIFICATION
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
var store_id = $('#filterStore').val();
loadStatisticsForFilteredPeriod(startDate, endDate, store_id);
} else {
var errorMsg = typeof response.messages === 'object'
? Object.values(response.messages).join('<br>')
: response.messages;
showAlert('danger', errorMsg);
}
},
error: function(xhr) {
console.error('❌ [UPDATE] Erreur:', xhr);
var errorMsg = 'Erreur lors de la modification';
try {
var response = JSON.parse(xhr.responseText);
if (response.messages) {
errorMsg = response.messages;
}
} catch(e) {
errorMsg += ': ' + xhr.statusText;
}
showAlert('danger', errorMsg);
}
});
});
console.log('✅ [INIT] Page complètement initialisée');
});
// ============================================
// AUTRES FONCTIONS (viewDetails, editEncaissement, deleteEncaissement, showAlert)
// ============================================
function viewDetails(id) {
$.ajax({
url: '<?= base_url('encaissements/details/') ?>' + id,
type: 'GET',
dataType: 'json',
success: function(response) {
if (response.success) {
var data = response.data;
$('#detailId').text(data.id);
$('#detailType').text(data.type_encaissement);
$('#detailMontant').text(new Intl.NumberFormat('fr-FR').format(data.montant) + ' Ar');
$('#detailMode').text(data.mode_paiement);
$('#detailDate').text(new Date(data.created_at).toLocaleString('fr-FR'));
$('#detailUser').text(data.user_name);
$('#detailEmail').text(data.user_email);
$('#detailStore').text(data.store_name);
$('#detailCommentaire').text(data.commentaire || 'Aucun commentaire');
$('#detailsModal').modal('show');
} else {
showAlert('danger', response.message || 'Erreur lors du chargement des détails');
}
},
error: function(xhr) {
showAlert('danger', 'Erreur lors du chargement des détails');
}
});
}
function editEncaissement(id) {
$.ajax({
url: '<?= base_url('encaissements/getEncaissement/') ?>' + id,
type: 'GET',
dataType: 'json',
success: function(response) {
if (response.success) {
var data = response.data;
$('#editId').val(data.id);
$('#editTypeEncaissement').val(data.type_encaissement);
$('#editAutreType').val(data.autre_type);
$('#editMontant').val(data.montant);
$('#editModePaiement').val(data.mode_paiement);
$('#editCommentaire').val(data.commentaire);
var predefinedTypes = ['Plastification', 'Duplicata', 'Retour Décaissement', 'Vente de Pièce'];
if (data.type_encaissement === 'Autre' || !predefinedTypes.includes(data.type_encaissement)) {
$('#editTypeEncaissement').val('Autre');
$('#editAutreTypeGroup').show();
$('#editAutreType').val(data.type_encaissement);
}
$('#editModal').modal('show');
} else {
showAlert('danger', response.message || 'Erreur lors du chargement');
}
},
error: function(xhr) {
showAlert('danger', 'Erreur lors du chargement de l\'encaissement');
}
});
}
function deleteEncaissement(id) {
Swal.fire({
title: 'Confirmation',
text: "Voulez-vous vraiment supprimer cet encaissement ?",
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
cancelButtonColor: '#3085d6',
confirmButtonText: 'Oui, supprimer',
cancelButtonText: 'Annuler'
}).then((result) => {
if (result.isConfirmed) {
console.log('🗑️ [DELETE] Suppression de l\'encaissement ID:', id);
$.ajax({
url: '<?= base_url('encaissements/delete/') ?>' + id,
type: 'POST',
dataType: 'json',
success: function(response) {
console.log('✅ [DELETE] Réponse:', response);
if (response.success) {
Swal.fire({
icon: 'success',
title: 'Supprimé',
text: response.messages,
timer: 2000,
showConfirmButton: false
});
$('#historyTable').DataTable().ajax.reload(null, false);
// ✅ RECHARGER LES STATISTIQUES APRÈS SUPPRESSION
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
var store_id = $('#filterStore').val();
loadStatisticsForFilteredPeriod(startDate, endDate, store_id);
} else {
Swal.fire({
icon: 'error',
title: 'Erreur',
text: response.messages || 'Erreur lors de la suppression'
});
}
},
error: function(xhr) {
console.error('❌ [DELETE] Erreur:', xhr);
Swal.fire({
icon: 'error',
title: 'Erreur serveur',
text: 'Impossible de supprimer cet encaissement'
});
}
});
}
});
}
function showAlert(type, message) {
var alertHtml = '<div class="alert alert-' + type + ' alert-dismissible alert-notification">' +
'<button type="button" class="close" data-dismiss="alert">&times;</button>' +
message +
'</div>';
$('.alert-notification').remove();
$('body').append(alertHtml);
setTimeout(function() {
$('.alert-notification').fadeOut('slow', function() {
$(this).remove();
});
}, 5000);
}
</script>

1120
app/Views/avances/avance.php

File diff suppressed because it is too large

492
app/Views/dashboard.php

@ -2,6 +2,231 @@
/* ============================================
VARIABLES CSS - Design System Couleurs Unies
============================================ */
.treasury-summary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
margin: 20px 0;
}
.treasury-summary .box-header {
background: rgba(255, 255, 255, 0.95);
border-bottom: 3px solid #667eea;
padding: 20px 25px;
}
.treasury-summary .box-title {
color: #667eea;
font-size: 22px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.treasury-summary .box-body {
background: #ffffff;
padding: 35px 25px;
}
/* Blocs de montants */
.amount-block {
background: #f8f9fa;
border-radius: 12px;
padding: 25px 15px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.amount-block::before {
content: '';
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: linear-gradient(90deg, transparent, currentColor, transparent);
opacity: 0;
transition: opacity 0.3s ease;
}
.amount-block:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.amount-block:hover::before {
opacity: 1;
}
/* Bloc Total Brut */
.amount-block.brut {
background: linear-gradient(135deg, #e8f5e9 0%, #c8e6c9 100%);
border-left: 5px solid #4caf50;
}
.amount-block.brut::before {
color: #4caf50;
}
/* Bloc Décaissements */
.amount-block.sorties {
background: linear-gradient(135deg, #ffebee 0%, #ffcdd2 100%);
border-left: 5px solid #f44336;
}
.amount-block.sorties::before {
color: #f44336;
}
/* Bloc Solde Net */
.amount-block.net {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-left: 5px solid #2196f3;
border: 3px solid #2196f3;
}
.amount-block.net::before {
color: #2196f3;
}
/* En-têtes des montants */
.amount-header {
font-size: 32px;
font-weight: 800;
margin: 15px 0 10px;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.05);
}
.amount-label {
font-size: 15px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 5px;
display: block;
}
.amount-sublabel {
font-size: 12px;
color: #6c757d;
font-style: italic;
}
/* Opérateurs mathématiques */
.math-operator {
font-size: 48px;
font-weight: 300;
color: #9e9e9e;
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 120px;
}
/* Séparateur stylisé */
.custom-divider {
height: 2px;
background: linear-gradient(90deg, transparent, #667eea, transparent);
margin: 30px 0;
border: none;
}
/* Détail par mode de paiement */
.payment-detail-block {
background: #ffffff;
border-radius: 10px;
padding: 20px;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
transition: all 0.3s ease;
border: 2px solid transparent;
}
.payment-detail-block:hover {
transform: scale(1.05);
border-color: currentColor;
box-shadow: 0 8px 20px rgba(0, 0, 0, 0.12);
}
.payment-detail-block.espece {
border-left: 4px solid #4caf50;
}
.payment-detail-block.mvola {
border-left: 4px solid #ff9800;
}
.payment-detail-block.banque {
border-left: 4px solid #2196f3;
}
.payment-icon {
font-size: 32px;
margin-bottom: 10px;
display: inline-block;
}
.payment-icon.espece {
color: #4caf50;
}
.payment-icon.mvola {
color: #ff9800;
}
.payment-icon.banque {
color: #2196f3;
}
.payment-amount {
font-size: 22px;
font-weight: 700;
color: #212529;
margin: 10px 0;
}
.payment-label {
font-size: 13px;
font-weight: 600;
text-transform: uppercase;
color: #6c757d;
letter-spacing: 0.5px;
}
/* Animation au chargement */
@keyframes fadeInUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.treasury-summary {
animation: fadeInUp 0.6s ease-out;
}
/* Responsive */
@media (max-width: 768px) {
.math-operator {
font-size: 32px;
min-height: 60px;
}
.amount-header {
font-size: 24px;
}
.treasury-summary .box-body {
padding: 20px 15px;
}
}
</style>
@ -79,28 +304,110 @@
</div>
<!-- Détail Orders vs Avances -->
<div class="container-fluid row" style="margin-top: 10px;">
<div class="col-lg-6 col-xs-12">
<div class="info-box bg-aqua">
<span class="info-box-icon"><i class="fa fa-shopping-cart"></i></span>
<div class="info-box-content">
<span class="info-box-text">Total Orders (Ventes complètes)</span>
<span class="info-box-number"><?php echo number_format($total_orders_only, 0, '.', ' '); ?> Ar</span>
<div class="container-fluid row" style="margin-top: 10px; margin-bottom: 20px;">
<div class="col-lg-12">
<div class="box box-success treasury-summary">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-calculator"></i> Résumé de Trésorerie
</h3>
</div>
</div>
</div>
<div class="col-lg-6 col-xs-12">
<div class="info-box bg-yellow">
<span class="info-box-icon"><i class="fa fa-clock-o"></i></span>
<div class="info-box-content">
<span class="info-box-text">Total Avances (Paiements partiels)</span>
<span class="info-box-number"><?php echo number_format($total_avances, 0, '.', ' '); ?> Ar</span>
<div class="box-body">
<!-- Calcul Principal : Brut - Sorties = Net -->
<div class="row text-center">
<!-- Total Brut Encaissé -->
<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;">
<?php echo number_format($total_brut, 0, '.', ' '); ?> Ar
</div>
<span class="amount-sublabel">Orders + Avances</span>
</div>
</div>
<!-- Opérateur Moins -->
<div class="col-sm-1 col-xs-12">
<div class="math-operator"></div>
</div>
<!-- Décaissements Payés -->
<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;">
<?php echo number_format($total_sorties, 0, '.', ' '); ?> Ar
</div>
<span class="amount-sublabel">Espèce + MVOLA + Virement</span>
</div>
</div>
<!-- Opérateur Égal -->
<div class="col-sm-1 col-xs-12">
<div class="math-operator">=</div>
</div>
<!-- Solde Net Disponible -->
<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;">
<?php echo number_format($total_caisse, 0, '.', ' '); ?> Ar
</div>
<span class="amount-sublabel">Disponible en caisse</span>
</div>
</div>
</div>
<!-- Séparateur Stylisé -->
<hr class="custom-divider">
<!-- Détail par Mode de Paiement -->
<div class="row text-center">
<!-- Espèce -->
<div class="col-sm-4 col-xs-12" style="margin-bottom: 15px;">
<div class="payment-detail-block espece">
<div class="payment-icon espece">
<i class="fa fa-money"></i>
</div>
<div class="payment-amount">
<?php echo number_format($total_espece_caisse, 0, '.', ' '); ?> Ar
</div>
<div class="payment-label">💵 Espèce Disponible</div>
</div>
</div>
<!-- MVOLA -->
<div class="col-sm-4 col-xs-12" style="margin-bottom: 15px;">
<div class="payment-detail-block mvola">
<div class="payment-icon mvola">
<i class="fa fa-mobile"></i>
</div>
<div class="payment-amount">
<?php echo number_format($total_mvola_caisse, 0, '.', ' '); ?> Ar
</div>
<div class="payment-label">📱 MVOLA Disponible</div>
</div>
</div>
<!-- Banque -->
<div class="col-sm-4 col-xs-12" style="margin-bottom: 15px;">
<div class="payment-detail-block banque">
<div class="payment-icon banque">
<i class="fa fa-bank"></i>
</div>
<div class="payment-amount">
<?php echo number_format($total_vb_caisse, 0, '.', ' '); ?> Ar
</div>
<div class="payment-label">🏦 Banque Disponible</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- 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">
@ -217,7 +524,7 @@
var caissierTable;
$(document).ready(function () {
console.log('🔍 Initialisation du tableau caissier...');
// console.log('🔍 Initialisation du tableau caissier...');
// Configuration DataTable pour caissier
caissierTable = $('#caissierperf').DataTable({
@ -288,7 +595,7 @@
const startDate = $('#startDateCaissier').val();
const endDate = $('#endDateCaissier').val();
console.log('🔍 Filtrage:', startDate, endDate);
// console.log('🔍 Filtrage:', startDate, endDate);
// Recharger les données avec les nouveaux paramètres
caissierTable.ajax.reload();
@ -670,79 +977,86 @@
<div class="col-12">
<!-- CARD COMMERCIAL -->
<div class="card shadow-sm border-0 mb-4">
<!-- CARD COMMERCIAL -->
<div class="card shadow-sm border-0 mb-4">
<div class="card-body">
<div class="row mt-4">
<div class="col-md-12 col-lg-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-white text-primary font-weight-bold">
<h3 class="card-title m-0">
<i class="fa fa-users"></i> Performances des commerciaux
</h3>
</div>
<div class="card-body">
<div class="row mt-4">
<div class="col-md-12 col-lg-12">
<div class="card shadow-sm border-0">
<div class="card-header bg-white text-primary font-weight-bold">
<h3 class="card-title m-0">
<i class="fa fa-users"></i> Performances des commercials
</h3>
</div>
<div class="card-body">
<!-- Filtres Commercial - AVEC IDs UNIQUES -->
<div class="row g-3 align-items-center mb-4" style="margin: 5px 0 5px 5px;">
<div class="col-md-4 d-flex align-items-end">
<br>
<button id="filterBtnComm" class="btn btn-primary" style="margin-right: 5px;">
<i class="fa fa-filter"></i> Filtrer
</button>
<button id="resetFilterComm" class="btn btn-warning" style="margin-right: 5px;">
<i class="fa fa-refresh"></i> Aujourd'hui
</button>
<button id="exportBtnComm" class="btn btn-success">
<i class="fa fa-file-excel-o"></i> Exporter
</button>
</div>
<div class="col-md-3">
<label for="pventeComm" class="form-label">Points de ventes</label>
<select id="pventeComm" class="form-control">
<option value="TOUS">TOUS</option>
<?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">
<br>
<button id="filterBtnComm" class="btn btn-primary w-100">
<i class="fa fa-filter"></i> Filtrer
</button>
<button id="exportBtnComm" class="btn btn-success w-100" style="margin-left: 5px;">
<i class="fa fa-file-excel-o"></i> Exporter
</button>
</div>
</div>
<!-- Filtres Commercial -->
<div class="row g-3 align-items-center mb-4" style="margin:5px;">
<table id="commperformance" class="table table-hover table-striped table-bordered">
<thead>
<tr>
<th>Nom et prénom</th>
<th>Email</th>
<th>Motos vendue</th>
<th>Date de vente</th>
<th>Prix d'achat</th>
<th>Prix de vente</th>
<th>Point de ventes</th>
<th>Bénefices</th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="5" style="text-align:right; font-weight: bold;">Total :</th>
<th style="font-weight: bold;"></th>
<th></th>
<th style="font-weight: bold;"></th>
</tr>
</tfoot>
</table>
</div>
</div>
<div class="col-md-3">
<label for="startDateComm" class="form-label">Date de début</label>
<input type="date" id="startDateComm" class="form-control">
</div>
<div class="col-md-3">
<label for="endDateComm" class="form-label">Date de fin</label>
<input type="date" id="endDateComm" class="form-control">
</div>
<div class="col-md-3">
<label for="pventeComm" class="form-label">Points de ventes</label>
<select id="pventeComm" class="form-control">
<option value="TOUS">TOUS</option>
<?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">
<button id="filterBtnComm" class="btn btn-primary w-100">
<i class="fa fa-filter"></i> Filtrer
</button>
<button id="resetFilterComm" class="btn btn-warning w-100" style="margin-left:5px;">
<i class="fa fa-refresh"></i> Aujourd'hui
</button>
<button id="exportBtnComm" class="btn btn-success w-100" style="margin-left:5px;">
<i class="fa fa-file-excel-o"></i> Exporter
</button>
</div>
</div>
<!-- TABLE -->
<table id="commperformance" class="table table-hover table-striped table-bordered">
<thead>
<tr>
<th>Nom et prénom</th>
<th>Email</th>
<th>Motos vendue</th>
<th>Date de vente</th>
<th>Prix d'achat</th>
<th>Prix de vente</th>
<th>Point de ventes</th>
<th>Bénéfices</th>
</tr>
</thead>
<tfoot>
<tr>
<th colspan="5" class="text-end fw-bold">Total :</th>
<th class="fw-bold"></th>
<th></th>
<th class="fw-bold"></th>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- CARD MÉCANICIEN -->
<div class="card shadow-sm border-0">
@ -905,7 +1219,7 @@ $(document).ready(function () {
// ✅ BOUTON FILTRAGE COMMERCIAL
$('#filterBtnComm').on('click', function () {
console.log('🔍 Filtrage Commercial:', {
('🔍 Filtrage Commercial:', {
startDate: $('#startDateComm').val(),
endDate: $('#endDateComm').val(),
pvente: $('#pventeComm').val()
@ -956,7 +1270,7 @@ $(document).ready(function () {
// ✅ BOUTON FILTRAGE MÉCANICIEN
$('#filterBtnMec').on('click', function () {
console.log('🔍 Filtrage Mécanicien:', {
('🔍 Filtrage Mécanicien:', {
startDate: $('#startDateMec').val(),
endDate: $('#endDateMec').val(),
pvente: $('#pventeMec').val()
@ -1280,7 +1594,7 @@ $(document).ready(function () {
let brand = order.name;
brandCount[brand] = (brandCount[brand] || 0) + 1;
});
console.log(brandCount);
// console.log(brandCount);
// Step 2: Convert to array and sort by count (descending)
let sortedBrands = Object.entries(brandCount)
@ -1290,7 +1604,7 @@ $(document).ready(function () {
// Step 3: Prepare data for the chart
let labels = sortedBrands.map(item => item[0]); // Brand names
let data = sortedBrands.map(item => item[1]); // Order counts
console.log(labels);
// console.log(labels);
// Step 4: Create the Pie Chart
let ctx2 = document.getElementById('MotosChart').getContext('2d');

28
app/Views/groups/edit.php

@ -459,7 +459,33 @@
}
} ?>></td>
</tr>
<tr>
<td>Autres encaissements</td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="createEncaissement"
<?php if ($serialize_permission) {
if (in_array('createEncaissement', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="updateEncaissement"
<?php if ($serialize_permission) {
if (in_array('updateEncaissement', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="viewEncaissement"
<?php if ($serialize_permission) {
if (in_array('viewEncaissement', $serialize_permission)) {
echo "checked";
}
} ?>></td>
<td><input type="checkbox" name="permission[]" id="permission" class="minimal" value="deleteEncaissement"
<?php if ($serialize_permission) {
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"

571
app/Views/mecanicien/index.php

@ -34,6 +34,90 @@
<button class="btn btn-primary" data-toggle="modal" data-target="#addModal">Ajouter une réparation</button>
<br /> <br />
<?php endif; ?>
<!-- ✅ SECTION FILTRES AVEC MÉCANICIENS -->
<div class="box">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-filter"></i> Filtres</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-3">
<label for="filterStartDate">Date de début</label>
<input type="date" id="filterStartDate" class="form-control">
</div>
<div class="col-md-3">
<label for="filterEndDate">Date de fin</label>
<input type="date" id="filterEndDate" class="form-control">
</div>
<!-- ✅ FILTRE PAR MÉCANICIEN -->
<div class="col-md-3">
<label for="filterMecanic">Mécanicien</label>
<select id="filterMecanic" class="form-control">
<option value="">Tous les mécaniciens</option>
<?php if (isset($users) && is_array($users)): ?>
<?php foreach ($users as $user): ?>
<option value="<?= $user['id'] ?>">
<?= $user['firstname'] . ' ' . $user['lastname'] ?>
</option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-3">
<label>&nbsp;</label><br>
<button id="btnFilter" class="btn btn-primary">
<i class="fa fa-filter"></i> Filtrer
</button>
<button id="btnReset" class="btn btn-warning">
<i class="fa fa-refresh"></i> Réinitialiser
</button>
</div>
</div>
</div>
</div>
<!-- ✅ STATISTIQUES PAR MÉCANICIEN -->
<div class="row" id="statsContainer">
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box bg-aqua">
<span class="info-box-icon"><i class="fa fa-wrench"></i></span>
<div class="info-box-content">
<span class="info-box-text">Total Réparations</span>
<span class="info-box-number" id="statTotal">0</span>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box bg-yellow">
<span class="info-box-icon"><i class="fa fa-clock-o"></i></span>
<div class="info-box-content">
<span class="info-box-text">En Cours</span>
<span class="info-box-number" id="statEnCours">0</span>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box bg-green">
<span class="info-box-icon"><i class="fa fa-check"></i></span>
<div class="info-box-content">
<span class="info-box-text">Réparées</span>
<span class="info-box-number" id="statRepare">0</span>
</div>
</div>
</div>
<div class="col-md-3 col-sm-6 col-xs-12">
<div class="info-box bg-red">
<span class="info-box-icon"><i class="fa fa-times"></i></span>
<div class="info-box-content">
<span class="info-box-text">Non Réparées</span>
<span class="info-box-number" id="statNonRepare">0</span>
</div>
</div>
</div>
</div>
<!-- ✅ TABLEAU -->
<div class="box">
<div class="box-header">
<h3 class="box-title">Gérer les Réparations</h3>
@ -45,7 +129,8 @@
<tr>
<th>Image</th>
<th>Motos</th>
<th>Username</th>
<th>Mécanicien</th>
<th>Magasin</th>
<th>Statut</th>
<th>Observation</th>
<th>Date de début</th>
@ -55,273 +140,325 @@
<?php endif; ?>
</tr>
</thead>
</table>
</div>
</div>
</div>
</div>
</section>
</div>
<?php if (in_array('createMecanicien', $user_permission)): ?>
<!-- create brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="addModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Ajouter une moto pour réparation</h4>
</div>
<form role="form" action="<?php echo base_url('mecanicien/create') ?>" method="post" id="createForm">
<div class="modal-body">
<div class="form-group">
<label for="motos">Nom du motos</label>
<!-- <select class="form-control" id="motos" name="motos" style="width: 100%;">
<//?php foreach ($moto as $k => $v): ?>
<option value="<//?php echo $v['id'] ?>"><//?php echo $v['name'] ?></option>
<//?php endforeach ?>
</select> -->
<select name="motos" class="form-control select2" id="moto_select" style="width: 100%;"
style="width:100%;" required>
<option value=""></option>
<?php foreach ($moto as $k => $v): ?>
<?php if ($v['product_sold'] == false): ?>
<option value="<?php echo $v['id'] ?>">
<?= $v['sku'] . ' | ' . $v['name'] . ' | ' . $v['numero_de_moteur'] . ' | ' . $v['puissance'] ?>
</option>
<!-- <//?php else: ?>
<option value="<?php echo $v['id'] ?>" disabled><?php echo $v['name'] ?> <span style="background-color: #dc3545; color: #ffffff; padding: 2px 5px; border-radius: 5px;"> (Rupture de stock)</span></option>
<?php endif; ?> -->
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="mecano">Nom du mecanicien</label>
<select class="form-control select2" id="moto_select2" name="mecano" style="width: 100%;">
<?php foreach ($users as $k => $v): ?>
<option value="<?php echo $v['id'] ?>"><?php echo $v['firstname'] . " " . $v['lastname'] ?></option>
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="statut">Status</label>
<select class="form-control" id="statut" name="statut" required>
<option value="1">En cours de réparation</option>
<option value="2">Réparer</option>
<option value="3">Non réparer</option>
</select>
</div>
</div>
<!-- Vos modaux existants (addModal, removeModal, editModal) restent inchangés -->
<?php if (in_array('createMecanicien', $user_permission)): ?>
<!-- create brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="addModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Ajouter une moto pour réparation</h4>
</div>
<div class="form-group">
<label for="observation">Observation</label>
<textarea class="form-control" name="observation" id="observation" cols="30" rows="3"
placeholder="votre observation"></textarea>
</div>
<form role="form" action="<?php echo base_url('mecanicien/create') ?>" method="post" id="createForm">
<div class="form-group">
<label for="date_debut">Date de debut</label>
<input type="date" name="date_debut" id="date_debut" class="form-control" required>
</div>
<div class="modal-body">
<div class="form-group">
<label for="date_fin">Date de fin</label>
<input type="date" name="date_fin" id="date_fin" class="form-control" required>
</div>
<div class="form-group">
<label for="motos">Nom du motos</label>
<select name="motos" class="form-control select2" id="moto_select" style="width: 100%;" required>
<option value=""></option>
<?php foreach ($moto as $k => $v): ?>
<?php if ($v['product_sold'] == false): ?>
<option value="<?php echo $v['id'] ?>">
<?= $v['sku'] . ' | ' . $v['name'] . ' | ' . $v['numero_de_moteur'] . ' | ' . $v['puissance'] ?>
</option>
<?php endif; ?>
<?php endforeach ?>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
<div class="form-group">
<label for="mecano">Nom du mecanicien</label>
<select class="form-control select2" id="moto_select2" name="mecano" style="width: 100%;">
<?php foreach ($users as $k => $v): ?>
<option value="<?php echo $v['id'] ?>"><?php echo $v['firstname'] . " " . $v['lastname'] ?></option>
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="statut">Status</label>
<select class="form-control" id="statut" name="statut" required>
<option value="1">En cours de réparation</option>
<option value="2">Réparer</option>
<option value="3">Non réparer</option>
</select>
</div>
</form>
<div class="form-group">
<label for="observation">Observation</label>
<textarea class="form-control" name="observation" id="observation" cols="30" rows="3"
placeholder="votre observation"></textarea>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<div class="form-group">
<label for="date_debut">Date de debut</label>
<input type="date" name="date_debut" id="date_debut" class="form-control" required>
</div>
<?php if (in_array('deleteMecanicien', $user_permission)): ?>
<!-- remove brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="removeModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Supprimer la réparation</h4>
<div class="form-group">
<label for="date_fin">Date de fin</label>
<input type="date" name="date_fin" id="date_fin" class="form-control" required>
</div>
</div>
<form role="form" action="<?php echo base_url('mecanicien/delete') ?>" method="post" id="removeForm">
<div class="modal-body">
<p>Voulez-vous vraiment supprimer ?</p>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Oui</button>
</div>
</form>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<?php if (in_array('updateMecanicien', $user_permission)): ?>
<!-- edit brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="editModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Mise à jour du réparation</h4>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<form role="form" action="<?php echo base_url('mecanicien/update') ?>" method="post" id="updateForm">
<?php if (in_array('deleteMecanicien', $user_permission)): ?>
<!-- remove brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="removeModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Supprimer la réparation</h4>
</div>
<div class="modal-body">
<form role="form" action="<?php echo base_url('mecanicien/delete') ?>" method="post" id="removeForm">
<div class="modal-body">
<p>Voulez-vous vraiment supprimer ?</p>
<div class="form-group">
<label for="motos">Nom du motos</label>
<select class="form-control" id="motos_edit" name="motos" style="width: 100%;" required>
<?php foreach ($moto as $k => $v): ?>
<?php if ($v['product_sold'] == false): ?>
<option value="<?php echo $v['id'] ?>">
<?= $v['sku'] . ' | ' . $v['name'] . ' | ' . $v['numero_de_moteur'] . ' | ' . $v['puissance'] ?>
</option>
<!-- <//?php else: ?>
<option value="<?php echo $v['id'] ?>" disabled><?php echo $v['name'] ?> <span style="background-color: #dc3545; color: #ffffff; padding: 2px 5px; border-radius: 5px;"> (Rupture de stock)</span></option>
<?php endif; ?> -->
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="mecano">Nom du mecanicien</label>
<select class="mecanic" id="mecano" name="mecano" style="width: 100%;">
<?php foreach ($users as $k => $v): ?>
<option value="<?php echo $v['id'] ?>"><?php echo $v['firstname'] . " " . $v['lastname'] ?></option>
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="statut">Status</label>
<select class="form-control" id="statut_edit" name="statut" required>
<option value="1">En cours de réparation</option>
<option value="2">Réparer</option>
<option value="3">Non réparer</option>
</select>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Oui</button>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<?php if (in_array('updateMecanicien', $user_permission)): ?>
<!-- edit brand modal -->
<div class="modal fade" tabindex="-1" role="dialog" id="editModal">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title">Mise à jour du réparation</h4>
</div>
<div class="form-group">
<label for="observation">Observation</label>
<textarea class="form-control" name="observation" id="observation_edit" cols="30" rows="3"
placeholder="votre observation"></textarea>
</div>
<form role="form" action="<?php echo base_url('mecanicien/update') ?>" method="post" id="updateForm">
<div class="form-group">
<label for="date_debut">Date de debut</label>
<input type="date" name="date_debut" id="date_debut_edit" class="form-control" required>
</div>
<div class="modal-body">
<div class="form-group">
<label for="date_fin">Date de fin</label>
<input type="date" name="date_fin" id="date_fin_edit" class="form-control" required>
</div>
<div class="form-group">
<label for="motos">Nom du motos</label>
<select class="form-control" id="motos_edit" name="motos" style="width: 100%;" required>
<?php foreach ($moto as $k => $v): ?>
<?php if ($v['product_sold'] == false): ?>
<option value="<?php echo $v['id'] ?>">
<?= $v['sku'] . ' | ' . $v['name'] . ' | ' . $v['numero_de_moteur'] . ' | ' . $v['puissance'] ?>
</option>
<?php endif; ?>
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="mecano">Nom du mecanicien</label>
<select class="mecanic" id="mecano" name="mecano" style="width: 100%;">
<?php foreach ($users as $k => $v): ?>
<option value="<?php echo $v['id'] ?>"><?php echo $v['firstname'] . " " . $v['lastname'] ?></option>
<?php endforeach ?>
</select>
</div>
<div class="form-group">
<label for="statut">Status</label>
<select class="form-control" id="statut_edit" name="statut" required>
<option value="1">En cours de réparation</option>
<option value="2">Réparer</option>
<option value="3">Non réparer</option>
</select>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
<div class="form-group">
<label for="observation">Observation</label>
<textarea class="form-control" name="observation" id="observation_edit" cols="30" rows="3"
placeholder="votre observation"></textarea>
</div>
</form>
<div class="form-group">
<label for="date_debut">Date de debut</label>
<input type="date" name="date_debut" id="date_debut_edit" class="form-control" required>
</div>
<div class="form-group">
<label for="date_fin">Date de fin</label>
<input type="date" name="date_fin" id="date_fin_edit" class="form-control" required>
</div>
</div>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">Fermer</button>
<button type="submit" class="btn btn-primary">Enregistrer</button>
</div>
</form>
</div><!-- /.modal-content -->
</div><!-- /.modal-dialog -->
</div><!-- /.modal -->
<?php endif; ?>
<script>
$(document).ready(function () {
$('#addModal').on('shown.bs.modal', function () {
// Only initialize if not already done
if (!$('#moto_select').hasClass("select2-hidden-accessible")) {
$('#moto_select').select2({
dropdownParent: $('#addModal'),
width: '100%' // Ensure it takes full width
width: '100%'
});
}
if (!$('#moto_select2').hasClass("select2-hidden-accessible")) {
$('#moto_select2').select2({
dropdownParent: $('#addModal'),
width: '100%' // Ensure it takes full width
width: '100%'
});
}
});
$("#motos_edit").select2();
$('.mecanic').select2();
$("#mecanicNav").addClass('active');
// initialize the datatable
// datatable-fr.js
$.extend(true, $.fn.dataTable.defaults, {
language: {
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sLengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
sInfo: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
sInfoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ment",
// ✅ INITIALISER LES DATES À AUJOURD'HUI
var today = new Date().toISOString().split('T')[0];
$('#filterStartDate').val(today);
$('#filterEndDate').val(today);
// Configuration DataTable en français
$.extend(true, $.fn.dataTable.defaults, {
language: {
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sLengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
sInfo: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
sInfoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ment",
sInfoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
sLoadingRecords: "Chargement en cours...",
sZeroRecords: "Aucun &eacute;l&eacute;ment &agrave; afficher",
sEmptyTable: "Aucune donn&eacute;e disponible dans le tableau",
sZeroRecords: "Aucun &eacute;l&eacute;ment &agrave; afficher",
sEmptyTable: "Aucune donn&eacute;e disponible dans le tableau",
oPaginate: {
sFirst: "Premier",
sPrevious: "Pr&eacute;c&eacute;dent",
sNext: "Suivant",
sLast: "Dernier"
},
oAria: {
sSortAscending: ": activer pour trier la colonne par ordre croissant",
sSortDescending: ": activer pour trier la colonne par ordre d&eacute;croissant"
sFirst: "Premier",
sPrevious: "Pr&eacute;c&eacute;dent",
sNext: "Suivant",
sLast: "Dernier"
}
}
});
// ✅ FONCTION POUR CALCULER LES STATISTIQUES
function updateStatistics(data) {
let total = data.length;
let enCours = 0;
let repare = 0;
let nonRepare = 0;
data.forEach(function(row) {
// Le statut est dans la colonne index 4
let statusHTML = row[4];
if (statusHTML.includes('label-warning')) {
enCours++;
} else if (statusHTML.includes('label-success')) {
repare++;
} else if (statusHTML.includes('label-danger')) {
nonRepare++;
}
});
$('#statTotal').text(total);
$('#statEnCours').text(enCours);
$('#statRepare').text(repare);
$('#statNonRepare').text(nonRepare);
}
});
// ✅ INITIALISATION DATATABLE AVEC FILTRES
manageTable = $('#manageTable').DataTable({
'ajax': `<?= base_url('mecanicien/fetchMecanicien') ?>`,
'order': []
});
ajax: {
url: '<?= base_url('mecanicien/fetchMecanicien') ?>',
type: 'GET',
data: function(d) {
d.startDate = $('#filterStartDate').val();
d.endDate = $('#filterEndDate').val();
d.mecanic_id = $('#filterMecanic').val();
console.log('📤 Filtres envoyés:', {
startDate: d.startDate,
endDate: d.endDate,
mecanic_id: d.mecanic_id
});
},
dataSrc: function(json) {
console.log('📥 Données reçues:', json.data.length, 'réparations');
updateStatistics(json.data);
return json.data;
},
error: function(xhr, error, code) {
console.error('❌ Erreur DataTables:', error);
}
},
order: []
});
// ✅ BOUTON FILTRER
$('#btnFilter').on('click', function() {
console.log('🔍 Bouton Filtrer cliqué');
console.log('Dates:', $('#filterStartDate').val(), 'à', $('#filterEndDate').val());
console.log('Mécanicien ID:', $('#filterMecanic').val());
manageTable.ajax.reload();
});
// ✅ BOUTON RÉINITIALISER
$('#btnReset').on('click', function() {
console.log('🔄 Réinitialisation des filtres');
// submit the create from
var today = new Date().toISOString().split('T')[0];
$('#filterStartDate').val(today);
$('#filterEndDate').val(today);
$('#filterMecanic').val('');
manageTable.ajax.reload();
});
// submit the create form
$("#createForm").unbind('submit').on('submit', function () {
var form = $(this);
// remove the text-danger
$(".text-danger").remove();
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: form.serialize(), // /converting the form data into array and sending it to server
data: form.serialize(),
dataType: 'json',
success: function (response) {
manageTable.ajax.reload(null, false);
if (response.success === true) {
@ -330,27 +467,18 @@ $.extend(true, $.fn.dataTable.defaults, {
'<strong> <span class="glyphicon glyphicon-ok-sign"></span> </strong>' + response.messages +
'</div>');
// hide the modal
$("#addModal").modal('hide');
// reset the form
$("#createForm")[0].reset();
$("#createForm .form-group").removeClass('has-error').removeClass('has-success');
} else {
if (response.messages instanceof Object) {
$.each(response.messages, function (index, value) {
var id = $("#" + index);
id.closest('.form-group')
.removeClass('has-error')
.removeClass('has-success')
.addClass(value.length > 0 ? 'has-error' : 'has-success');
id.after(value);
});
} else {
$("#messages").html('<div class="alert alert-warning alert-dismissible" role="alert">' +
@ -365,7 +493,7 @@ $.extend(true, $.fn.dataTable.defaults, {
return false;
});
})
});
// edit function
function editFunc(id) {
@ -377,28 +505,25 @@ $.extend(true, $.fn.dataTable.defaults, {
console.log(response);
let date1 = new Date(response.reparation_debut);
let date2 = new Date(response.reparation_fin);
let formattedDate1 = date1.toLocaleDateString("fr-CA").split("T")[0]; // Convert to YYYY-MM-DD
let formattedDate2 = date2.toLocaleDateString("fr-CA").split("T")[0]; // Convert to YYYY-MM-DD
let formattedDate1 = date1.toLocaleDateString("fr-CA").split("T")[0];
let formattedDate2 = date2.toLocaleDateString("fr-CA").split("T")[0];
$("#motos_edit").val(response.produit_id).change();
$("#mecano").val(response.user_id).trigger('change');
$("#statut_edit").val(response.reparation_statut);
$("#observation_edit").val(response.reparation_observation);
$("#date_debut_edit").val(formattedDate1);
$("#date_fin_edit").val(formattedDate2);
// submit the edit from
$("#updateForm").unbind('submit').bind('submit', function () {
var form = $(this);
// remove the text-danger
$(".text-danger").remove();
$.ajax({
url: form.attr('action').replace(/\/?$/, '/') + id,
type: form.attr('method'),
data: form.serialize(), // /converting the form data into array and sending it to server
data: form.serialize(),
dataType: 'json',
success: function (response) {
manageTable.ajax.reload(null, false);
if (response.success === true) {
@ -407,25 +532,17 @@ $.extend(true, $.fn.dataTable.defaults, {
'<strong> <span class="glyphicon glyphicon-ok-sign"></span> </strong>' + response.messages +
'</div>');
// hide the modal
$("#editModal").modal('hide');
// reset the form
$("#updateForm .form-group").removeClass('has-error').removeClass('has-success');
} else {
if (response.messages instanceof Object) {
$.each(response.messages, function (index, value) {
var id = $("#" + index);
id.closest('.form-group')
.removeClass('has-error')
.removeClass('has-success')
.addClass(value.length > 0 ? 'has-error' : 'has-success');
id.after(value);
});
} else {
$("#messages").html('<div class="alert alert-warning alert-dismissible" role="alert">' +
@ -439,7 +556,6 @@ $.extend(true, $.fn.dataTable.defaults, {
return false;
});
}
});
}
@ -447,21 +563,15 @@ $.extend(true, $.fn.dataTable.defaults, {
function removeFunc(id) {
if (id) {
$("#removeForm").on('submit', function () {
var form = $(this);
// remove the text-danger
$(".text-danger").remove();
$.ajax({
url: form.attr('action'),
type: form.attr('method'),
data: {
reparation_id: id
},
data: { reparation_id: id },
dataType: 'json',
success: function (response) {
manageTable.ajax.reload(null, false);
if (response.success === true) {
@ -470,11 +580,8 @@ $.extend(true, $.fn.dataTable.defaults, {
'<strong> <span class="glyphicon glyphicon-ok-sign"></span> </strong>' + response.messages +
'</div>');
// hide the modal
$("#removeModal").modal('hide');
} else {
$("#messages").html('<div class="alert alert-warning alert-dismissible" role="alert">' +
'<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>' +
'<strong> <span class="glyphicon glyphicon-exclamation-sign"></span> </strong>' + response.messages +

217
app/Views/orders/createbyid.php

@ -145,58 +145,66 @@
<br /> <br />
<table class="table table-bordered" id="product_info_table">
<thead>
<tr>
<th style="width:40%">Produit</th>
<th style="width:15%">Puissance (CC)</th>
<th style="width:15%">Prix unitaire</th>
<th style="width:20%">Montant</th>
<th style="width:10%"></th>
</tr>
</thead>
<tbody>
<tr id="row_1">
<td>
<select class="form-control select_group product" data-row-id="row_1"
id="product_1" name="product[]" style="width:100%;"
onchange="getProductData(1)" required>
<option value="<?= $products['id'] ?>">
<?= $products['sku'] . ' | ' . $products['name'] . ' | ' . $products['numero_de_moteur'] ?>
</option>
</select>
</td>
<td>
<input type="number" name="puissance[]" id="puissance_1"
class="form-control" placeholder="CC"
value="<?= esc($products['puissance'] ?? '1') ?>"
min="1" autocomplete="off">
</td>
<td>
<input type="text" name="rate[]" id="rate_1" class="form-control" disabled
autocomplete="off" value="<?= esc($pu) ?>" min="0">
<input type="hidden" name="rate_value[]" id="rate_value_1"
value="<?= esc($pu) ?>" class="form-control" autocomplete="off">
<input type="hidden" id="min_price_1" name="min_price[]" value="">
</td>
<td>
<input type="text" name="amount[]" id="amount_1" class="form-control"
disabled autocomplete="off" min="0">
<input type="hidden" name="amount_value[]" id="amount_value_1"
class="form-control" autocomplete="off">
</td>
<td>
<button type="button" class="btn btn-default" onclick="removeRow('1')">
<i class="fa fa-close"></i>
</button>
</td>
</tr>
</tbody>
</table>
<thead>
<tr>
<th style="width:35%">Produit</th>
<!-- ✅ COLONNE QUANTITÉ (cachée par défaut) -->
<th class="qty-column" style="width:10%; display:none;">Quantité</th>
<th style="width:12%">Puissance (CC)</th>
<th style="width:15%">Prix unitaire</th>
<th style="width:18%">Montant</th>
<th style="width:10%"><button type="button" class="btn btn-default" id="add_row"><i class="fa fa-plus"></i></button></th>
</tr>
</thead>
<tbody>
<tr id="row_1">
<td>
<select class="form-control select_group product" data-row-id="row_1"
id="product_1" name="product[]" style="width:100%;"
onchange="getProductData(1)" required>
<option value="<?= $products['id'] ?>">
<?= $products['sku'] . ' | ' . $products['name'] . ' | ' . $products['numero_de_moteur'] ?>
</option>
</select>
</td>
<!-- ✅ CHAMP QUANTITÉ (caché par défaut) -->
<td class="qty-column" style="display:none;">
<input type="number" name="qty[]" id="qty_1"
class="form-control numeric-input"
value="1" min="1" disabled autocomplete="off">
</td>
<td>
<input type="number" name="puissance[]" id="puissance_1"
class="form-control" placeholder="CC"
value="<?= esc($products['puissance'] ?? '1') ?>"
min="1" autocomplete="off">
</td>
<td>
<input type="text" name="rate[]" id="rate_1" class="form-control" disabled
autocomplete="off" value="<?= esc($pu) ?>" min="0">
<input type="hidden" name="rate_value[]" id="rate_value_1"
value="<?= esc($pu) ?>" class="form-control" autocomplete="off">
<input type="hidden" id="min_price_1" name="min_price[]" value="">
</td>
<td>
<input type="text" name="amount[]" id="amount_1" class="form-control"
disabled autocomplete="off" min="0">
<input type="hidden" name="amount_value[]" id="amount_value_1"
class="form-control" autocomplete="off">
</td>
<td>
<button type="button" class="btn btn-default" onclick="removeRow('1')">
<i class="fa fa-close"></i>
</button>
</td>
</tr>
</tbody>
</table>
<!-- ✅ Champ qty caché (toujours = 1) -->
<input type="hidden" name="qty[]" value="1" id="qty_1" class="form-control">
@ -254,10 +262,32 @@
<script type="text/javascript">
var base_url = "<?php echo base_url(); ?>";
// ✅ Fonction pour valider les nombres positifs
// ✅ Fonction pour afficher/masquer la colonne quantité
function toggleQuantityColumn() {
var customerType = $('#customer_type').val();
if (customerType === 'revendeur') {
$('.qty-column').show();
$('input[name="qty[]"]').prop('disabled', false);
} else {
$('.qty-column').hide();
$('input[name="qty[]"]').val(1).prop('disabled', true);
recalculateAllRows();
}
}
// ✅ Fonction pour recalculer tous les montants
function recalculateAllRows() {
var tableProductLength = $("#product_info_table tbody tr").length;
for (var i = 1; i <= tableProductLength; i++) {
if ($("#row_" + i).length > 0) {
getTotal(i);
}
}
}
function validatePositiveNumber(input) {
let value = parseFloat(input.value);
if (isNaN(value) || value < 0) {
input.value = '';
return false;
@ -265,20 +295,17 @@
return true;
}
// ✅ Fonction pour afficher l'erreur sur le champ prix demandé
function showDiscountError(message) {
$('#discount_error_text').text(message);
$('#discount_error').slideDown(200);
$('#discount').addClass('border-danger');
}
// ✅ Fonction pour masquer l'erreur sur le champ prix demandé
function hideDiscountError() {
$('#discount_error').slideUp(200);
$('#discount').removeClass('border-danger');
}
// ✅ Fonction pour valider que le prix demandé ne dépasse pas le prix affiché
function validatePrixDemande(input) {
let prixDemande = parseFloat(input.value);
let prixAffiche = parseFloat($('#gross_amount').val()) || 0;
@ -302,7 +329,6 @@
return true;
}
// ✅ Fonction pour empêcher la saisie de caractères négatifs
function preventNegativeInput(e) {
if (e.key === '-' || e.key === 'e' || e.key === 'E' || e.key === '+') {
e.preventDefault();
@ -311,18 +337,34 @@
}
$(document).ready(function () {
// ✅ INITIALISATION : Calculer le total au chargement
getTotal(1);
// ✅ INITIALISATION : S'assurer que rate_value_1 est bien rempli
$("#rate_value_1").val($("#rate_1").val());
$(".select_group").select2();
$("#mainOrdersNav").addClass('active');
$("#addOrderNav").addClass('active');
// ✅ Appliquer la validation sur tous les champs numériques
// ✅ Initialiser l'affichage selon le type de client
toggleQuantityColumn();
// ✅ Écouter les changements du type de client
$('#customer_type').on('change', function() {
toggleQuantityColumn();
});
// ✅ Gérer les changements de quantité
$(document).on('input', 'input[name="qty[]"]', function() {
var row_id = $(this).attr('id').replace('qty_', '');
var qty = parseInt($(this).val()) || 1;
if (qty < 1) {
$(this).val(1);
qty = 1;
}
getTotal(row_id);
});
$(document).on('keydown', '.numeric-input, input[type="number"]', function(e) {
preventNegativeInput(e);
});
@ -331,7 +373,6 @@
validatePositiveNumber(this);
});
// ✅ Validation sur le champ discount
$("#discount").on('input', function() {
validatePrixDemande(this);
});
@ -341,7 +382,6 @@
checkMinimalPrice();
});
// Bloquer la soumission du formulaire
$('form').on('submit', function(e) {
let hasNegative = false;
$('.numeric-input, input[type="number"]').each(function() {
@ -363,9 +403,7 @@
}
});
// Add new row in the table
$("#add_row").unbind('click').bind('click', function () {
var table = $("#product_info_table");
var count_table_tbody_tr = $("#product_info_table tbody tr").length;
var row_id = count_table_tbody_tr + 1;
@ -374,18 +412,21 @@
type: 'post',
dataType: 'json',
success: function (response) {
var customerType = $('#customer_type').val();
var qtyColumnVisible = (customerType === 'revendeur') ? '' : 'style="display:none;"';
var qtyDisabled = (customerType === 'revendeur') ? '' : 'disabled';
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 + ')">' +
'<option value=""></option>';
$.each(response, function (index, value) {
// ✅ NE PLUS AFFICHER LA PUISSANCE DANS LE SELECT
html += '<option value="' + value.id + '">' + value.sku + ' | ' + value.name + ' | ' + value.numero_de_moteur + '</option>';
});
html += '</select>' +
'</td>' +
// ✅ COLONNE PUISSANCE
html += '</select></td>' +
'<td class="qty-column" ' + qtyColumnVisible + '><input type="number" name="qty[]" id="qty_' + row_id + '" class="form-control numeric-input" min="1" value="1" ' + qtyDisabled + ' autocomplete="off"></td>' +
'<td><input type="number" name="puissance[]" id="puissance_' + row_id + '" class="form-control" placeholder="CC" min="1" value="1" autocomplete="off"></td>' +
'<td><input type="text" name="rate[]" id="rate_' + row_id + '" class="form-control numeric-input" disabled min="0"><input type="hidden" name="rate_value[]" id="rate_value_' + row_id + '" class="form-control"><input type="hidden" id="min_price_' + row_id + '" name="min_price[]" value=""></td>' +
'<td><input type="text" name="amount[]" id="amount_' + row_id + '" class="form-control numeric-input" disabled min="0"><input type="hidden" name="amount_value[]" id="amount_value_' + row_id + '" class="form-control"></td>' +
@ -404,16 +445,15 @@
return false;
});
}); // /document
});
function getTotal(row = null) {
if (row) {
var rate = Number($("#rate_value_" + row).val());
var qty = Number($("#qty_" + row).val());
var rate = Number($("#rate_value_" + row).val()) || 0;
var qty = Number($("#qty_" + row).val()) || 1;
if (rate < 0) rate = 0;
if (qty < 0) qty = 0;
if (qty < 1) qty = 1;
var total = rate * qty;
total = total.toFixed(2);
@ -421,13 +461,11 @@
$("#amount_value_" + row).val(total);
subAmount();
} else {
alert('no row !! please refresh the page');
}
}
// ✅ GET PRODUCT DATA - Récupère prix + puissance
function getProductData(row_id) {
var product_id = $("#product_" + row_id).val();
if (product_id == "") {
@ -435,20 +473,16 @@
$("#rate_value_" + row_id).val("");
$("#min_price_" + row_id).val("");
$("#puissance_" + row_id).val("1");
$("#qty_" + row_id).val("1");
$("#amount_" + row_id).val("");
$("#amount_value_" + row_id).val("");
} else {
$.ajax({
url: base_url + 'orders/getProductValueById',
type: 'post',
data: {
product_id: product_id
},
data: { product_id: product_id },
dataType: 'json',
success: function (response) {
console.log('✅ Response:', response); // Debug
var prixVente = parseFloat(response.prix_vente) || 0;
var prixMinimal = parseFloat(response.prix_minimal) || 0;
@ -459,14 +493,11 @@
$("#rate_value_" + row_id).val(prixVente);
$("#min_price_" + row_id).val(prixMinimal);
// ✅ REMPLIR LA PUISSANCE
var puissanceValue = response.puissance || '1';
console.log('✅ Puissance extraite:', puissanceValue); // Debug
$("#puissance_" + row_id).val(puissanceValue);
$("#qty_" + row_id).val(1);
var total = prixVente * 1;
var qty = parseInt($("#qty_" + row_id).val()) || 1;
var total = prixVente * qty;
total = total.toFixed(2);
$("#amount_" + row_id).val(total);
$("#amount_value_" + row_id).val(total);
@ -477,7 +508,6 @@
}
}
// calculate the total amount of the order
function subAmount() {
var service_charge = <?php echo ($company_data['service_charge_value'] > 0) ? $company_data['service_charge_value'] : 0; ?>;
var vat_charge = <?php echo ($company_data['vat_charge_value'] > 0) ? $company_data['vat_charge_value'] : 0; ?>;
@ -496,23 +526,19 @@
totalSubAmount = totalSubAmount.toFixed(2);
// sub total
$("#gross_amount").val(totalSubAmount);
$("#gross_amount_value").val(totalSubAmount);
// vat
var vat = (Number($("#gross_amount").val()) / 100) * vat_charge;
vat = vat.toFixed(2);
$("#vat_charge").val(vat);
$("#vat_charge_value").val(vat);
// service
var service = (Number($("#gross_amount").val()) / 100) * service_charge;
service = service.toFixed(2);
$("#service_charge").val(service);
$("#service_charge_value").val(service);
// total amount
var totalAmount = (Number(totalSubAmount));
totalAmount = totalAmount.toFixed(2);
@ -534,7 +560,6 @@
}
}
// ✅ Vérification du rabais vs prix minimal
function checkMinimalPrice() {
var discount = Number($("#discount").val()) || 0;
@ -577,7 +602,7 @@
subAmount();
}
// ✅ INITIALISATION AU CHARGEMENT
// ✅ INITIALISATION
const quantity = document.getElementById('qty_1')
const rates = document.getElementById('rate_1')
const amount = document.getElementById('amount_1')

319
app/Views/orders/edit.php

@ -1,3 +1,10 @@
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
.border-danger {
border: 2px solid #d9534f !important;
box-shadow: 0 0 5px rgba(217, 83, 79, 0.5) !important;
}
</style>
<div class="content-wrapper">
<section class="content-header">
<h1>
@ -65,7 +72,7 @@
</div>
<div class="col-md-4 col-xs-12 pull pull-left">
<!--NOUVEAU : Type de document -->
<!-- ✅ Type de document -->
<div class="form-group">
<label for="document_type" class="col-sm-5 control-label" style="text-align:left;">
Type de document <span class="text-danger">*</span>
@ -85,7 +92,7 @@
</div>
</div>
<!-- Types d'impression (ancien système - peut être supprimé si vous voulez) -->
<!-- Types d'impression -->
<div class="form-group">
<label for="types" class="col-sm-5 control-label" style="text-align:left;">Types d'impression</label>
<div class="col-sm-7">
@ -125,7 +132,7 @@
</div>
</div>
<!--NOUVEAU CHAMP : Type de client -->
<!-- ✅ Type de client -->
<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">
@ -137,7 +144,7 @@
</div>
</div>
<!--NOUVEAU CHAMP : Source -->
<!-- ✅ Source -->
<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">
@ -158,10 +165,12 @@
<table class="table table-bordered" id="product_info_table">
<thead>
<tr>
<th style="width:50%">Produit</th>
<th style="width:15%">Puissance (CC)</th>
<th style="width:10%">Prix unitaire</th>
<th style="width:20%">Montant</th>
<th style="width:35%">Produit</th>
<!-- ✅ COLONNE QUANTITÉ (cachée par défaut) -->
<th class="qty-column" style="width:10%; display:none;">Quantité</th>
<th style="width:12%">Puissance (CC)</th>
<th style="width:15%">Prix unitaire</th>
<th style="width:18%">Montant</th>
<th style="width:10%"><button type="button" id="add_row" class="btn btn-default"><i class="fa fa-plus"></i></button></th>
</tr>
</thead>
@ -190,6 +199,14 @@
</select>
</td>
<!-- ✅ CHAMP QUANTITÉ (caché par défaut) -->
<td class="qty-column" style="display:none;">
<input type="number" name="qty[]" id="qty_<?php echo $x; ?>"
class="form-control numeric-input"
value="<?php echo isset($val['qty']) ? esc($val['qty']) : '1'; ?>"
min="1" disabled autocomplete="off">
</td>
<td>
<input type="number" name="puissance[]" id="puissance_<?php echo $x; ?>"
class="form-control" placeholder="Puissance"
@ -203,6 +220,7 @@
value="<?php echo esc($val['rate']) ?>" autocomplete="off">
<input type="hidden" name="rate_value[]" id="rate_value_<?php echo $x; ?>"
class="form-control" value="<?php echo esc($val['rate']) ?>" autocomplete="off">
<input type="hidden" id="min_price_<?php echo $x; ?>" name="min_price[]" value="">
</td>
<td>
@ -241,14 +259,21 @@
<div class="form-group">
<label for="discount" class="col-sm-5 control-label">Prix demandé</label>
<div class="col-sm-7">
<?php
$users = session()->get('user');
if($users && $users['group_name'] == 'COMMERCIALE'):
?>
<input type="text" class="form-control" id="discount" name="discount" placeholder="Discount" onkeyup="subAmount()" value="<?php echo $order_data['order']['discount'] ?>" autocomplete="off">
<?php else: ?>
<input type="text" class="form-control" id="discount" name="discount" readonly value="<?php echo $order_data['order']['discount'] ?>" autocomplete="off">
<?php endif; ?>
<div id="discount_error" class="alert alert-danger" style="display:none; padding: 8px; margin-bottom: 5px; font-size: 13px;">
<i class="fa fa-exclamation-triangle"></i> <span id="discount_error_text"></span>
</div>
<?php
$users = session()->get('user');
if($users && $users['group_name'] == 'COMMERCIALE'):
?>
<input type="number" class="form-control numeric-input" id="discount" name="discount"
placeholder="Prix demandé" onkeyup="subAmount()"
oninput="validatePrixDemande(this)"
value="<?php echo $order_data['order']['discount'] ?>" autocomplete="off">
<?php else: ?>
<input type="text" class="form-control" id="discount" name="discount" readonly
value="<?php echo $order_data['order']['discount'] ?>" autocomplete="off">
<?php endif; ?>
</div>
</div>
@ -357,10 +382,83 @@ let Imprimente = document.getElementById('Imprimente');
let typesCommande = document.getElementById('typesCommande');
let documentType = document.getElementById('document_type');
// ✅ Fonction pour afficher/masquer la colonne quantité
function toggleQuantityColumn() {
var customerType = $('#customer_type').val();
if (customerType === 'revendeur') {
$('.qty-column').show();
$('input[name="qty[]"]').prop('disabled', false);
} else {
$('.qty-column').hide();
$('input[name="qty[]"]').val(1).prop('disabled', true);
recalculateAllRows();
}
}
// ✅ Fonction pour recalculer tous les montants
function recalculateAllRows() {
var tableProductLength = $("#product_info_table tbody tr").length;
for (var i = 1; i <= tableProductLength; i++) {
if ($("#row_" + i).length > 0) {
getTotal(i);
}
}
}
function validatePositiveNumber(input) {
let value = parseFloat(input.value);
if (isNaN(value) || value < 0) {
input.value = '';
return false;
}
return true;
}
function showDiscountError(message) {
$('#discount_error_text').text(message);
$('#discount_error').slideDown(200);
$('#discount').addClass('border-danger');
}
function hideDiscountError() {
$('#discount_error').slideUp(200);
$('#discount').removeClass('border-danger');
}
function validatePrixDemande(input) {
let prixDemande = parseFloat(input.value);
let prixAffiche = parseFloat($('#gross_amount').val()) || 0;
if (isNaN(prixDemande) || prixDemande < 0) {
input.value = '';
showDiscountError('Le prix demandé ne peut pas être négatif.');
setTimeout(hideDiscountError, 3000);
return false;
}
if (prixDemande > prixAffiche) {
showDiscountError('Le prix demandé (' + prixDemande.toFixed(2) + ') ne peut pas être supérieur au prix affiché (' + prixAffiche.toFixed(2) + ').');
input.value = prixAffiche;
subAmount();
setTimeout(hideDiscountError, 4000);
return false;
}
hideDiscountError();
return true;
}
function preventNegativeInput(e) {
if (e.key === '-' || e.key === 'e' || e.key === 'E' || e.key === '+') {
e.preventDefault();
return false;
}
}
function updatePrintLink() {
let type = documentType ? documentType.value : 'facture';
// Synchroniser l'ancien select (si vous le gardez)
if (typesCommande) {
if (type === 'facture') {
typesCommande.value = 1;
@ -371,7 +469,6 @@ function updatePrintLink() {
}
}
// Mettre à jour le lien d'impression
if (Imprimente) {
Imprimente.removeAttribute("href");
@ -391,17 +488,13 @@ function updatePrintLink() {
}
}
// ✅ Écouter les changements sur le nouveau select
if (documentType) {
documentType.addEventListener('change', updatePrintLink);
// Initialiser au chargement
updatePrintLink();
}
// ✅ Garder la compatibilité avec l'ancien système (optionnel)
if (typesCommande) {
typesCommande.addEventListener('change', function () {
// Synchroniser avec le nouveau select
if (documentType) {
if (typesCommande.value == 1) {
documentType.value = 'facture';
@ -414,25 +507,72 @@ if (typesCommande) {
updatePrintLink();
});
}
typesCommande.addEventListener('change', function () {
if (typesCommande.value == 1) {
Imprimente.removeAttribute("href");
Imprimente.setAttribute("href", base_url +'orders/printDiv/' + idData);
} else if(typesCommande.value == 3) {
Imprimente.removeAttribute("href");
Imprimente.setAttribute("href", base_url +'orders/printDivBL/' + idData);
} else {
Imprimente.removeAttribute("href");
Imprimente.setAttribute("href", base_url +'orders/printDivBLF/' + idData);
}
});
$(document).ready(function() {
$(document).ready(function() {
$(".select_group").select2();
$("#mainOrdersNav").addClass('active');
$("#manageOrdersNav").addClass('active');
// ✅ Initialiser l'affichage selon le type de client
toggleQuantityColumn();
// ✅ Écouter les changements du type de client
$('#customer_type').on('change', function() {
toggleQuantityColumn();
});
// ✅ Gérer les changements de quantité
$(document).on('input', 'input[name="qty[]"]', function() {
var row_id = $(this).attr('id').replace('qty_', '');
var qty = parseInt($(this).val()) || 1;
if (qty < 1) {
$(this).val(1);
qty = 1;
}
getTotal(row_id);
});
$(document).on('keydown', '.numeric-input, input[type="number"]', function(e) {
preventNegativeInput(e);
});
$(document).on('input', '.numeric-input, input[type="number"]', function() {
validatePositiveNumber(this);
});
$("#discount").on('input', function() {
validatePrixDemande(this);
});
$("#discount").on('blur', function() {
validatePrixDemande(this);
checkMinimalPrice();
});
$('form').on('submit', function(e) {
let hasNegative = false;
$('.numeric-input, input[type="number"]').each(function() {
if (parseFloat($(this).val()) < 0) {
hasNegative = true;
$(this).val('');
}
});
if (hasNegative) {
alert('Les valeurs négatives ne sont pas autorisées.');
e.preventDefault();
return false;
}
if (!checkMinimalPrice()) {
e.preventDefault();
return false;
}
});
var paymentTranche = 1;
function getMontantTotal() {
@ -492,6 +632,10 @@ if (typesCommande) {
type: 'post',
dataType: 'json',
success: function(response) {
var customerType = $('#customer_type').val();
var qtyColumnVisible = (customerType === 'revendeur') ? '' : 'style="display:none;"';
var qtyDisabled = (customerType === 'revendeur') ? '' : 'disabled';
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 + ')">' +
@ -502,16 +646,13 @@ if (typesCommande) {
if (value.numero_de_moteur) {
displayText += ' | ' + value.numero_de_moteur;
}
if (value.puissance) {
displayText += ' | ' + value.puissance;
}
html += '<option value="' + value.id + '">' + displayText + '</option>';
});
html += '</select>' +
'</td>' +
'<td><input type="number" name="puissance[]" id="puissance_' + row_id + '" class="form-control" placeholder="Puissance" autocomplete="off" min="1"></td>' +
'<td><input type="text" name="rate[]" id="rate_' + row_id + '" class="form-control" disabled><input type="hidden" name="rate_value[]" id="rate_value_' + row_id + '" class="form-control"></td>' +
html += '</select></td>' +
'<td class="qty-column" ' + qtyColumnVisible + '><input type="number" name="qty[]" id="qty_' + row_id + '" class="form-control numeric-input" min="1" value="1" ' + qtyDisabled + ' autocomplete="off"></td>' +
'<td><input type="number" name="puissance[]" id="puissance_' + row_id + '" class="form-control" placeholder="CC" min="1" autocomplete="off"></td>' +
'<td><input type="text" name="rate[]" id="rate_' + row_id + '" class="form-control" disabled><input type="hidden" name="rate_value[]" id="rate_value_' + row_id + '" class="form-control"><input type="hidden" id="min_price_' + row_id + '" name="min_price[]" value=""></td>' +
'<td><input type="text" name="amount[]" id="amount_' + row_id + '" class="form-control" disabled><input type="hidden" name="amount_value[]" id="amount_value_' + row_id + '" class="form-control"></td>' +
'<td><button type="button" class="btn btn-default" onclick="removeRow(\'' + row_id + '\')"><i class="fa fa-close"></i></button></td>' +
'</tr>';
@ -532,10 +673,17 @@ if (typesCommande) {
function getTotal(row = null) {
if (row) {
var total = Number($("#rate_value_" + row).val());
var rate = Number($("#rate_value_" + row).val()) || 0;
var qty = Number($("#qty_" + row).val()) || 1;
if (rate < 0) rate = 0;
if (qty < 1) qty = 1;
var total = rate * qty;
total = total.toFixed(2);
$("#amount_" + row).val(total);
$("#amount_value_" + row).val(total);
subAmount();
} else {
alert('no row !! please refresh the page');
@ -547,7 +695,9 @@ if (typesCommande) {
if (product_id == "") {
$("#rate_" + row_id).val("");
$("#rate_value_" + row_id).val("");
$("#min_price_" + row_id).val("");
$("#puissance_" + row_id).val("");
$("#qty_" + row_id).val("1");
$("#amount_" + row_id).val("");
$("#amount_value_" + row_id).val("");
} else {
@ -557,15 +707,25 @@ if (typesCommande) {
data: { product_id: product_id },
dataType: 'json',
success: function(response) {
$("#rate_" + row_id).val(response.prix_vente);
$("#rate_value_" + row_id).val(response.prix_vente);
var prixVente = parseFloat(response.prix_vente) || 0;
var prixMinimal = parseFloat(response.prix_minimal) || 0;
if (prixVente < 0) prixVente = 0;
if (prixMinimal < 0) prixMinimal = 0;
$("#puissance_" + row_id).val(response.puissance || '');
$("#rate_" + row_id).val(prixVente);
$("#rate_value_" + row_id).val(prixVente);
$("#min_price_" + row_id).val(prixMinimal);
var total = Number(response.prix_vente);
var puissanceValue = response.puissance || '';
$("#puissance_" + row_id).val(puissanceValue);
var qty = parseInt($("#qty_" + row_id).val()) || 1;
var total = prixVente * qty;
total = total.toFixed(2);
$("#amount_" + row_id).val(total);
$("#amount_value_" + row_id).val(total);
subAmount();
}
});
@ -582,7 +742,9 @@ if (typesCommande) {
var tr = $("#product_info_table tbody tr")[x];
var count = $(tr).attr('id');
count = count.substring(4);
totalSubAmount = Number(totalSubAmount) + Number($("#amount_" + count).val());
var amount = Number($("#amount_" + count).val());
if (amount < 0) amount = 0;
totalSubAmount = Number(totalSubAmount) + amount;
}
totalSubAmount = totalSubAmount.toFixed(2);
@ -599,11 +761,17 @@ if (typesCommande) {
$("#service_charge").val(service);
$("#service_charge_value").val(service);
// ✅ CORRECTION : net_amount = gross_amount - discount
var discount = Number($("#discount").val()) || 0;
if (discount < 0) {
discount = 0;
$("#discount").val('');
}
var grossAmount = Number(totalSubAmount);
var netAmount = grossAmount - discount;
if (netAmount < 0) netAmount = 0;
netAmount = netAmount.toFixed(2);
$("#net_amount").val(netAmount);
$("#net_amount_value").val(netAmount);
@ -617,7 +785,44 @@ if (typesCommande) {
}
updateMontantTranches();
}
}
function checkMinimalPrice() {
var discount = Number($("#discount").val()) || 0;
if (discount < 0) {
alert("Le prix demandé ne peut pas être négatif.");
$("#discount").val('');
subAmount();
return false;
}
if (discount === 0) return true;
var tableProductLength = $("#product_info_table tbody tr").length;
var error = false;
var messages = [];
for (var i = 0; i < tableProductLength; i++) {
var tr = $("#product_info_table tbody tr")[i];
var rowId = $(tr).attr('id').replace('row_', '');
var minPrice = Number($("#min_price_" + rowId).val()) || 0;
if (minPrice > 0 && discount < minPrice) {
error = true;
var productText = $("#product_" + rowId + " option:selected").text();
messages.push("Le prix demandé (" + discount.toFixed(2) + ") pour « " + productText + " » est inférieur au prix minimal (" + minPrice.toFixed(2) + ")");
}
}
if (error) {
alert(messages.join("\n"));
$("#discount").val('');
subAmount();
return false;
}
return true;
}
function paidAmount() {
var grandTotal = $("#net_amount_value").val();
@ -637,13 +842,10 @@ if (typesCommande) {
function getMontantPourTranches() {
var discount = parseFloat($("#discount").val()) || 0;
var grossAmount = parseFloat($("#gross_amount_value").val()) || 0;
// Si discount existe, on utilise gross_amount - discount
// Sinon on utilise gross_amount
return discount > 0 ? (grossAmount - discount) : grossAmount;
}
}
function updateMontantTranches() {
function updateMontantTranches() {
var montant = getMontantPourTranches();
var discount = parseFloat($("#discount").val()) || 0;
@ -662,14 +864,15 @@ function updateMontantTranches() {
} else {
calculerTranche2();
}
}
function calculerTranche2() {
}
function calculerTranche2() {
var montantTotal = getMontantPourTranches();
var tranche1 = parseFloat($("#payment_amount_1").val()) || 0;
var tranche2 = montantTotal - tranche1;
if (tranche2 < 0) tranche2 = 0;
$("#payment_amount_2").val(tranche2.toFixed(2));
}
}
$("#discount").on('keyup', function() {
subAmount();

25
app/Views/orders/index.php

@ -380,7 +380,8 @@
}
// ✅ FONCTION MODIFIÉE POUR AFFICHER LES NOUVELLES COLONNES
$(document).on('click', '.btn-view', function(e) {
// ✅ FONCTION MODIFIÉE POUR AFFICHER LA QUANTITÉ
$(document).on('click', '.btn-view', function(e) {
e.preventDefault();
var orderId = $(this).data('order-id');
currentOrderId = orderId;
@ -428,6 +429,7 @@
'<th>Numéro de Série</th>' +
'<th>N° de Moteur</th>' +
'<th>Châssis</th>' +
'<th>Quantité</th>' + // ✅ AJOUT
'<th>N° Facture</th>' +
'<th>Statut</th>'
);
@ -436,7 +438,9 @@
$headers.append(
'<th>Marque</th>' +
'<th>Désignation</th>' +
'<th>Prix de vente</th>' +
'<th>Quantité</th>' + // ✅ AJOUT
'<th>Prix unitaire</th>' + // ✅ MODIFIÉ
'<th>Montant</th>' + // ✅ AJOUT (prix × quantité)
'<th>Prix demandé</th>' +
'<th>Remise</th>' +
'<th>Statut</th>'
@ -465,6 +469,9 @@
}
});
// ✅ RÉCUPÉRATION DE LA QUANTITÉ
var qty = parseInt(item.qty) || 1;
<?php if ($users['group_name'] === 'SECURITE'): ?>
// ✅ AFFICHAGE POUR SECURITE
$tb.append(
@ -474,23 +481,27 @@
'<td>' + (product.sku || '') + '</td>' +
'<td>' + (product.numero_de_moteur || 'N/A') + '</td>' +
'<td>' + (product.chasis || 'N/A') + '</td>' +
'<td>' + qty + '</td>' + // ✅ AJOUT
'<td>' + (d.bill_no || 'N/A') + '</td>' +
'<td>' + statutHtml + '</td>' +
'</tr>'
);
<?php else: ?>
// ✅ AFFICHAGE POUR LES AUTRES RÔLES
var prixVente = parseFloat(product.prix_vente || 0);
var prixDemande = parseFloat(d.discount || 0); // Prix demandé = discount
var netAmount = parseFloat(d.net_amount || 0); // Remise = net_amount
var prixUnitaire = parseFloat(item.rate || 0); // Prix unitaire depuis l'item
var montant = prixUnitaire * qty; // ✅ CALCUL DU MONTANT TOTAL
var prixDemande = parseFloat(d.discount || 0);
var remise = parseFloat(d.net_amount || 0);
$tb.append(
'<tr>' +
'<td>' + (brandName || 'Aucune marque') + '</td>' +
'<td>' + (product.name || '') + '</td>' +
'<td>' + prixVente.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + qty + '</td>' +
'<td>' + prixUnitaire.toLocaleString('fr-FR') + ' Ar</td>' + // ✅ PRIX UNITAIRE
'<td>' + montant.toLocaleString('fr-FR') + ' Ar</td>' + // ✅ MONTANT TOTAL
'<td>' + prixDemande.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + netAmount.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + remise.toLocaleString('fr-FR') + ' Ar</td>' +
'<td>' + statutHtml + '</td>' +
'</tr>'
);

797
app/Views/securite/index.php

@ -1,11 +1,138 @@
<!-- application/Views/securite/index.php -->
<!-- application/Views/securite/history.php -->
<style>
.validation-summary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
overflow: hidden;
margin: 20px 0;
}
.validation-summary .box-header {
background: rgba(255, 255, 255, 0.95);
border-bottom: 3px solid #667eea;
padding: 20px 25px;
}
.validation-summary .box-title {
color: #667eea;
font-size: 22px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.5px;
}
.validation-summary .box-body {
background: #ffffff;
padding: 35px 25px;
}
.stat-card {
background: linear-gradient(135deg, #e3f2fd 0%, #bbdefb 100%);
border-radius: 12px;
padding: 25px 15px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
border-left: 5px solid #2196f3;
text-align: center;
}
.stat-card:hover {
transform: translateY(-5px);
box-shadow: 0 8px 25px rgba(0, 0, 0, 0.1);
}
.stat-number {
font-size: 36px;
font-weight: 800;
color: #1976d2;
margin: 10px 0;
}
.stat-label {
font-size: 14px;
font-weight: 600;
text-transform: uppercase;
color: #1565c0;
letter-spacing: 0.5px;
}
.badge-validated {
background-color: #4caf50;
color: white;
padding: 5px 10px;
border-radius: 12px;
font-size: 12px;
font-weight: bold;
}
.table-responsive {
border-radius: 10px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
}
#historyTable thead {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
}
#historyTable thead th {
border: none !important;
font-weight: 600;
text-transform: uppercase;
font-size: 13px;
letter-spacing: 0.5px;
}
#historyTable tbody tr {
transition: all 0.2s ease;
}
#historyTable tbody tr:hover {
background-color: #f5f5f5;
transform: scale(1.01);
}
.moto-image {
width: 80px;
height: 60px;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.agent-name {
color: #667eea;
font-weight: 600;
}
.validation-date {
color: #6c757d;
font-size: 13px;
}
.filter-section {
background: #f8f9fa;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
}
</style>
<div class="content-wrapper">
<!-- Content Header -->
<section class="content-header">
<h1>Validation de sortie <small>de matériel</small></h1>
<h1>
Historique des Livraisons
<small>Suivi des sorties validées par magasin</small>
</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-dashboard"></i> Accueil</a></li>
<li class="active">Validation des matériels</li>
<li><a href="<?= base_url('#') ?>"><i class="fa fa-dashboard"></i> Accueil</a></li>
<li><a href="<?= base_url('validateSecurite') ?>"><i class="fa fa-shield"></i> Livraisons</a></li>
<li class="active">Historique</li>
</ol>
</section>
@ -14,129 +141,603 @@
<div id="messages"></div>
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success"><?= session()->getFlashdata('success') ?></div>
<?php elseif (session()->getFlashdata('error')): ?>
<div class="alert alert-danger"><?= session()->getFlashdata('error') ?></div>
<div class="alert alert-success alert-dismissible">
<button type="button" class="close" data-dismiss="alert">&times;</button>
<?= session()->getFlashdata('success') ?>
</div>
<?php endif; ?>
<div class="box">
<div class="box-header"><h3 class="box-title">Espace validation de sortie de vente</h3></div>
<div class="box-body">
<table id="manageTable" class="table table-bordered table-striped">
<thead>
<tr>
<th>Image</th>
<th>UGS</th>
<th>Désignation</th>
<th>Statut</th>
<th>Action</th>
</tr>
</thead>
</table>
<!-- Statistiques -->
<div class="row">
<div class="col-md-12">
<div class="validation-summary">
<div class="box-header">
<h3 class="box-title">
<i class="fa fa-bar-chart"></i> Résumé des Livraisons
<span id="storeNameDisplay" style="color: #764ba2; margin-left: 10px;"></span>
</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-md-4">
<div class="stat-card">
<i class="fa fa-check-circle" style="font-size: 40px; color: #2196f3;"></i>
<div class="stat-number" id="totalValidations">0</div>
<div class="stat-label">Total Livraisons</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card">
<i class="fa fa-calendar" style="font-size: 40px; color: #2196f3;"></i>
<div class="stat-number" id="todayValidations">0</div>
<div class="stat-label">Livraisons Aujourd'hui</div>
</div>
</div>
<div class="col-md-4">
<div class="stat-card">
<i class="fa fa-motorcycle" style="font-size: 40px; color: #2196f3;"></i>
<div class="stat-number" id="monthValidations">0</div>
<div class="stat-label">Livraisons ce Mois</div>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Filtres -->
<div class="row">
<div class="col-md-12">
<div class="filter-section">
<div class="row">
<div class="col-md-3">
<label for="filterStartDate">Date de début</label>
<input type="date" id="filterStartDate" class="form-control">
</div>
<div class="col-md-3">
<label for="filterEndDate">Date de fin</label>
<input type="date" id="filterEndDate" class="form-control">
</div>
<!-- ✅ FILTRE PAR MAGASIN -->
<div class="col-md-3">
<label for="filterStore">Magasin</label>
<select id="filterStore" class="form-control">
<option value="">Tous les magasins</option>
<?php if (isset($stores) && is_array($stores)): ?>
<?php foreach ($stores as $store): ?>
<option value="<?= $store['id'] ?>"><?= $store['name'] ?></option>
<?php endforeach; ?>
<?php endif; ?>
</select>
</div>
<div class="col-md-3">
<label>&nbsp;</label><br>
<button id="btnFilter" class="btn btn-primary">
<i class="fa fa-filter"></i> Filtrer
</button>
<button id="btnReset" class="btn btn-warning">
<i class="fa fa-refresh"></i> Réinitialiser
</button>
<button id="btnExport" class="btn btn-success">
<i class="fa fa-file-excel-o"></i> Exporter
</button>
</div>
</div>
</div>
</div>
</div>
<!-- Tableau historique -->
<div class="row">
<div class="col-md-12">
<div class="box">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-history"></i> Historique des Livraisons
</h3>
</div>
<div class="box-body">
<div class="table-responsive">
<table id="historyTable" class="table table-bordered table-striped table-hover">
<thead>
<tr>
<th>Image</th>
<th>N° Facture</th>
<th>Désignation</th>
<th>UGS</th>
<th>Marque</th>
<th>Client</th>
<th>Magasin</th>
<th>Agent Sécurité</th>
<th>Date Validation</th>
<th>Statut</th>
<th>Quantité</th>
</tr>
</thead>
<tbody>
<!-- Les données seront chargées par DataTables -->
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
</section>
</div>
<!-- Modal -->
<div class="modal fade" id="editModal" tabindex="-1">
<div class="modal-dialog">
<!-- Modal Détails -->
<div class="modal fade" id="detailsModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<form id="updateForm" action="<?= base_url('validateSecurite/update') ?>" method="post">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal">&times;</button>
<h4 class="modal-title">Valider la sortie de la moto</h4>
</div>
<div class="modal-body row">
<div class="col-sm-4 text-center">
<img id="motoImage" class="img-responsive img-thumbnail" src="" alt="Image moto">
<div class="modal-header" style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white;">
<button type="button" class="close" data-dismiss="modal" style="color: white;">&times;</button>
<h4 class="modal-title">
<i class="fa fa-info-circle"></i> Détails de la Validation
</h4>
</div>
<div class="modal-body">
<div class="row">
<div class="col-md-4 text-center">
<img id="detailImage" class="img-responsive img-thumbnail" src="" alt="Image moto" style="max-height: 250px;">
</div>
<div class="col-sm-8">
<h5><strong>Nom :</strong> <span id="motoNom"></span></h5>
<h5><strong>UGS :</strong> <span id="motoGSU"></span></h5>
<h5><strong>FACTURE :</strong> <span id="motoAdditionalInfo"></span></h5>
<h5><strong>Client :</strong> <span id="customer_name"></span></h5>
<h5><strong>Adresse :</strong> <span id="customer_address"></span></h5>
<h5><strong>Téléphone :</strong> <span id="customer_phone"></span></h5>
<h5><strong>CIN :</strong> <span id="customer_cin"></span></h5>
<div class="col-md-8">
<h4 style="border-bottom: 2px solid #667eea; padding-bottom: 10px; margin-bottom: 20px;">
<i class="fa fa-motorcycle"></i> Informations Produit
</h4>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>N° Facture:</strong></div>
<div class="col-xs-6"><span id="detailBillNo"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Désignation:</strong></div>
<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>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Marque:</strong></div>
<div class="col-xs-6"><span id="detailMarque"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>N° de Série:</strong></div>
<div class="col-xs-6"><span id="detailSerie"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Magasin:</strong></div>
<div class="col-xs-6"><span id="detailStore"></span></div>
</div>
<h4 style="border-bottom: 2px solid #667eea; padding-bottom: 10px; margin: 20px 0;">
<i class="fa fa-user"></i> Informations Client
</h4>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Nom:</strong></div>
<div class="col-xs-6"><span id="detailClientName"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Téléphone:</strong></div>
<div class="col-xs-6"><span id="detailClientPhone"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Adresse:</strong></div>
<div class="col-xs-6"><span id="detailClientAddress"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>CIN:</strong></div>
<div class="col-xs-6"><span id="detailClientCIN"></span></div>
</div>
<h4 style="border-bottom: 2px solid #667eea; padding-bottom: 10px; margin: 20px 0;">
<i class="fa fa-shield"></i> Informations Validation
</h4>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Agent Sécurité:</strong></div>
<div class="col-xs-6"><span id="detailAgent" class="agent-name"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Date Validation:</strong></div>
<div class="col-xs-6"><span id="detailDateValidation"></span></div>
</div>
<div class="row" style="margin-bottom: 15px;">
<div class="col-xs-6"><strong>Statut:</strong></div>
<div class="col-xs-6"><span id="detailStatut"></span></div>
</div>
</div>
</div>
<div class="modal-footer">
<input type="hidden" name="status" value="SORTIE">
<button type="button" class="btn btn-default" data-dismiss="modal">Annuler</button>
<button type="submit" class="btn btn-success">Valider</button>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-default" data-dismiss="modal">
<i class="fa fa-times"></i> Fermer
</button>
</div>
</div>
</div>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
$(function() {
$("#securiteNav").addClass('active');
// ===== Initialisation DataTable =====$.extend(true, $.fn.dataTable.defaults, {
// Configuration DataTable en français
$.extend(true, $.fn.dataTable.defaults, {
language: {
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sLengthMenu: "Afficher _MENU_ &eacute;l&eacute;ments",
sInfo: "Affichage de l'&eacute;lement _START_ &agrave; _END_ sur _TOTAL_ &eacute;l&eacute;ments",
sInfoEmpty: "Affichage de l'&eacute;lement 0 &agrave; 0 sur 0 &eacute;l&eacute;ment",
sInfoFiltered: "(filtr&eacute; de _MAX_ &eacute;l&eacute;ments au total)",
sLoadingRecords: "Chargement en cours...",
sZeroRecords: "Aucun &eacute;l&eacute;ment &agrave; afficher",
sEmptyTable: "Aucune donn&eacute;e disponible dans le tableau",
oPaginate: {
sFirst: "Premier",
sPrevious: "Pr&eacute;c&eacute;dent",
sNext: "Suivant",
sLast: "Dernier"
},
oAria: {
sSortAscending: ": activer pour trier la colonne par ordre croissant",
sSortDescending: ": activer pour trier la colonne par ordre d&eacute;croissant"
}
sProcessing: "Traitement en cours...",
sSearch: "Rechercher&nbsp;:",
sLengthMenu: "Afficher _MENU_ éléments",
sInfo: "Affichage de _START_ à _END_ sur _TOTAL_ éléments",
sInfoEmpty: "Aucun élément à afficher",
sInfoFiltered: "(filtré de _MAX_ éléments au total)",
sLoadingRecords: "Chargement en cours...",
sZeroRecords: "Aucune validation trouvée",
sEmptyTable: "Aucune donnée disponible",
oPaginate: {
sFirst: "Premier",
sPrevious: "Précédent",
sNext: "Suivant",
sLast: "Dernier"
}
}
});
$('#manageTable').DataTable({
});
// ✅ INITIALISER LES DATES À AUJOURD'HUI PAR DÉFAUT
var today = new Date().toISOString().split('T')[0];
$('#filterStartDate').val(today);
$('#filterEndDate').val(today);
// ✅ CHARGER LES STATISTIQUES DU MOIS AU DÉMARRAGE
loadStatisticsForCurrentMonth();
// ✅ INITIALISATION DATATABLE
var historyTable = $('#historyTable').DataTable({
ajax: {
url: '<?= base_url('validateSecurite/fetchSecuriteData') ?>',
url: '<?= base_url('reports/detail/fetchSecuritePerformances') ?>',
type: 'GET',
dataSrc: 'data'
data: function(d) {
d.startDate = $('#filterStartDate').val();
d.endDate = $('#filterEndDate').val();
d.store_id = $('#filterStore').val();
console.log('📤 Filtres envoyés:', {
startDate: d.startDate,
endDate: d.endDate,
store_id: d.store_id
});
},
dataSrc: function(json) {
console.log('📥 Données reçues:', json.data.length, 'lignes');
return json.data;
},
error: function(xhr, error, code) {
console.error('❌ Erreur DataTables:', error);
alert('Erreur lors du chargement des données');
}
},
columns: [
{ data: 'image', title: 'Image', orderable: false },
{ data: 'ugs', title: 'UGS' },
{ data: 'designation', title: 'Désignation' },
{ data: 'statut', title: 'Statut', orderable: false },
{ data: 'action', title: 'Action', orderable: false }
{ data: 0, orderable: false, render: function(data) {
return '<img src="' + data + '" class="moto-image" alt="Moto">';
}},
{ data: 1 },
{ data: 2 },
{ data: 3 },
{ data: 4 },
{ data: 5 },
{ data: 7, render: function(data) {
return '<span class="badge badge-info">' + data + '</span>';
}},
{ data: 6, render: function(data) {
return '<span class="agent-name">' + data + '</span>';
}},
{ data: 8, render: function(data) {
return '<span class="validation-date">' + data + '</span>';
}},
{ data: 9, orderable: false },
{ data: 10, orderable: true, className: 'text-center', render: function(data) {
return '<span class="badge badge-primary">' + data + '</span>';
}}
],
order: [[1, 'asc']]
order: [[8, 'desc']],
pageLength: 10,
lengthMenu: [[5, 10, 25, 50, -1], [5, 10, 25, 50, "Tout"]]
});
// ===== Edition / validation =====
function editFunc(id) {
$.post('<?= base_url('validateSecurite/fetchSecuriteDataById') ?>/'+id, function(resp) {
$('#motoImage').attr('src', resp.image);
$('#motoNom').text(resp.nom);
$('#motoGSU').text(resp.ugs);
$('#motoAdditionalInfo').text(resp.bill_no);
$('#customer_name').text(resp.customer_name);
$('#customer_address').text(resp.customer_address);
$('#customer_phone').text(resp.customer_phone);
$('#customer_cin').text(resp.customer_cin);
$('#editModal').modal('show');
$('#updateForm').off('submit').on('submit', function(e) {
e.preventDefault();
$.post(this.action+'/'+id, $(this).serialize(), function(res) {
$('#editModal').modal('hide');
$('#manageTable').DataTable().ajax.reload(null, false);
$('#messages').html(
'<div class="alert alert-'+(res.success?'success':'danger')+'">'+res.messages+'</div>'
);
}, 'json');
// ✅ BOUTON FILTRER - RECHARGE TABLEAU + STATISTIQUES
$('#btnFilter').on('click', function() {
console.log('🔍 Bouton Filtrer cliqué');
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
var storeId = $('#filterStore').val();
console.log('Dates:', startDate, 'à', endDate);
console.log('Store ID:', storeId || 'Tous');
historyTable.ajax.reload();
// ✅ RECHARGER LES STATISTIQUES SELON LA PÉRIODE ET LE MAGASIN FILTRÉS
loadStatisticsForFilteredPeriod(startDate, endDate, storeId);
});
// ✅ BOUTON RÉINITIALISER
$('#btnReset').on('click', function() {
console.log('🔄 Réinitialisation des filtres');
var today = new Date().toISOString().split('T')[0];
$('#filterStartDate').val(today);
$('#filterEndDate').val(today);
$('#filterStore').val('');
$('#storeNameDisplay').text('');
historyTable.ajax.reload();
loadStatisticsForCurrentMonth();
});
// ✅ CHANGEMENT DE MAGASIN - METTRE À JOUR LES STATS
$('#filterStore').on('change', function() {
var storeId = $(this).val();
var storeName = $(this).find('option:selected').text();
var startDate = $('#filterStartDate').val();
var endDate = $('#filterEndDate').val();
console.log('🏪 Magasin changé:', storeName);
// Afficher le nom du magasin dans le titre
if (storeId) {
$('#storeNameDisplay').text('- ' + storeName);
} else {
$('#storeNameDisplay').text('');
}
// Recharger automatiquement le tableau et les stats
historyTable.ajax.reload();
if (startDate && endDate) {
loadStatisticsForFilteredPeriod(startDate, endDate, storeId);
} else {
loadStatisticsForCurrentMonth();
}
});
// ✅ FONCTION : CHARGER LES STATISTIQUES DU MOIS EN COURS
function loadStatisticsForCurrentMonth() {
console.log('📊 Chargement des statistiques du mois en cours...');
var now = new Date();
var firstDay = new Date(now.getFullYear(), now.getMonth(), 1);
var lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0);
var startDate = firstDay.toISOString().split('T')[0];
var endDate = lastDay.toISOString().split('T')[0];
loadStatisticsForFilteredPeriod(startDate, endDate, '');
}
// ✅ FONCTION : CHARGER LES STATISTIQUES POUR UNE PÉRIODE ET UN MAGASIN DONNÉS
function loadStatisticsForFilteredPeriod(startDate, endDate, storeId) {
console.log('📊 Chargement des statistiques pour:', startDate, 'au', endDate, 'Store ID:', storeId || 'TOUS');
$.ajax({
url: '<?= base_url('reports/detail/fetchSecuritePerformances') ?>',
type: 'GET',
data: {
startDate: startDate,
endDate: endDate,
store_id: storeId
},
success: function(response) {
console.log('✅ Statistiques chargées:', response.data.length, 'lignes');
calculateStatistics(response.data, startDate, endDate, storeId);
},
error: function(xhr, error) {
console.error('❌ Erreur chargement statistiques:', error);
}
});
}
// ✅ FONCTION POUR CALCULER LES STATISTIQUES (AVEC FILTRAGE PAR MAGASIN)
function calculateStatistics(data, filterStartDate, filterEndDate, filterStoreId) {
var today = new Date();
today.setHours(0, 0, 0, 0);
var todayCount = 0;
var monthCount = 0;
var filteredMonth = null;
var filteredYear = null;
if (filterStartDate && filterEndDate) {
var startDateObj = new Date(filterStartDate);
var endDateObj = new Date(filterEndDate);
if (startDateObj.getMonth() === endDateObj.getMonth() &&
startDateObj.getFullYear() === endDateObj.getFullYear()) {
filteredMonth = startDateObj.getMonth();
filteredYear = startDateObj.getFullYear();
}
}
$.each(data, function(i, item) {
var itemDateStr = item[8]; // Date de validation
if (itemDateStr && itemDateStr !== 'N/A') {
try {
var date;
if (itemDateStr.includes('/')) {
var parts = itemDateStr.split(' ')[0].split('/');
date = new Date(parts[2], parts[1] - 1, parts[0]);
} else {
date = new Date(itemDateStr);
}
if (!isNaN(date.getTime())) {
date.setHours(0, 0, 0, 0);
// Compter aujourd'hui
if (date.getTime() === today.getTime()) {
todayCount++;
}
// Compter le mois filtré (ou mois en cours)
if (filteredMonth !== null) {
if (date.getMonth() === filteredMonth && date.getFullYear() === filteredYear) {
monthCount++;
}
} else {
if (date.getMonth() === today.getMonth() && date.getFullYear() === today.getFullYear()) {
monthCount++;
}
}
}
} catch (e) {
console.warn('Date invalide:', itemDateStr, e);
}
}
});
// ✅ METTRE À JOUR LES COMPTEURS
$('#totalValidations').text(monthCount);
$('#todayValidations').text(todayCount);
$('#monthValidations').text(monthCount);
console.log('📈 Statistiques:', {
store_id: filterStoreId || 'TOUS',
total: monthCount,
aujourd_hui: todayCount,
ce_mois: monthCount,
periode_filtree: filterStartDate + ' au ' + filterEndDate
});
}
// ✅ BOUTON EXPORT EXCEL
$('#btnExport').on('click', function() {
var wb = XLSX.utils.book_new();
var data = [];
data.push([
'N° Facture',
'Désignation',
'UGS',
'Marque',
'Client',
'Magasin',
'Agent Sécurité',
'Date Validation',
'Statut',
'Quantité'
]);
historyTable.rows({ search: 'applied' }).every(function() {
var rowData = this.data();
data.push([
rowData[1],
rowData[2],
rowData[3],
rowData[4],
rowData[5],
rowData[7],
rowData[6],
rowData[8],
'VALIDÉE',
rowData[10]
]);
});
var ws = XLSX.utils.aoa_to_sheet(data);
ws['!cols'] = [
{ wch: 15 }, { wch: 30 }, { wch: 15 }, { wch: 20 }, { wch: 25 },
{ wch: 20 }, { wch: 25 }, { wch: 20 }, { wch: 12 }, { wch: 10 }
];
XLSX.utils.book_append_sheet(wb, ws, "Historique");
var today = new Date().toISOString().split('T')[0];
var filename = 'historique_livraisons_' + today + '.xlsx';
XLSX.writeFile(wb, filename);
console.log('✅ Export Excel réalisé:', filename);
});
});
// ✅ FONCTION POUR AFFICHER LES DÉTAILS
function viewDetails(id) {
$.get('<?= base_url('reports/detail/getSecuriteValidationDetails') ?>/' + id, function(data) {
$('#detailImage').attr('src', data[0]);
$('#detailBillNo').text(data[1]);
$('#detailDesignation').text(data[2]);
$('#detailUGS').text(data[3]);
$('#detailMarque').text(data[4]);
$('#detailSerie').text(data[3]);
$('#detailStore').text(data[7]);
$('#detailClientName').text(data[5]);
$('#detailClientPhone').text(data[12]);
$('#detailClientAddress').text(data[13]);
$('#detailClientCIN').text(data[14]);
$('#detailAgent').text(data[6]);
$('#detailDateValidation').text(formatDate(data[8]));
$('#detailStatut').html(data[9]);
var additionalInfo = '';
if (data[15] && data[15] !== 'N/A') {
additionalInfo += '<div class="row" style="margin-bottom: 15px;">';
additionalInfo += '<div class="col-xs-6"><strong>N° de Moteur:</strong></div>';
additionalInfo += '<div class="col-xs-6"><span>' + data[15] + '</span></div>';
additionalInfo += '</div>';
}
if (data[16] && data[16] !== 'N/A') {
additionalInfo += '<div class="row" style="margin-bottom: 15px;">';
additionalInfo += '<div class="col-xs-6"><strong>N° de Châssis:</strong></div>';
additionalInfo += '<div class="col-xs-6"><span>' + data[16] + '</span></div>';
additionalInfo += '</div>';
}
if (additionalInfo) {
$('#detailSerie').closest('.row').after(additionalInfo);
}
$('#detailsModal').modal('show');
}, 'json');
}
// ✅ FONCTION POUR FORMATER LA DATE
function formatDate(dateString) {
if (!dateString || dateString === 'N/A') return 'N/A';
try {
var date;
if (dateString.includes('/')) {
var parts = dateString.split(' ');
var dateParts = parts[0].split('/');
var timeParts = parts[1] ? parts[1].split(':') : ['00', '00'];
date = new Date(dateParts[2], dateParts[1] - 1, dateParts[0], timeParts[0], timeParts[1]);
} else {
date = new Date(dateString);
}
if (isNaN(date.getTime())) {
return dateString;
}
var options = {
year: 'numeric',
month: 'long',
day: 'numeric',
hour: '2-digit',
minute: '2-digit'
};
return date.toLocaleDateString('fr-FR', options);
} catch (e) {
console.warn('Erreur formatage date:', dateString, e);
return dateString;
}
}
</script>

9
app/Views/templates/side_menubar.php

@ -145,6 +145,15 @@
</a>
</li>
<?php endif; ?>
<?php if (in_array('createEncaissement', $user_permission) || in_array('updateEncaissement', $user_permission) || in_array('viewEncaissement', $user_permission) || in_array('deleteEncaissement', $user_permission)): ?>
<li id="storeNav">
<a href="<?php echo base_url('encaissements/') ?>">
<i class="fa fa-money"></i> <span>Autres Encaissements </span>
</a>
</li>
<?php endif; ?>
<?php if (in_array('createRecouvrement', $user_permission) || in_array('viewRecouvrement', $user_permission) || in_array('updateRecouvrement', $user_permission) || in_array('deleteRecouvrement', $user_permission)): ?>
<li id="recouvrement">
<a href="<?php echo base_url('recouvrement/') ?>">

Loading…
Cancel
Save