diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 728d45b5..e6298c0a 100644 --- a/app/Config/Routes.php +++ b/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 diff --git a/app/Controllers/AutresEncaissementsController.php b/app/Controllers/AutresEncaissementsController.php new file mode 100644 index 00000000..74989cc3 --- /dev/null +++ b/app/Controllers/AutresEncaissementsController.php @@ -0,0 +1,571 @@ +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 .= ' '; + } + + if (in_array('updateEncaissement', $this->permission)) { + $buttons .= ' '; + } + + if (in_array('deleteEncaissement', $this->permission)) { + $buttons .= ''; + } + +// ✅ Badge coloré pour le mode de paiement + $badgeClass = match($item['mode_paiement']) { + 'Espèces' => 'success', + 'MVola' => 'warning', + 'Virement Bancaire' => 'info', + default => 'default' + }; + + $modePaiementBadge = '' . $item['mode_paiement'] . ''; + + $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() + ]); + } +} +} \ No newline at end of file diff --git a/app/Controllers/AvanceController.php b/app/Controllers/AvanceController.php index 2e886ede..0cd23767 100644 --- a/app/Controllers/AvanceController.php +++ b/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 .= ' '; + // ✅ Badge de statut validation (pour le commercial uniquement) + if ($isCommercial && $value['validated'] == 0) { + $buttons .= 'En attente validation'; } - // ✅ Bouton Voir pour Direction (toujours visible, sans impression) - if ($isDirection && in_array('viewAvance', $this->permission) && !$isCaissier) { - $buttons .= ' '; } - // ✅ MODIFIÉ : Bouton Modifier - Le caissier peut maintenant modifier toutes les avances - if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) { - $buttons .= ' '; + // ✅ 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 .= ' '; + } } - // ✅ MODIFIÉ : Bouton Supprimer - Le caissier peut maintenant supprimer toutes les avances - if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) { - $buttons .= ' '; + // ✅ 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 .= ' '; + } } 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']}"); + + $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 = ' '; + + // ✅ PAS de bouton "Voir" pour les avances non validées + // $actions .= ' '; + + $actions .= ''; + + $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); + } -public function fetchExpiredAvance() +/** + * ✅ 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' + ]); + } } diff --git a/app/Controllers/Dashboard.php b/app/Controllers/Dashboard.php index 933dd3de..b4dc3fdf 100644 --- a/app/Controllers/Dashboard.php +++ b/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") { diff --git a/app/Controllers/MecanicienController.php b/app/Controllers/MecanicienController.php index bc7db875..33fa7b98 100644 --- a/app/Controllers/MecanicienController.php +++ b/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,141 +22,100 @@ 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']); + + // ✅ UTILISER LA MÉTHODE AVEC FILTRES (mecanicId au lieu de storeId) + $reparation = $Mecanicien->getReparationWithFilters($data['id'], $startDate, $endDate, $mecanicId); + $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'; - } - - return $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 .= ''; } - - // Check permissions for deleting the store + if (in_array('deleteMecanicien', $this->permission)) { - $buttons .= ' '; + $buttons .= ' '; } - - $image = '' . $repa['name'] . ''; + + $image = '' . $repa['name'] . ''; $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 .= ''; - } - - // Check permissions for deleting the store - if (in_array('deleteMecanicien', $this->permission)) { - $buttons .= ' '; + + $status = ''; + if ($repa['reparation_statut'] == 1) { + $status = 'En cours'; + } elseif ($repa['reparation_statut'] == 2) { + $status = 'Réparer'; + } else { + $status = 'Non réparer'; } - - $image = '' . $repa['name'] . ''; - $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, $buttons ]; } - - // Return data in JSON format + + log_message('debug', '📊 Nombre de réparations trouvées: ' . count($result['data'])); + return $this->response->setJSON($result); } @@ -164,7 +124,7 @@ class MecanicienController extends AdminController $this->verifyRole('createMecanicien'); $response = []; $data = []; - + $validation = \Config\Services::validation(); $validation->setRules([ 'motos' => 'required', @@ -174,7 +134,7 @@ class MecanicienController extends AdminController 'date_debut' => 'required', 'date_fin' => 'required', ]); - + $validationData = [ 'motos' => $this->request->getPost('motos'), 'mecano' => $this->request->getPost('mecano'), @@ -183,10 +143,12 @@ class MecanicienController extends AdminController 'date_debut' => $this->request->getPost('date_debut'), '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,11 +168,10 @@ 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(); } - + return $this->response->setJSON($response); } @@ -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,10 +240,9 @@ 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 '
';
-                // die(var_dump($data));
-
+    
                 if ($Mecanicien->updateReparation($data, $id)) {
                     $response['success'] = true;
                     $response['messages'] = 'Mise à jour réussie';
@@ -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();
             }
@@ -303,10 +258,13 @@ class MecanicienController extends AdminController
             $response['success'] = false;
             $response['messages'] = 'Erreur, veuillez actualiser la page à nouveau !!';
         }
-
+    
         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 = '' . $repa['name'] . '';
-                $produit     = esc($repa['name']);
-                $first_name  = esc($repa['firstname']);
-                $last_name   = esc($repa['lastname']);
+                $image = '' . $repa['name'] . '';
+                $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 = '' . $repa['name'] . '';
+                $image = '' . $repa['name'] . '';
                 $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);
     }
-}
+}
\ No newline at end of file
diff --git a/app/Controllers/OrderController.php b/app/Controllers/OrderController.php
index bd10ea07..ff25d39f 100644
--- a/app/Controllers/OrderController.php
+++ b/app/Controllers/OrderController.php
@@ -442,303 +442,239 @@ class OrderController extends AdminController
  * ✅ AMÉLIORATION : Notifications centralisées pour Direction/DAF/SuperAdmin (tous stores)
  */
 
-public function create()
-{
-    $this->verifyRole('createOrder');
-    $data['page_title'] = $this->pageTitle;
-
-    $validation = \Config\Services::validation();
-    $products = $this->request->getPost('product[]');
-
-    if ($products !== null && (count($products) !== count(array_unique($products)))) {
-        return redirect()->back()->withInput()->with('errors', ['product' => 'Chaque produit sélectionné doit être unique.']);
-    }
-
-    $validation->setRules([
-        'product[]' => 'required',
-        'customer_type' => 'required',
-        'source' => 'required'
-    ]);
-
-    $validationData = [
-        'product[]' => $this->request->getPost('product[]'),
-        'customer_type' => $this->request->getPost('customer_type'),
-        'source' => $this->request->getPost('source')
-    ];
-
-    $Orders = new Orders();
-    $Company = new Company();
-    $Products = new Products();
-
-    if ($this->request->getMethod() === 'post' && $validation->run($validationData)) {
-        
-        $session = session();
-        $users = $session->get('user');
-        $user_id = $users['id'];
-        
-        //  Générer le numéro séquentiel
-        $bill_no = $this->generateSimpleSequentialBillNo($users['store_id']);
-        
-        //  Récupérer le type de document
-        $document_type = $this->request->getPost('document_type') ?? 'facture';
-        
-        $posts = $this->request->getPost('product[]');
-        $rates = $this->request->getPost('rate_value[]');
-        $amounts = $this->request->getPost('amount_value[]');
-        $puissances = $this->request->getPost('puissance[]');
-        $discount = (float)$this->request->getPost('discount') ?? 0;
-        $gross_amount = $this->calculGross($amounts);
-        $net_amount = $gross_amount - $discount;
-
-
-        // Vérification prix minimal SI rabais existe
-        if ($discount > 0) {
-            $FourchettePrix = new \App\Models\FourchettePrix();
-
-            foreach ($posts as $index => $productId) {
-                $productId = (int)$productId;
-
-                $productData = $Products->getProductData($productId);
-                $fourchette = $FourchettePrix->getFourchettePrixByProductId($productId);
-
-                if ($fourchette) {
-                    $prixMinimal = (float)$fourchette['prix_minimal'];
-
-                    if ($discount < $prixMinimal) {
-                        $prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' ');
-                        $discountFormatted = number_format($discount, 0, ',', ' ');
-                        
-                        return redirect()->back()
-                            ->withInput()
-                            ->with('errors', [
-                                "⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé."
-                            ]);
-                    }
-                }
-            }
-        }
-
-        $discount = (float)$this->request->getPost('discount') ?? 0;
-        $gross_amount = $this->calculGross($amounts);
-        
-        
-        $net_amount = $gross_amount - $discount;
-        
-        $tranche_1 = (float)$this->request->getPost('tranche_1') ?? 0;
-        $tranche_2 = (float)$this->request->getPost('tranche_2') ?? 0;
-        
-        // Si des tranches sont définies, vérifier la cohérence
-        if ($tranche_1 > 0 && $tranche_2 > 0) {
-            $total_tranches = $tranche_1 + $tranche_2;
-            // S'assurer que les tranches correspondent au net_amount
-            if (abs($total_tranches - $net_amount) > 0.01) {
-                return redirect()->back()
-                    ->withInput()
-                    ->with('errors', [
-                        'Les tranches de paiement ne correspondent pas au montant total (' . 
-                        number_format($net_amount, 0, ',', ' ') . ' Ar)'
-                    ]);
-            }
-        }
-        
-        $data = [
-            'bill_no' => $bill_no,
-            'document_type' => $document_type,  
-            'customer_name' => $this->request->getPost('customer_name'),
-            'customer_address' => $this->request->getPost('customer_address'),
-            'customer_phone' => $this->request->getPost('customer_phone'),
-            'customer_cin' => $this->request->getPost('customer_cin'),
-            'customer_type' => $this->request->getPost('customer_type'),
-            'source' => $this->request->getPost('source'),
-            'date_time' => date('Y-m-d H:i:s'),
-            'service_charge_rate' => 0,
-            'vat_charge_rate' => 0,
-            'vat_charge' => 0,
-            'net_amount' => $net_amount,
-            'discount' => $discount,
-            'paid_status' => 2,
-            'user_id' => $user_id,
-            'amount_value' => $amounts,
-            'gross_amount' => $gross_amount,
-            'rate_value' => $rates,
-            'puissance' => $puissances,
-            'store_id' => $users['store_id'],
-            'tranche_1' => $tranche_1,
-            'tranche_2' => $tranche_2,
-            'order_payment_mode' => $this->request->getPost('order_payment_mode_1'),
-            'order_payment_mode_1' => $this->request->getPost('order_payment_mode_2')
-        ];
-
-        $order_id = $Orders->create($data, $posts);
-
-        if ($order_id) {
-            // ✅ NOUVEAU : Marquer immédiatement les produits comme réservés
-            $productModel = new Products();
-            foreach ($posts as $product_id) {
-                $productModel->update($product_id, ['product_sold' => 1]);
-            }
-            
-            session()->setFlashdata('success', 'Créé avec succès');
-            
-
-            $Notification = new NotificationController();
-            $Stores = new Stores();
-
-            if ($discount > 0) {
-                // ✅ DEMANDE DE REMISE : NOTIFIER TOUS LES STORES
-                $Order_item1 = new OrderItems();
-                $order_item_data = $Order_item1->getOrdersItemData($order_id);
-                $product_ids = array_column($order_item_data, 'product_id');
-
-                $productData = new Products();
-                $product_data_results = [];
-
-                foreach ($product_ids as $prod_id) {
-                    $id = (int) $prod_id;
-                    $product_data_results[] = $productData->getProductData($id);
-                }
-
-                $product_lines = [];
-                foreach ($product_data_results as $product) {
-                    if (isset($product['sku'], $product['price'])) {
-                        $sku = $product['sku'];
-                        $price = $product['price'];
-                        $product_lines[] = "{$sku}:{$price}";
-                    }
-                }
-
-                $product_output = implode("\n", $product_lines);
-
-                $data1 = [
-                    'date_demande' => date('Y-m-d H:i:s'),
-                    'montant_demande' => $discount,
-                    'total_price' => $amounts,
-                    'id_store' => $users['store_id'],
-                    'id_order' => $order_id,
-                    'product' => $product_output,
-                    'demande_status' => 'En attente'
-                ];
-
-                $Remise = new Remise();
-                $id_remise = $Remise->addDemande($data1);
-                
-                // ✅ RÉCUPÉRER TOUS LES STORES
-                $allStores = $Stores->getActiveStore();
-                
-                $montantFormatted = number_format($discount, 0, ',', ' ');
-                $message = "💰 Nouvelle demande de remise : {$montantFormatted} Ar
" . - "Commande : {$bill_no}
" . - "Store : " . $this->returnStore($users['store_id']) . "
" . - "Demandeur : {$users['firstname']} {$users['lastname']}"; - - // ✅ NOTIFIER SUPERADMIN, DIRECTION, DAF DE TOUS LES STORES - if (is_array($allStores) && count($allStores) > 0) { - foreach ($allStores as $store) { - // SuperAdmin (validation) - $Notification->createNotification( - $message, - "SuperAdmin", - (int)$store['id'], - 'remise/' - ); - - // Direction (consultation) - $Notification->createNotification( - $message . "
Pour information", - "Direction", - (int)$store['id'], - 'remise/' - ); - - // DAF (consultation) - $Notification->createNotification( - $message . "
Pour information", - "DAF", - (int)$store['id'], - 'remise/' - ); - } - } - - } else { - // ✅ COMMANDE SANS REMISE : NOTIFIER CAISSIÈRE DU STORE + TOUS LES CENTRAUX - - // Caissière du store concerné - $Notification->createNotification( - "📦 Nouvelle commande à valider : {$bill_no}
" . - "Client : {$data['customer_name']}
" . - "Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar", - "Caissière", - (int)$users['store_id'], - "orders" - ); - - // ✅ RÉCUPÉRER TOUS LES STORES - $allStores = $Stores->getActiveStore(); - - $messageGlobal = "📋 Nouvelle commande créée : {$bill_no}
" . - "Store : " . $this->returnStore($users['store_id']) . "
" . - "Client : {$data['customer_name']}
" . - "Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar
" . - "Créée par : {$users['firstname']} {$users['lastname']}"; - - // ✅ NOTIFIER DIRECTION, DAF, SUPERADMIN DE TOUS LES STORES - if (is_array($allStores) && count($allStores) > 0) { - foreach ($allStores as $store) { - $Notification->createNotification( - $messageGlobal, - "Direction", - (int)$store['id'], - 'orders' - ); - - $Notification->createNotification( - $messageGlobal, - "DAF", - (int)$store['id'], - 'orders' - ); - - $Notification->createNotification( - $messageGlobal, - "SuperAdmin", - (int)$store['id'], - 'orders' - ); - } - } - } - - if ($users["group_name"] != "COMMERCIALE") { - $this->checkProductisNull($posts, $users['store_id']); - } - return redirect()->to('orders/'); - - } else { - session()->setFlashdata('errors', 'Error occurred!!'); - return redirect()->to('orders/create/'); - } - - } else { - // Affichage du formulaire - $company = $Company->getCompanyData(1); - $session = session(); - $users = $session->get('user'); - $store_id = $users['store_id']; - - $data = [ - 'paid_status' => 2, - 'company_data' => $company, - 'is_vat_enabled' => ($company['vat_charge_value'] > 0), - 'is_service_enabled' => ($company['service_charge_value'] > 0), - 'products' => $Products->getProductData2($store_id), - 'validation' => $validation, - 'page_title' => $this->pageTitle, - ]; - - return $this->render_template('orders/create', $data); - } -} + public function create() + { + $this->verifyRole('createOrder'); + $data['page_title'] = $this->pageTitle; + + $validation = \Config\Services::validation(); + $products = $this->request->getPost('product[]'); + + if ($products !== null && (count($products) !== count(array_unique($products)))) { + return redirect()->back()->withInput()->with('errors', ['product' => 'Chaque produit sélectionné doit être unique.']); + } + + $validation->setRules([ + 'product[]' => 'required', + 'customer_type' => 'required', + 'source' => 'required' + ]); + + $validationData = [ + 'product[]' => $this->request->getPost('product[]'), + 'customer_type' => $this->request->getPost('customer_type'), + 'source' => $this->request->getPost('source') + ]; + + $Orders = new Orders(); + $Company = new Company(); + $Products = new Products(); + + if ($this->request->getMethod() === 'post' && $validation->run($validationData)) { + + $session = session(); + $users = $session->get('user'); + $user_id = $users['id']; + + $bill_no = $this->generateSimpleSequentialBillNo($users['store_id']); + $document_type = $this->request->getPost('document_type') ?? 'facture'; + + $posts = $this->request->getPost('product[]'); + $rates = $this->request->getPost('rate_value[]'); + $amounts = $this->request->getPost('amount_value[]'); + $puissances = $this->request->getPost('puissance[]'); + $quantities = $this->request->getPost('qty[]'); // ✅ RÉCUPÉRER LES QUANTITÉS + + $discount = (float)$this->request->getPost('discount') ?? 0; + $gross_amount = $this->calculGross($amounts); + $net_amount = $gross_amount - $discount; + + // Vérification prix minimal SI rabais existe + if ($discount > 0) { + $FourchettePrix = new \App\Models\FourchettePrix(); + + foreach ($posts as $index => $productId) { + $productId = (int)$productId; + $productData = $Products->getProductData($productId); + $fourchette = $FourchettePrix->getFourchettePrixByProductId($productId); + + if ($fourchette) { + $prixMinimal = (float)$fourchette['prix_minimal']; + + if ($discount < $prixMinimal) { + $prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' '); + $discountFormatted = number_format($discount, 0, ',', ' '); + + return redirect()->back() + ->withInput() + ->with('errors', [ + "⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé." + ]); + } + } + } + } + + $tranche_1 = (float)$this->request->getPost('tranche_1') ?? 0; + $tranche_2 = (float)$this->request->getPost('tranche_2') ?? 0; + + if ($tranche_1 > 0 && $tranche_2 > 0) { + $total_tranches = $tranche_1 + $tranche_2; + if (abs($total_tranches - $net_amount) > 0.01) { + return redirect()->back() + ->withInput() + ->with('errors', [ + 'Les tranches de paiement ne correspondent pas au montant total (' . + number_format($net_amount, 0, ',', ' ') . ' Ar)' + ]); + } + } + + $data = [ + 'bill_no' => $bill_no, + 'document_type' => $document_type, + 'customer_name' => $this->request->getPost('customer_name'), + 'customer_address' => $this->request->getPost('customer_address'), + 'customer_phone' => $this->request->getPost('customer_phone'), + 'customer_cin' => $this->request->getPost('customer_cin'), + 'customer_type' => $this->request->getPost('customer_type'), + 'source' => $this->request->getPost('source'), + 'date_time' => date('Y-m-d H:i:s'), + 'service_charge_rate' => 0, + 'vat_charge_rate' => 0, + 'vat_charge' => 0, + 'net_amount' => $net_amount, + 'discount' => $discount, + 'paid_status' => 2, + 'user_id' => $user_id, + 'amount_value' => $amounts, + 'gross_amount' => $gross_amount, + 'rate_value' => $rates, + 'puissance' => $puissances, + 'qty' => $quantities, // ✅ AJOUTER LES QUANTITÉS + 'store_id' => $users['store_id'], + 'tranche_1' => $tranche_1, + 'tranche_2' => $tranche_2, + 'order_payment_mode' => $this->request->getPost('order_payment_mode_1'), + 'order_payment_mode_1' => $this->request->getPost('order_payment_mode_2') + ]; + + $order_id = $Orders->create($data, $posts); + + if ($order_id) { + // ✅ MARQUER LES PRODUITS COMME VENDUS (SEULEMENT SI QTY = 1) + $productModel = new Products(); + foreach ($posts as $index => $product_id) { + $qty = isset($quantities[$index]) ? (int)$quantities[$index] : 1; + + if ($qty == 1) { + // Vente unitaire : marquer comme vendu + $productModel->update($product_id, ['product_sold' => 1]); + } + // Si qty > 1, vous devriez décrémenter un stock si vous en avez un + } + + session()->setFlashdata('success', 'Créé avec succès'); + + $Notification = new NotificationController(); + $Stores = new Stores(); + + if ($discount > 0) { + // Demande de remise + $Order_item1 = new OrderItems(); + $order_item_data = $Order_item1->getOrdersItemData($order_id); + $product_ids = array_column($order_item_data, 'product_id'); + + $productData = new Products(); + $product_data_results = []; + + foreach ($product_ids as $prod_id) { + $id = (int) $prod_id; + $product_data_results[] = $productData->getProductData($id); + } + + $product_lines = []; + foreach ($product_data_results as $product) { + if (isset($product['sku'], $product['price'])) { + $sku = $product['sku']; + $price = $product['price']; + $product_lines[] = "{$sku}:{$price}"; + } + } + + $product_output = implode("\n", $product_lines); + + $data1 = [ + 'date_demande' => date('Y-m-d H:i:s'), + 'montant_demande' => $discount, + 'total_price' => $amounts, + 'id_store' => $users['store_id'], + 'id_order' => $order_id, + 'product' => $product_output, + 'demande_status' => 'En attente' + ]; + + $Remise = new Remise(); + $id_remise = $Remise->addDemande($data1); + + $allStores = $Stores->getActiveStore(); + + $montantFormatted = number_format($discount, 0, ',', ' '); + $message = "💰 Nouvelle demande de remise : {$montantFormatted} Ar
" . + "Commande : {$bill_no}
" . + "Store : " . $this->returnStore($users['store_id']) . "
" . + "Demandeur : {$users['firstname']} {$users['lastname']}"; + + if (is_array($allStores) && count($allStores) > 0) { + foreach ($allStores as $store) { + $Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'remise/'); + $Notification->createNotification($message . "
Pour information", "Direction", (int)$store['id'], 'remise/'); + $Notification->createNotification($message . "
Pour information", "DAF", (int)$store['id'], 'remise/'); + } + } + + } else { + // Commande sans remise + $Notification->createNotification( + "📦 Nouvelle commande à valider : {$bill_no}
" . + "Client : {$data['customer_name']}
" . + "Montant : " . number_format($gross_amount, 0, ',', ' ') . " Ar", + "Caissière", + (int)$users['store_id'], + "orders" + ); + } + + if ($users["group_name"] != "COMMERCIALE") { + $this->checkProductisNull($posts, $users['store_id']); + } + return redirect()->to('orders/'); + + } else { + session()->setFlashdata('errors', 'Error occurred!!'); + return redirect()->to('orders/create/'); + } + + } else { + // Affichage du formulaire + $company = $Company->getCompanyData(1); + $session = session(); + $users = $session->get('user'); + $store_id = $users['store_id']; + + $data = [ + 'paid_status' => 2, + 'company_data' => $company, + 'is_vat_enabled' => ($company['vat_charge_value'] > 0), + 'is_service_enabled' => ($company['service_charge_value'] > 0), + 'products' => $Products->getProductData2($store_id), + 'validation' => $validation, + 'page_title' => $this->pageTitle, + ]; + + return $this->render_template('orders/create', $data); + } + } /** * Marquer une commande comme livrée * Accessible uniquement par le rôle SECURITE @@ -782,8 +718,14 @@ public function markAsDelivered() return $this->response->setJSON($response); } - // Mettre à jour UNIQUEMENT le statut à 3 (Livré) - $updated = $Orders->update((int)$order_id, ['paid_status' => 3]); + // ✅ MISE À JOUR : Enregistrer delivered_by et delivered_at + $updateData = [ + 'paid_status' => 3, + 'delivered_by' => $users['id'], // ← NOUVEAU + 'delivered_at' => date('Y-m-d H:i:s') // ← NOUVEAU + ]; + + $updated = $Orders->update((int)$order_id, $updateData); if ($updated) { // ✅ Créer une notification centralisée pour tous les stores @@ -795,7 +737,8 @@ public function markAsDelivered() $messageGlobal = "📦 Commande livrée : {$current_order['bill_no']}
" . "Store : " . $this->returnStore($current_order['store_id']) . "
" . "Client : {$current_order['customer_name']}
" . - "Livrée par : {$users['firstname']} {$users['lastname']}"; + "Livrée par : {$users['firstname']} {$users['lastname']}
" . + "Date livraison : " . date('d/m/Y H:i'); // ✅ NOTIFIER DIRECTION, DAF, SUPERADMIN DE TOUS LES STORES if (is_array($allStores) && count($allStores) > 0) { @@ -925,7 +868,6 @@ public function update(int $id) $Orders = new Orders(); $current_order = $Orders->getOrdersData($id); - // ✅ AJOUT : Vérification plus détaillée if (!$current_order || !isset($current_order['id'])) { log_message('error', 'Commande introuvable pour ID: ' . $id); session()->setFlashData('errors', 'Commande introuvable.'); @@ -938,11 +880,15 @@ public function update(int $id) } $validation->setRules([ - 'product' => 'required' + 'product' => 'required', + 'customer_type' => 'required', + 'source' => 'required' ]); $validationData = [ - 'product' => $this->request->getPost('product') + 'product' => $this->request->getPost('product'), + 'customer_type' => $this->request->getPost('customer_type'), + 'source' => $this->request->getPost('source') ]; $Company = new Company(); @@ -988,27 +934,64 @@ public function update(int $id) $discount = $original_discount; } + // ✅ RÉCUPÉRER LES QUANTITÉS + $posts = $this->request->getPost('product'); + $rates = $this->request->getPost('rate_value'); + $amounts = $this->request->getPost('amount_value'); + $puissances = $this->request->getPost('puissance'); + $quantities = $this->request->getPost('qty'); // ✅ NOUVEAU + + $gross_amount = $this->calculGross($amounts); + $net_amount = $gross_amount - (float)$discount; + + // ✅ Vérification prix minimal SI rabais existe + if ((float)$discount > 0) { + $FourchettePrix = new \App\Models\FourchettePrix(); + + foreach ($posts as $index => $productId) { + $productId = (int)$productId; + $productData = $Products->getProductData($productId); + $fourchette = $FourchettePrix->getFourchettePrixByProductId($productId); + + if ($fourchette) { + $prixMinimal = (float)$fourchette['prix_minimal']; + + if ((float)$discount < $prixMinimal) { + $prixMinimalFormatted = number_format($prixMinimal, 0, ',', ' '); + $discountFormatted = number_format($discount, 0, ',', ' '); + + return redirect()->back() + ->withInput() + ->with('errors', [ + "⚠️ Mise à jour bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé." + ]); + } + } + } + } + $dataUpdate = [ - 'document_type' => $this->request->getPost('document_type'), // ✅ AJOUTER CETTE LIGNE + 'document_type' => $this->request->getPost('document_type'), 'customer_name' => $this->request->getPost('customer_name'), 'customer_address' => $this->request->getPost('customer_address'), 'customer_phone' => $this->request->getPost('customer_phone'), 'customer_cin' => $this->request->getPost('customer_cin'), 'customer_type' => $this->request->getPost('customer_type'), 'source' => $this->request->getPost('source'), - 'gross_amount' => $this->request->getPost('gross_amount_value'), + 'gross_amount' => $gross_amount, 'service_charge_rate' => $this->request->getPost('service_charge_rate'), 'service_charge' => max(0, (float)$this->request->getPost('service_charge_value')), 'vat_charge_rate' => $this->request->getPost('vat_charge_rate'), 'vat_charge' => max(0, (float)$this->request->getPost('vat_charge_value')), - 'net_amount' => $this->request->getPost('net_amount_value'), + 'net_amount' => $net_amount, 'discount' => (float)$discount, 'paid_status' => $paid_status, - 'product' => $this->request->getPost('product'), + 'product' => $posts, 'product_sold' => true, - 'rate_value' => $this->request->getPost('rate_value'), - 'amount_value' => $this->request->getPost('amount_value'), - 'puissance' => $this->request->getPost('puissance'), + 'rate_value' => $rates, + 'amount_value' => $amounts, + 'puissance' => $puissances, + 'qty' => $quantities, // ✅ AJOUTER LES QUANTITÉS 'tranche_1' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_1') : null, 'tranche_2' => $role !== 'COMMERCIALE' ? $this->request->getPost('tranche_2') : null, 'order_payment_mode' => $role !== 'COMMERCIALE' ? $this->request->getPost('order_payment_mode_1') : null, @@ -1018,6 +1001,21 @@ public function update(int $id) ]; if ($Orders->updates($id, $dataUpdate)) { + + // ✅ GÉRER LE STATUT product_sold SELON LA QUANTITÉ + $productModel = new Products(); + foreach ($posts as $index => $product_id) { + $qty = isset($quantities[$index]) ? (int)$quantities[$index] : 1; + + if ($qty == 1) { + // Vente unitaire : marquer comme vendu + $productModel->update($product_id, ['product_sold' => 1]); + } else { + // Vente multiple : ne pas marquer comme vendu (géré par stock si applicable) + $productModel->update($product_id, ['product_sold' => 0]); + } + } + $order_item_data = $OrderItems->getOrdersItemData($id); $product_ids = array_column($order_item_data, 'product_id'); @@ -1092,7 +1090,7 @@ public function update(int $id) $data1 = [ 'date_demande' => date('Y-m-d H:i:s'), 'montant_demande' => $this->request->getPost('discount'), - 'total_price' => $this->request->getPost('amount_value'), + 'total_price' => $amounts, 'id_store' => $user['store_id'], 'id_order' => $id, 'product' => $product_output, @@ -1102,12 +1100,22 @@ public function update(int $id) $Remise = new Remise(); $Remise->updateRemise1($id, $data1); - $Notification->createNotification( - "Une nouvelle demande de remise a été ajoutée", - "Direction", - (int)$user['store_id'] ?? null, - "remise/" - ); + $Stores = new Stores(); + $allStores = $Stores->getActiveStore(); + + $montantFormatted = number_format($discount, 0, ',', ' '); + $message = "💰 Demande de remise mise à jour : {$montantFormatted} Ar
" . + "Commande : {$bill_no}
" . + "Store : " . $this->returnStore($user['store_id']) . "
" . + "Demandeur : {$user['firstname']} {$user['lastname']}"; + + if (is_array($allStores) && count($allStores) > 0) { + foreach ($allStores as $store) { + $Notification->createNotification($message, "SuperAdmin", (int)$store['id'], 'remise/'); + $Notification->createNotification($message . "
Pour information", "Direction", (int)$store['id'], 'remise/'); + $Notification->createNotification($message . "
Pour information", "DAF", (int)$store['id'], 'remise/'); + } + } } session()->setFlashData('success', 'Commande mise à jour avec succès.'); @@ -1126,7 +1134,6 @@ public function update(int $id) $orders_data = $Orders->getOrdersData($id); - // ✅ VÉRIFICATION SUPPLÉMENTAIRE if (!$orders_data || !isset($orders_data['id'])) { log_message('error', 'Données de commande vides pour ID: ' . $id); session()->setFlashData('errors', 'Impossible de charger les données de la commande.'); @@ -2229,77 +2236,89 @@ public function print5(int $id) '; // ✅ TABLEAU ADAPTÉ SELON LE TYPE - if ($isAvanceMere) { - $html .= ' - - - - - - - - '; - - foreach ($items as $it) { - $details = $this->getOrderItemDetails($it); - - if (!$details) continue; - - $prixAffiche = ($discount > 0) ? $discount : $details['prix']; - - $html .= ' - - '; - } +// Dans la boucle foreach ($items as $item) +if ($isAvanceMere) { + $html .= ' +
ProduitPrix (Ar)
'.esc($details['product_name']); - - if (!empty($details['commentaire'])) { - $html .= '
'.esc($details['commentaire']).''; - } - - $html .= '
'.number_format($prixAffiche, 0, '', ' ').'
+ + + + + + + + + '; - $html .= ' - -
ProduitQuantité Prix unitaire (Ar)Montant (Ar)
'; + foreach ($items as $it) { + $details = $this->getOrderItemDetails($it); + + if (!$details) continue; + + $qty = isset($it['qty']) ? (int)$it['qty'] : 1; // ✅ RÉCUPÉRATION + $prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix']; + $prixTotal = $prixUnitaire * $qty; + + $html .= ''.esc($details['product_name']); + + if (!empty($details['commentaire'])) { + $html .= '
'.esc($details['commentaire']).''; + } + + $html .= ' + '.esc($qty).' + '.number_format($prixUnitaire, 0, '', ' ').' + '.number_format($prixTotal, 0, '', ' ').' + '; + } - } else { - $html .= ' - - - - - - - - - - - - '; + $html .= ' + +
MARQUEDésignationN° MoteurN° ChâssisPuissance (CC)PRIX (Ar)
'; - foreach ($items as $it) { - $details = $this->getOrderItemDetails($it); - - if (!$details) continue; - - $prixAffiche = ($discount > 0) ? $discount : $details['prix']; - - $html .= ' - - '.esc($details['marque']).' - '.esc($details['product_name']).' - '.esc($details['numero_moteur']).' - '.esc($details['numero_chassis']).' - '.esc($details['puissance']).' - '.number_format($prixAffiche, 0, '', ' ').' - '; - } +} else { + $html .= ' + + + + + + + + + + + + + + '; - $html .= ' - -
MARQUEDésignationN° MoteurN° ChâssisPuissance (CC)Quantité Prix Unit. (Ar)Montant (Ar)
'; - } + foreach ($items as $it) { + $details = $this->getOrderItemDetails($it); + + if (!$details) continue; + + $qty = isset($it['qty']) ? (int)$it['qty'] : 1; // ✅ RÉCUPÉRATION + $prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix']; + $prixTotal = $prixUnitaire * $qty; + + $html .= ' + + '.esc($details['marque']).' + '.esc($details['product_name']).' + '.esc($details['numero_moteur']).' + '.esc($details['numero_chassis']).' + '.esc($details['puissance']).' + '.esc($qty).' + '.number_format($prixUnitaire, 0, '', ' ').' + '.number_format($prixTotal, 0, '', ' ').' + '; + } + $html .= ' + + '; +} $html .= ' @@ -2614,21 +2633,25 @@ public function print7(int $id) // ✅ TABLEAU ADAPTÉ SELON LE TYPE if ($isAvanceMere) { $html .= ' -
- - - - - - - '; - +
ProduitPrix Unitaire (Ar)
+ + + + + + + + + '; + foreach ($items as $item) { $details = $this->getOrderItemDetails($item); if (!$details) continue; - $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + $qty = isset($item['qty']) ? (int)$item['qty'] : 1; // ✅ RÉCUPÉRATION + $prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix']; + $prixTotal = $prixUnitaire * $qty; $html .= ' @@ -2639,40 +2662,46 @@ public function print7(int $id) } $html .= ' - + + + '; } - + $html .= ' - -
ProduitQuantité Prix Unitaire (Ar)Montant (Ar)
'.number_format($prixAffiche, 0, '', ' ').''.esc($qty).' '.number_format($prixUnitaire, 0, '', ' ').''.number_format($prixTotal, 0, '', ' ').'
'; - + + '; + } else { $html .= ' - - - - - - - - - - - - - '; - +
NomMarqueCatégorieN° MoteurChâssisPuissance (CC)Prix Unitaire (Ar)
+ + + + + + + + + + + + + + '; + $Products = new Products(); $Brand = new Brands(); $Category = new Category(); - + foreach ($items as $item) { $details = $this->getOrderItemDetails($item); if (!$details) continue; - $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + $qty = isset($item['qty']) ? (int)$item['qty'] : 1; // ✅ RÉCUPÉRATION + $prixUnitaire = ($discount > 0) ? ($discount / $qty) : $details['prix']; + $prixTotal = $prixUnitaire * $qty; $categoryName = 'Non définie'; if ($details['type'] === 'terre' && !empty($item['product_id'])) { @@ -2693,15 +2722,16 @@ public function print7(int $id) - + + + '; } - + $html .= ' - -
NomMarqueCatégorieN° MoteurChâssisPuissance (CC)Quantité Prix Unit. (Ar)Montant (Ar)
'.esc($details['numero_moteur']).' '.esc($details['numero_chassis']).' '.esc($details['puissance']).''.number_format($prixAffiche, 0, '', ' ').''.esc($qty).' '.number_format($prixUnitaire, 0, '', ' ').''.number_format($prixTotal, 0, '', ' ').'
'; + + '; } - $html .= ' @@ -2848,6 +2878,7 @@ public function print31(int $id) th, td { border:1px solid #000; padding:6px; text-align:left; } th { background:#f0f0f0; } .right { text-align:right; } + .center { text-align:center; } .signature { display:flex; justify-content:space-between; margin-top:50px; } .signature div { text-align:center; } .page-break { page-break-after: always; } @@ -2866,8 +2897,18 @@ public function print31(int $id) if (!$details) continue; + // ✅ RÉCUPÉRATION DE LA QUANTITÉ + $qty = isset($item['qty']) ? (int)$item['qty'] : 1; $unitPrice = $details['prix']; - $prixAffiche = ($discount > 0 && $isAvanceMere) ? $discount : $unitPrice; + + // Calcul du prix selon la quantité et la remise + if ($discount > 0 && $isAvanceMere) { + $prixTotal = $discount; + $prixUnitaire = $discount / $qty; + } else { + $prixUnitaire = $unitPrice / $qty; + $prixTotal = $unitPrice; + } echo ''; echo ''; @@ -2900,20 +2941,24 @@ public function print31(int $id) // ✅ TABLEAU ADAPTÉ SELON LE TYPE if ($isAvanceMere) { - // TABLE SIMPLIFIÉE POUR AVANCE "SUR MER" echo '
'; echo ''; echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo ''; + echo ''; echo ''; echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo ''; if (!empty($details['commentaire'])) { - echo ''; + echo ''; } echo '
DésignationProduit à compléterPrix (Ar)QuantitéPrix Unit. (Ar)Montant (Ar)
' . esc($details['product_name']) . '                    ' . number_format($prixAffiche, 0, '', ' ') . '' . esc($qty) . '' . number_format($prixUnitaire, 0, '', ' ') . '' . number_format($prixTotal, 0, '', ' ') . '
Remarque : ' . esc($details['commentaire']) . '
Remarque : ' . esc($details['commentaire']) . '
'; @@ -2931,8 +2976,17 @@ public function print31(int $id) } echo ''; - echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; echo ''; + echo ''; echo ''; echo ''; @@ -2940,19 +2994,21 @@ public function print31(int $id) echo ''; echo ''; echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo ''; echo '
NomMarqueCatégorieN° MoteurChâssisPuissance (CC)Prix Unit. (Ar)NomMarqueCatégorieN° MoteurChâssisPuissance (CC)QtéPrix Unit. (Ar)Montant (Ar)
' . esc($details['product_name']) . '' . esc($details['marque']) . '' . esc($details['numero_moteur']) . '' . esc($details['numero_chassis']) . '' . esc($details['puissance']) . '' . number_format($prixAffiche, 0, '', ' ') . '' . esc($qty) . '' . number_format($prixUnitaire, 0, '', ' ') . '' . number_format($prixTotal, 0, '', ' ') . '
'; } // Récapitulatif pour cette facture - $itemHT = $prixAffiche / 1.20; - $itemTVA = $prixAffiche - $itemHT; + $itemHT = $prixTotal / 1.20; + $itemTVA = $prixTotal - $itemHT; echo ''; echo ''; echo ''; - echo ''; + echo ''; echo ''; if (!empty($order_data['order_payment_mode'])) { @@ -3011,33 +3067,61 @@ public function print31(int $id) echo '
Total HT :' . number_format($itemHT, 0, '', ' ') . ' Ar
TVA (20%) :' . number_format($itemTVA, 0, '', ' ') . ' Ar
Total TTC :' . number_format($prixAffiche, 0, '', ' ') . ' Ar
Total TTC :' . number_format($prixTotal, 0, '', ' ') . ' Ar
Statut :' . $paid_status . '
'; echo ''; echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo ''; foreach ($items as $item) { $details = $this->getOrderItemDetails($item); if (!$details) continue; - $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + $qty = isset($item['qty']) ? (int)$item['qty'] : 1; + + if ($discount > 0) { + $prixTotal = $discount; + $prixUnitaire = $discount / $qty; + } else { + $prixUnitaire = $details['prix'] / $qty; + $prixTotal = $details['prix']; + } echo ''; echo ''; echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo ''; } echo '
DésignationProduit à compléterPrix (Ar)QuantitéPrix Unit. (Ar)Montant (Ar)
' . esc($details['product_name']) . '                    ' . number_format($prixAffiche, 0, '', ' ') . '' . esc($qty) . '' . number_format($prixUnitaire, 0, '', ' ') . '' . number_format($prixTotal, 0, '', ' ') . '
'; } else { echo ''; - echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; + echo ''; echo ''; foreach ($items as $item) { $details = $this->getOrderItemDetails($item); if (!$details) continue; - $prixAffiche = ($discount > 0) ? $discount : $details['prix']; + $qty = isset($item['qty']) ? (int)$item['qty'] : 1; + + if ($discount > 0) { + $prixTotal = $discount; + $prixUnitaire = $discount / $qty; + } else { + $prixUnitaire = $details['prix'] / $qty; + $prixTotal = $details['prix']; + } $categoryName = 'Non définie'; if ($details['type'] === 'terre' && !empty($item['product_id'])) { @@ -3055,7 +3139,9 @@ public function print31(int $id) echo ''; echo ''; echo ''; - echo ''; + echo ''; + echo ''; + echo ''; echo ''; } diff --git a/app/Controllers/ReportController.php b/app/Controllers/ReportController.php index 97edd024..74fa56ec 100644 --- a/app/Controllers/ReportController.php +++ b/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 { @@ -35,6 +38,7 @@ class ReportController extends AdminController $Store = new Stores(); $parking_data = $Reports->getOrderData($today_year); $data['report_years'] = $Reports->getOrderYear(); + // Process the parking data and calculate total amounts $final_parking_data = []; @@ -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 + ' VALIDÉE', // 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 + ' VALIDÉE', // 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); + } } \ No newline at end of file diff --git a/app/Controllers/SecuriteController.php b/app/Controllers/SecuriteController.php index 335ad936..cb2fbdce 100644 --- a/app/Controllers/SecuriteController.php +++ b/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(); diff --git a/app/Controllers/SortieCaisseController.php b/app/Controllers/SortieCaisseController.php index 99a91c63..e31db383 100644 --- a/app/Controllers/SortieCaisseController.php +++ b/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.
' . + 'Disponible: ' . number_format($fonds_disponible, 0, ',', ' ') . ' Ar
' . + 'Demandé: ' . number_format($montant_retire, 0, ',', ' ') . ' Ar

' . + 'Soldes actuels:
' . + '• Espèce: ' . number_format($total_espece_disponible, 0, ',', ' ') . ' Ar
' . + '• MVOLA: ' . number_format($total_mvola_disponible, 0, ',', ' ') . ' Ar
' . + '• 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
" . + $message = "💰 Décaissement payé - " . $montant . " Ar (" . $mode_paiement . ")
" . "Motif: " . $decaissement['motif'] . "
" . "Caissière: " . $users['firstname'] . ' ' . $users['lastname'] . "
" . - "Store: " . $this->returnStoreName($decaissement['store_id']); + "Store: " . $this->returnStoreName($decaissement['store_id']) . "
" . + "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 PAYÉ
' . - 'Tous les Direction, DAF et SuperAdmin ont été notifiés.
' . - 'Montant: ' . number_format($decaissement['montant_retire'], 0, ',', ' ') . ' Ar' + 'Montant: ' . number_format($decaissement['montant_retire'], 0, ',', ' ') . ' Ar (' . $mode_paiement . ')
' . + 'Nouveau solde ' . $mode_paiement_label . ': ' . number_format($fonds_disponible - $montant_retire, 0, ',', ' ') . ' Ar
' . + 'Tous les Direction, DAF et SuperAdmin ont été notifiés.' ]); } 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.
' . - 'Disponible: ' . number_format($fonds_disponible, 0, ',', ' ') . ' Ar
' . - 'Demandé: ' . number_format($montant_retire, 0, ',', ' ') . ' Ar

' . - 'Soldes actuels:
' . - '• Espèce: ' . number_format($total_espece_disponible, 0, ',', ' ') . ' Ar
' . - '• MVOLA: ' . number_format($total_mvola_disponible, 0, ',', ' ') . ' Ar
' . - '• 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']) . "
" . "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
' . 'Mode de paiement: ' . $mode_paiement . '
' . - 'Nouveau solde ' . $mode_paiement_label . ': ' . number_format($fonds_disponible - $montant_retire, 0, ',', ' ') . ' Ar
' . - 'Notification envoyée à tous les Direction, DAF et SuperAdmin' + 'En attente de validation' ]); } 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,131 +951,9 @@ 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.
' . - 'Disponible: ' . number_format($fonds_disponible, 0, ',', ' ') . ' Ar
' . - 'Demandé: ' . number_format($montant_retire, 0, ',', ' ') . ' Ar' - ]); - } + // ✅ SUPPRESSION: Plus de vérification des fonds à la modification + // La vérification se fera uniquement au moment du paiement - // PRÉPARATION DES DONNÉES - $data = [ 'montant_retire' => $montant_retire, 'date_retrait' => date('Y-m-d H:i:s'), @@ -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
Store: " . $this->returnStoreName($store_id); + $message = "✅ Le décaissement a été validé par la Direction
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
Store: " . $this->returnStoreName($store_id) . "
Raison: " . $this->request->getPost('admin_raison'); + $message = "❌ Le décaissement a été refusé par la Direction
Store: " . $this->returnStoreName($store_id) . "
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
Store: " . $this->returnStoreName($store_id); + $message = "⏳ Le décaissement a été mis en attente par la Direction
Store: " . $this->returnStoreName($store_id); $Notification->createNotification($message, "Caissière", (int)$store_id, 'sortieCaisse'); break; } diff --git a/app/Models/AutresEncaissements.php b/app/Models/AutresEncaissements.php new file mode 100644 index 00000000..1fa45cf1 --- /dev/null +++ b/app/Models/AutresEncaissements.php @@ -0,0 +1,302 @@ +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(); + } +} \ No newline at end of file diff --git a/app/Models/Avance.php b/app/Models/Avance.php index e0cad91d..7804e4d6 100644 --- a/app/Models/Avance.php +++ b/app/Models/Avance.php @@ -11,8 +11,9 @@ 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) { try { @@ -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(); - - // ✅ 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 - ]; - } - - return $result; - - } catch (\Exception $e) { - log_message('error', 'Erreur getPaymentModesAvance: ' . $e->getMessage()); +// ✅ 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']); + + 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']); + } + + $result = $builder->get()->getRowObject(); + + // ✅ 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 { diff --git a/app/Models/OrderItems.php b/app/Models/OrderItems.php index 48ec685d..6382c784 100644 --- a/app/Models/OrderItems.php +++ b/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) { diff --git a/app/Models/Orders.php b/app/Models/Orders.php index cfddc929..788f9f88 100644 --- a/app/Models/Orders.php +++ b/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']); diff --git a/app/Models/SortieCaisse.php b/app/Models/SortieCaisse.php index 1d59cda3..0cff65fe 100644 --- a/app/Models/SortieCaisse.php +++ b/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) { diff --git a/app/Models/Users.php b/app/Models/Users.php index e9b4d12b..2e5998e8 100644 --- a/app/Models/Users.php +++ b/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 diff --git a/app/Views/autres_encaissements/index.php b/app/Views/autres_encaissements/index.php new file mode 100644 index 00000000..a488a716 --- /dev/null +++ b/app/Views/autres_encaissements/index.php @@ -0,0 +1,978 @@ + + + +
+ +
+
+

+ Autres Encaissements +

+

+ Gestion des encaissements divers (Plastification, Duplicata, etc.) +

+
+
+ + +
+
+ + +
+
+
+ +
0 Ar
+
Total Encaissements
+
+
+
+
+ +
0
+
Encaissements Aujourd'hui
+
+
+
+ + + +
+
+
+

Ajouter un Encaissement

+ +
+
+
+
+ + +
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+ + +
+
+ + +
+
+
+ +
+
+
+ + +
+
+
+ +
+ + +
+ +
+
+
+ + + +
+
+
+
+
+ + +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+
+
+
+
+ + +
+
+
+
+

+ Historique des Encaissements +

+
+
+
+
NomMarqueCatégorieN° MoteurChâssisPuissancePrix (Ar)NomMarqueCatégorieN° MoteurChâssisPuissanceQtéPrix Unit. (Ar)Montant (Ar)
' . esc($details['numero_moteur']) . '' . esc($details['numero_chassis']) . '' . esc($details['puissance']) . '' . number_format($prixAffiche, 0, '', ' ') . '' . esc($qty) . '' . number_format($prixUnitaire, 0, '', ' ') . '' . number_format($prixTotal, 0, '', ' ') . '
+ + + + + + + + + + + + + + + + +
IDTypeMontantMode CommentaireCréé parMagasinDateActions
+ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/Views/avances/avance.php b/app/Views/avances/avance.php index f18a8368..2cec4e4e 100644 --- a/app/Views/avances/avance.php +++ b/app/Views/avances/avance.php @@ -1,5 +1,12 @@ +get('user'); +$isAdmin = isset($users['group_name']) && in_array($users['group_name'], ['SuperAdmin', 'Direction', 'DAF']); +$isCommerciale = isset($users['group_name']) && in_array($users['group_name'], ['COMMERCIALE']); +$isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Caissière']); +?>
@@ -21,9 +28,20 @@

+
+ +
+ +
+ +
-
@@ -36,74 +54,55 @@ -

- -get('user'); -$isAdmin = isset($users['group_name']) && in_array($users['group_name'], ['SuperAdmin', 'Direction', 'DAF']); -$isCommerciale = isset($users['group_name']) && in_array($users['group_name'], ['COMMERCIALE']); -$isCaissier = isset($users['group_name']) && in_array($users['group_name'], ['Caissière']); -?> - - -
-
- -
-
- + + +
+
+ +
+
+ +
-

Liste des avances

+
+

Liste des avances

+
- get('user'); - $isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction','DAF']); - $isCommerciale = in_array($users['group_name'], ['COMMERCIALE']); - $isCaissier = in_array($users['group_name'], ['Caissière']); - if ($isAdmin): ?> - - - - - - - - - - - - - + + + + + + + + + + + + + + - get('user'); - $isAdmin = in_array($users['group_name'], ['SuperAdmin', 'Direction', 'DAF']); - $isCommerciale = in_array($users['group_name'], ['COMMERCIALE']); - $isCaissier = in_array($users['group_name'], ['Caissière']); - if ($isCommerciale || $isCaissier): ?> - - - - - - - - - - + + + + + + + + + + + + @@ -422,56 +421,51 @@ var base_url = "", brutCreate = 0, brutEdit = 0; // 🔥 FONCTIONS DE FORMATAGE DES NOMBRES // ===================================================== -// Fonction pour formater les nombres avec séparateurs de milliers function formatNumber(number) { if (!number && number !== 0) return ''; - - // Convertir en string et nettoyer var numStr = number.toString().replace(/\s/g, ''); - - // Vérifier que c'est un nombre valide if (!/^\d+$/.test(numStr)) return number; - - // Formater avec espaces return numStr.replace(/\B(?=(\d{3})+(?!\d))/g, " "); } -// Fonction pour enlever les espaces et convertir en nombre function unformatNumber(formattedNumber) { if (!formattedNumber) return 0; return parseFloat(formattedNumber.toString().replace(/\s/g, '')) || 0; } -// Fonction pour gérer le formatage en temps réel function handleNumberFormat(input) { - // Sauvegarder la position du curseur var cursorPosition = input.selectionStart; var originalLength = input.value.length; - - // Récupérer la valeur et enlever les espaces var rawValue = input.value.replace(/\s/g, ''); - // Formater seulement si c'est un nombre valide if (rawValue === '' || /^\d+$/.test(rawValue)) { var formattedValue = formatNumber(rawValue); input.value = formattedValue; - - // Ajuster la position du curseur var newLength = formattedValue.length; var lengthDiff = newLength - originalLength; var newCursorPosition = cursorPosition + lengthDiff; - - // S'assurer que la nouvelle position est valide input.setSelectionRange(newCursorPosition, newCursorPosition); } } +function rebuildTableHeaders(headers) { + var thead = ''; + headers.forEach(function(header) { + thead += ''; + }); + thead += ''; + + $('#avanceTable').html(thead + ''); +} -$(document).ready(function() { +// ===================================================== +// 🔥 DOCUMENT READY +// ===================================================== +$(document).ready(function() { $('#avance_menu').addClass("active"); $('.select2').select2(); - // 📌 Configuration langue FR + // Configuration langue française DataTable var datatableLangFr = { lengthMenu: "Afficher _MENU_ enregistrements par page", zeroRecords: "Aucun résultat trouvé", @@ -480,14 +474,14 @@ $(document).ready(function() { infoFiltered: "(filtré depuis _MAX_ enregistrements au total)", search: "Rechercher :", paginate: { - first: "Premier", - last: "Dernier", - next: "Suivant", + first: "Premier", + last: "Dernier", + next: "Suivant", previous: "Précédent" } }; - // 📌 Fonction pour initialiser la DataTable + // Fonction pour initialiser la DataTable function initAvanceTable(url, columns) { if ($.fn.DataTable.isDataTable('#avanceTable')) { $('#avanceTable').DataTable().destroy(); @@ -499,55 +493,24 @@ $(document).ready(function() { }); } - // 🔄 FONCTION DE MISE À JOUR DYNAMIQUE DE LA DATATABLE + // Fonction de rafraîchissement function refreshDataTable() { if (typeof manageTable !== 'undefined' && manageTable) { manageTable.ajax.reload(null, false); } } - // ✅ INITIALISATION : Définir "terre" par défaut au chargement du modal création - $('#createModal').on('show.bs.modal', function() { - $('#create_avance_form')[0].reset(); - - // Par défaut : mode "terre" - $('#product_select_container').show(); - $('#id_product').prop('required', true); - $('#product_text_container').hide(); - $('#product_name_text').prop('required', false); - $('#gross_amount').prop('readonly', true).prop('type', 'text'); - $('#commentaire_container').hide(); - - // Réattacher les événements - $('#avance_amount').off('keyup').on('keyup', updateDueCreate); - }); - - // 🔥 ÉVÉNEMENTS DE FORMATAGE EN TEMPS RÉEL - $('#avance_amount').on('input', function() { - handleNumberFormat(this); - updateDueCreate(); - }); - - $('#gross_amount').on('input', function() { - if ($('#type_avance').val() === 'mere') { - handleNumberFormat(this); - updateDueCreate(); - } - }); - - $('#avance_amount_edit').on('input', function() { - handleNumberFormat(this); - updateDueEdit(); - }); + // Variables de rôles + var isCaissier = ; + var isCommerciale = ; + var isAdmin = ; - $('#gross_amount_edit').on('input', function() { - if ($('#type_avance_edit').val() === 'mere') { - handleNumberFormat(this); - updateDueEdit(); - } - }); + // ===================================================== + // 🔥 INITIALISATION DES COLONNES SELON LE RÔLE + // ===================================================== + // Colonnes pour ADMIN var adminColumns = [ { title: "Client" }, { title: "Téléphone" }, @@ -573,26 +536,30 @@ $(document).ready(function() { }, { title: "Date" } - ,{ title: "Action", orderable: false, searchable: false } + ,{ title: "Action", orderable: false, searchable: false } ]; var manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns); - $('#avance_order').on('click', function () { - manageTable = initAvanceTable(base_url + 'avances/fetchAvanceBecameOrder', adminColumns); + $('#avance_no_order').on('click', function() { + $('#table-title').text('Avances Incomplètes'); + manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns); }); - $('#avance_expired').on('click', function () { - manageTable = initAvanceTable(base_url + 'avances/fetchExpiredAvance', adminColumns); + $('#avance_order').on('click', function() { + $('#table-title').text('Avances Complètes'); + manageTable = initAvanceTable(base_url + 'avances/fetchAvanceBecameOrder', adminColumns); }); - $('#avance_no_order').on('click', function () { - manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', adminColumns); + $('#avance_expired').on('click', function() { + $('#table-title').text('Avances Expirées'); + manageTable = initAvanceTable(base_url + 'avances/fetchExpiredAvance', adminColumns); }); - + - + + // Colonnes pour COMMERCIAL/CAISSIÈRE var userColumns = [ { title: "#" }, { title: "Produit" }, @@ -610,47 +577,258 @@ $(document).ready(function() { }, { title: "Date" } - ,{ title: "Action", orderable: false, searchable: false } + ,{ title: "Action", orderable: false, searchable: false } ]; var manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', userColumns); - $('#avance_order').on('click', function () { - manageTable = initAvanceTable(base_url + 'avances/fetchAvanceBecameOrder', userColumns); + $('#avance_no_order').on('click', function() { + console.log('🔍 Clic sur Avances Incomplètes'); + + $('#table-title').text('Avances Incomplètes'); + + // Détruire la table + if ($.fn.DataTable.isDataTable('#avanceTable')) { + $('#avanceTable').DataTable().destroy(); + } + + // ✅ Reconstruire les headers (format caissière/commercial) + rebuildTableHeaders([ + '#', + 'Produit', + 'Avance', + 'Reste à payer', + 'Date', + 'Action' + ]); + + // Initialiser + manageTable = $('#avanceTable').DataTable({ + ajax: { + url: base_url + 'avances/fetchAvanceData', + type: 'GET', + dataSrc: 'data' + }, + columns: userColumns, + language: datatableLangFr }); + + console.log('✅ DataTable Incomplètes initialisée'); +}); - $('#avance_no_order').on('click', function () { - manageTable = initAvanceTable(base_url + 'avances/fetchAvanceData', userColumns); +$('#avance_order').on('click', function() { + console.log('🔍 Clic sur Avances Complètes'); + + $('#table-title').text('Avances Complètes'); + + if ($.fn.DataTable.isDataTable('#avanceTable')) { + $('#avanceTable').DataTable().destroy(); + } + + rebuildTableHeaders([ + '#', + 'Produit', + 'Avance', + 'Reste à payer', + 'Date', + 'Action' + ]); + + manageTable = $('#avanceTable').DataTable({ + ajax: { + url: base_url + 'avances/fetchAvanceBecameOrder', + type: 'GET', + dataSrc: 'data' + }, + columns: userColumns, + language: datatableLangFr }); - $('#avance_expired').on('click', function () { - manageTable = initAvanceTable(base_url + 'avances/fetchExpiredAvance', userColumns); + console.log('✅ DataTable Complètes initialisée'); +}); + +$('#avance_expired').on('click', function() { + console.log('🔍 Clic sur Avances Expirées'); + + $('#table-title').text('Avances Expirées'); + + if ($.fn.DataTable.isDataTable('#avanceTable')) { + $('#avanceTable').DataTable().destroy(); + } + + rebuildTableHeaders([ + '#', + 'Produit', + 'Avance', + 'Reste à payer', + 'Date', + 'Action' + ]); + + manageTable = $('#avanceTable').DataTable({ + ajax: { + url: base_url + 'avances/fetchExpiredAvance', + type: 'GET', + dataSrc: 'data' + }, + columns: userColumns, + language: datatableLangFr }); + + console.log('✅ DataTable Expirées initialisée'); +}); + +// Charger le compteur d'avances en attente +loadPendingCount(); +setInterval(loadPendingCount, 30000); + +console.log('✅ Module caissière initialisé'); + // ===================================================== + // 🔥 ONGLET "EN ATTENTE VALIDATION" (CAISSIÈRE UNIQUEMENT) + // ===================================================== + + +var pendingColumns = [ + { title: "#" }, + { title: "Client" }, + { title: "Téléphone" }, + { title: "Produit" }, + { + title: "Prix", + render: function(data) { return formatNumber(data); } + }, + { + title: "Avance", + render: function(data) { return formatNumber(data); } + }, + { title: "Date" }, + { title: "Action", orderable: false, searchable: false } +]; + +// ✅ CORRECTION COMPLÈTE : Reconstruire le tableau avant d'initialiser DataTables +$('#avance_pending').on('click', function() { + console.log('🔍 Clic sur bouton En attente validation'); + + $('#table-title').text('Avances en attente de validation'); + + // ✅ 1. Détruire complètement la DataTable existante + if ($.fn.DataTable.isDataTable('#avanceTable')) { + $('#avanceTable').DataTable().destroy(); + } + + // ✅ 2. Reconstruire les headers du tableau HTML + rebuildTableHeaders([ + '#', + 'Client', + 'Téléphone', + 'Produit', + 'Prix', + 'Avance', + 'Date', + 'Action' + ]); + + // ✅ 3. Initialiser la nouvelle DataTable + manageTable = $('#avanceTable').DataTable({ + ajax: { + url: base_url + 'avances/fetchPendingValidation', + type: 'POST', + dataSrc: 'data', + error: function(xhr, error, code) { + console.error('❌ Erreur AJAX:', error); + console.error('❌ Statut HTTP:', xhr.status); + console.error('❌ Réponse:', xhr.responseText); + + Swal.fire({ + icon: 'error', + title: 'Erreur de chargement', + html: '

Impossible de charger les avances en attente

' + + '

Erreur : ' + error + '

' + + '

Code HTTP : ' + xhr.status + '

', + confirmButtonText: 'OK' + }); + } + }, + columns: pendingColumns, + language: datatableLangFr, + order: [[0, 'desc']] + }); + + console.log('✅ DataTable "En attente validation" initialisée'); +}); + + // Charger le compteur d'avances en attente + loadPendingCount(); + setInterval(loadPendingCount, 30000); // Actualiser toutes les 30 secondes + + + // ===================================================== + // 🔥 MODAL CRÉATION - INITIALISATION + // ===================================================== + + $('#createModal').on('show.bs.modal', function() { + $('#create_avance_form')[0].reset(); + + // Par défaut : mode "terre" + $('#product_select_container').show(); + $('#id_product').prop('required', true); + $('#product_text_container').hide(); + $('#product_name_text').prop('required', false); + $('#gross_amount').prop('readonly', true).prop('type', 'text'); + $('#commentaire_container').hide(); + + // Réattacher les événements + $('#avance_amount').off('keyup').on('keyup', updateDueCreate); + }); + + // ===================================================== + // 🔥 ÉVÉNEMENTS DE FORMATAGE EN TEMPS RÉEL + // ===================================================== + + $('#avance_amount').on('input', function() { + handleNumberFormat(this); + updateDueCreate(); + }); + + $('#gross_amount').on('input', function() { + if ($('#type_avance').val() === 'mere') { + handleNumberFormat(this); + updateDueCreate(); + } + }); + + $('#avance_amount_edit').on('input', function() { + handleNumberFormat(this); + updateDueEdit(); + }); + + $('#gross_amount_edit').on('input', function() { + if ($('#type_avance_edit').val() === 'mere') { + handleNumberFormat(this); + updateDueEdit(); + } + }); + // ===================================================== // 🔥 GESTION DU TYPE D'AVANCE - CRÉATION // ===================================================== + $('#type_avance').on('change', function() { var typeAvance = $(this).val(); if (typeAvance === 'mere') { - // MASQUER le select de produit $('#product_select_container').hide(); $('#id_product').prop('required', false).val(''); - // AFFICHER le champ texte pour produit $('#product_text_container').show(); $('#product_name_text').prop('required', true); - // RENDRE le prix modifiable ET changer en type="number" $('#gross_amount').prop('readonly', false).val('').prop('required', true); - - // AFFICHER le commentaire $('#commentaire_container').show(); - // Calcul simple : Prix - Avance $('#avance_amount, #gross_amount').off('keyup').on('keyup', function() { var prix = unformatNumber($('#gross_amount').val()); var avance = unformatNumber($('#avance_amount').val()); @@ -659,27 +837,20 @@ $(document).ready(function() { }); } else if (typeAvance === 'terre') { - // AFFICHER le select de produit $('#product_select_container').show(); $('#id_product').prop('required', true); - // MASQUER le champ texte $('#product_text_container').hide(); $('#product_name_text').prop('required', false).val(''); - // RENDRE le prix readonly ET en type="text" $('#gross_amount').prop('readonly', true).prop('required', false); - - // MASQUER le commentaire $('#commentaire_container').hide(); $('#commentaire').val(''); - // Restaurer le comportement normal avec validation 25% $('#avance_amount').off('keyup').on('keyup', updateDueCreate); $('#gross_amount').off('keyup'); } - // Réinitialiser les champs $('#gross_amount').val(''); $('#avance_amount').val(''); $('#amount_due').val(''); @@ -688,6 +859,7 @@ $(document).ready(function() { // ===================================================== // 🔥 GESTION DU TYPE D'AVANCE - ÉDITION // ===================================================== + $('#type_avance_edit').on('change', function() { var typeAvance = $(this).val(); @@ -724,12 +896,14 @@ $(document).ready(function() { } }); - // ✅ CRÉATION avec actualisation automatique + // ===================================================== + // 🔥 SOUMISSION FORMULAIRE CRÉATION + // ===================================================== + $('#create_avance_form').on('submit', function(e) { e.preventDefault(); const $form = $(this); - // Désformatage des valeurs avant soumission var brut = unformatNumber($('#gross_amount').val()); var avance = unformatNumber($('#avance_amount').val()); var typeAvance = $('#type_avance').val(); @@ -748,7 +922,6 @@ $(document).ready(function() { } } - // Validation pour "mère" if (typeAvance === 'mere') { if (!$('#product_name_text').val() || brut === 0 || avance === 0) { Swal.fire({ @@ -762,14 +935,13 @@ $(document).ready(function() { $form.find('button[type="submit"]').prop('disabled', true).text('Enregistrement...'); - // Cloner le formulaire et désformater les valeurs var formData = new FormData(this); formData.set('gross_amount', brut); formData.set('avance_amount', avance); formData.set('amount_due', unformatNumber($('#amount_due').val())); $.ajax({ - url: '/avances/createAvance', + url: base_url + 'avances/createAvance', type: 'POST', data: formData, processData: false, @@ -808,11 +980,14 @@ $(document).ready(function() { }); }); + // ===================================================== // 🔥 SUPPRESSION + // ===================================================== + window.removeFunc = function(id, product_id) { $('#removeModal').modal('show'); $('#removeForm input[name="avance_id"]').val(id); - $('#removeForm input[name="product_id"]').val(product_id); + $('#removeForm input[name="product_id"]').val(product_id || 0); }; $('#removeForm').on('submit', function(e) { @@ -867,15 +1042,14 @@ $(document).ready(function() { return false; }); -}); // Fin document.ready +}); // FIN DOCUMENT READY // ===================================================== -// 🔥 FONCTIONS UTILITAIRES CORRIGÉES +// 🔥 FONCTIONS UTILITAIRES // ===================================================== function getProductDataCreate() { var id = $('#id_product').val(); - console.log('🔄 getProductDataCreate appelé, ID produit:', id); if (!id) { brutCreate = 0; @@ -886,18 +1060,12 @@ function getProductDataCreate() { } $.post(base_url + 'orders/getProductValueById', { product_id: id }, function(r) { - console.log('📦 Données reçues:', r); - brutCreate = parseFloat(r.prix_vente) || 0; - console.log('💰 Prix brut:', brutCreate); - // ✅ FORMATER les valeurs avant de les afficher var prixFormate = formatNumber(brutCreate.toFixed(0)); - var avance25 = brutCreate * 0.5; - var avanceFormatee = formatNumber(avance25.toFixed(0)); - var resteFormate = formatNumber((brutCreate - avance25).toFixed(0)); - - console.log('🎯 Valeurs formatées - Prix:', prixFormate, 'Avance:', avanceFormatee, 'Reste:', resteFormate); + var avance50 = brutCreate * 0.5; + var avanceFormatee = formatNumber(avance50.toFixed(0)); + var resteFormate = formatNumber((brutCreate - avance50).toFixed(0)); $('#gross_amount').val(prixFormate); $('#avance_amount').val(avanceFormatee); @@ -917,7 +1085,6 @@ function updateDueCreate() { function getProductDataUpdate() { var id = $('#id_product_edit').val(); - console.log('🔄 getProductDataUpdate appelé, ID produit:', id); if (!id) { brutEdit = 0; @@ -933,18 +1100,12 @@ function getProductDataUpdate() { data: { product_id: id }, dataType: 'json', success: function(r) { - console.log('📦 Données reçues (update):', r); - brutEdit = parseFloat(r.prix_vente) || 0; - console.log('💰 Prix brut (update):', brutEdit); - // ✅ FORMATER les valeurs avant de les afficher var prixFormate = formatNumber(brutEdit.toFixed(0)); - var avance25 = brutEdit * 0.5; - var avanceFormatee = formatNumber(avance25.toFixed(0)); - var resteFormate = formatNumber((brutEdit - avance25).toFixed(0)); - - console.log('🎯 Valeurs formatées (update) - Prix:', prixFormate, 'Avance:', avanceFormatee, 'Reste:', resteFormate); + var avance50 = brutEdit * 0.5; + var avanceFormatee = formatNumber(avance50.toFixed(0)); + var resteFormate = formatNumber((brutEdit - avance50).toFixed(0)); $('#gross_amount_edit').val(prixFormate); $('#avance_amount_edit').val(avanceFormatee); @@ -962,57 +1123,244 @@ function updateDueEdit() { $('#amount_due_edit').val(formatNumber(reste.toFixed(0))); } -// ✅ Fonction pour afficher le modal de prévisualisation (CAISSIÈRE) -window.viewFunc = function(avance_id) { +// ===================================================== +// 🔥 FONCTION MODIFICATION +// ===================================================== + +function editFunc(id) { + $('#update_avance_form')[0].reset(); + $.ajax({ - url: base_url + 'avances/getInvoicePreview/' + avance_id, + url: base_url + 'avances/fetchSingleAvance/' + id, type: 'GET', dataType: 'json', - beforeSend: function() { - $('#invoice-preview-container').html('

Chargement...

'); - $('#viewModal').modal('show'); - }, - success: function(response) { - if (response.success) { - $('#invoice-preview-container').html(response.html); - - // ✅ Stocker l'ID et vérifier si l'utilisateur peut imprimer - $('#viewModal').data('avance-id', response.avance_id); - $('#viewModal').data('can-print', response.can_print); - - // ✅ Afficher/Masquer le bouton selon le droit d'impression - if (response.can_print === true) { - $('#viewModal .modal-footer .btn-primary').show(); - } else { - $('#viewModal .modal-footer .btn-primary').hide(); - } - } else { - $('#invoice-preview-container').html('
Erreur lors du chargement de la facture
'); - } + success: function(r) { + populateEditModal(r, id); }, error: function() { - $('#invoice-preview-container').html('
Erreur de connexion
'); + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: 'Impossible de récupérer les données de l\'avance' + }); } }); -}; +} -// ✅ MODIFIER la fonction printFromModal -window.printFromModal = function() { - var avanceId = $('#viewModal').data('avance-id'); - var canPrint = $('#viewModal').data('can-print'); +function populateEditModal(r, id) { + $('#avance_id_edit').val(r.avance_id || r.id || id); + $('#customer_name_avance_edit').val(r.customer_name || ''); + $('#customer_phone_avance_edit').val(r.customer_phone || ''); + $('#customer_address_avance_edit').val(r.customer_address || r.customer_adress || ''); + $('#customer_cin_avance_edit').val(r.customer_cin || ''); + $('#type_avance_edit').val(r.type_avance || ''); + $('#type_payment_edit').val(r.type_payment || ''); - if (!avanceId) { - Swal.fire({ - icon: 'error', - title: 'Erreur', - text: 'ID de facture non trouvé' - }); - return; - } + var grossAmount = r.gross_amount || r.product_price || 0; + $('#gross_amount_edit').val(formatNumber(grossAmount)); + brutEdit = parseFloat(grossAmount); - // ✅ Vérifier que l'utilisateur a le droit d'imprimer - if (canPrint !== true) { - Swal.fire({ + $('#avance_amount_edit').val(formatNumber(r.avance_amount || '')); + $('#amount_due_edit').val(formatNumber(r.amount_due || '')); + $('#commentaire_edit').val(r.commentaire || ''); + + if (r.type_avance === 'mere') { + var productNameMer = r.product_name || ''; + $('#product_name_text_edit').val(productNameMer); + } else if (r.type_avance === 'terre') { + var productId = r.product_id || r.id_product; + + if (productId) { + setTimeout(function() { + $('#id_product_edit').val(productId); + + if ($('#id_product_edit option:selected').val() != productId) { + var productNameFromDB = r.product_name_db || 'Produit #' + productId; + $('#id_product_edit').append( + $('') + .attr('value', productId) + .attr('selected', 'selected') + .text(productNameFromDB + ' (Actuellement sélectionné)') + ); + } + }, 100); + } + } + + $('#updateModal').modal('show'); + + $('#updateModal').on('shown.bs.modal', function(e) { + $('#type_avance_edit').trigger('change'); + $(this).off('shown.bs.modal'); + }); + + $('#update_avance_form').off('submit').on('submit', function(e) { + e.preventDefault(); + + var $form = $(this); + var $submitBtn = $form.find('button[type="submit"]'); + var typeAvance = $('#type_avance_edit').val(); + var avance = unformatNumber($('#avance_amount_edit').val()); + var brut = unformatNumber($('#gross_amount_edit').val()); + + if (typeAvance === 'terre') { + var minAvance = brutEdit * 0.5; + if (avance < minAvance) { + Swal.fire({ + icon: 'error', + title: 'Avance insuffisante', + html: `L'avance doit être au minimum de 50% du prix du produit (${formatNumber(minAvance.toFixed(0))} Ar).`, + confirmButtonText: 'OK', + confirmButtonColor: '#d33' + }); + return; + } + } + + if (typeAvance === 'mere') { + if (!$('#product_name_text_edit').val() || brut === 0 || avance === 0) { + Swal.fire({ + icon: 'warning', + title: 'Champs manquants', + text: 'Veuillez remplir tous les champs : produit, prix et avance.' + }); + return; + } + } + + $submitBtn.prop('disabled', true).text('Modification...'); + + var formData = new FormData(this); + formData.set('gross_amount_edit', brut); + formData.set('avance_amount_edit', avance); + formData.set('amount_due_edit', unformatNumber($('#amount_due_edit').val())); + + $.ajax({ + url: base_url + 'avances/updateAvance', + type: 'POST', + data: formData, + processData: false, + contentType: false, + success: function(res) { + if (res.success === true) { + $('#updateModal').modal('hide'); + + if (res.converted === true && res.redirect_url) { + Swal.fire({ + icon: 'success', + title: '🎉 Conversion automatique !', + html: '
' + + '

✅ Avance modifiée avec succès !

' + + '

🔄 L\'avance sur terre étant complète, elle a été automatiquement convertie en commande.

' + + '
' + + '

N° Commande : ' + (res.bill_no || 'N/A') + '

' + + '

Vous allez être redirigé vers la commande...

' + + '
', + timer: 4000, + timerProgressBar: true, + showConfirmButton: false, + allowOutsideClick: false + }).then(() => { + window.location.href = res.redirect_url; + }); + } else { + Swal.fire({ + icon: 'success', + title: 'Succès', + text: res.messages, + timer: 2000, + showConfirmButton: false + }).then(() => { + location.reload(); + }); + } + } else { + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: res.messages + }); + } + }, + error: function(xhr, status, error) { + console.error('Erreur AJAX:', error); + Swal.fire({ + icon: 'error', + title: 'Erreur de connexion', + text: 'Impossible de modifier l\'avance' + }); + }, + complete: function() { + $submitBtn.prop('disabled', false).text('Modifier'); + } + }); + }); +} + +// ===================================================== +// 🔥 FONCTION VISUALISATION FACTURE +// ===================================================== + +window.viewFunc = function(avance_id) { + if (!avance_id) { + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: 'ID avance manquant' + }); + return; + } + + $.ajax({ + url: base_url + 'avances/getInvoicePreview/' + avance_id, + type: 'GET', + dataType: 'json', + beforeSend: function() { + $('#invoice-preview-container').html('

Chargement...

'); + $('#viewModal').modal('show'); + }, + success: function(response) { + if (response.success) { + $('#invoice-preview-container').html(response.html); + + $('#viewModal').data('avance-id', response.avance_id); + $('#viewModal').data('can-print', response.can_print); + + if (response.can_print === true) { + $('#btnPrintInvoice').show(); + } else { + $('#btnPrintInvoice').hide(); + } + } else { + $('#invoice-preview-container').html('
' + (response.messages || 'Erreur lors du chargement') + '
'); + } + }, + error: function(xhr, status, error) { + console.error('Erreur viewFunc:', error); + $('#invoice-preview-container').html('
Erreur de connexion au serveur
'); + } + }); +}; + +// ===================================================== +// 🔥 IMPRESSION DEPUIS MODAL +// ===================================================== + +window.printFromModal = function() { + var avanceId = $('#viewModal').data('avance-id'); + var canPrint = $('#viewModal').data('can-print'); + + if (!avanceId) { + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: 'ID de facture non trouvé' + }); + return; + } + + if (canPrint !== true) { + Swal.fire({ icon: 'warning', title: 'Accès refusé', text: 'Seule la caissière peut imprimer les factures' @@ -1020,7 +1368,6 @@ window.printFromModal = function() { return; } - // ✅ Envoyer la notification ET marquer comme imprimée $.ajax({ url: base_url + 'avances/notifyPrintInvoice', type: 'POST', @@ -1028,7 +1375,6 @@ window.printFromModal = function() { dataType: 'json', success: function(response) { if (response.success) { - // ✅ Notification envoyée et avance marquée comme imprimée printInvoiceContent(avanceId); } else { Swal.fire({ @@ -1048,9 +1394,7 @@ window.printFromModal = function() { }); }; -// ✅ NOUVELLE FONCTION pour récupérer et imprimer le contenu function printInvoiceContent(avanceId) { - // Afficher un loader Swal.fire({ title: 'Préparation de l\'impression...', allowOutsideClick: false, @@ -1061,7 +1405,6 @@ function printInvoiceContent(avanceId) { } }); - // Récupérer le HTML complet de la facture pour impression $.ajax({ url: base_url + 'avances/getFullInvoiceForPrint/' + avanceId, type: 'GET', @@ -1070,7 +1413,6 @@ function printInvoiceContent(avanceId) { if (response.success) { Swal.close(); - // Créer un iframe caché pour l'impression var printFrame = $('') .attr('id', 'print-frame') .css({ @@ -1082,7 +1424,6 @@ function printInvoiceContent(avanceId) { }) .appendTo('body'); - // Écrire le contenu HTML dans l'iframe var frameDoc = printFrame[0].contentWindow || printFrame[0].contentDocument; if (frameDoc.document) frameDoc = frameDoc.document; @@ -1090,34 +1431,27 @@ function printInvoiceContent(avanceId) { frameDoc.write(response.html); frameDoc.close(); - // Attendre que le contenu soit chargé puis imprimer setTimeout(function() { try { printFrame[0].contentWindow.focus(); printFrame[0].contentWindow.print(); - // ✅ FERMER LE MODAL AVANT D'AFFICHER LE SWEET ALERT $('#viewModal').modal('hide'); - // ✅ ATTENDRE UN PEU POUR LAISSER LE MODAL SE FERMER setTimeout(function() { - // ✅ Afficher Sweet Alert de succès Swal.fire({ icon: 'success', title: 'Impression réussie !', html: '

La facture a été imprimée avec succès.

' + - '

La Direction a été notifiée.

' + - '

Le bouton d\'impression ne sera plus disponible pour cette avance.

', + '

La Direction a été notifiée.

', confirmButtonText: 'OK', confirmButtonColor: '#28a745', allowOutsideClick: false }).then(() => { - // ✅ Rafraîchir la page pour mettre à jour l'affichage location.reload(); }); }, 500); - // Supprimer l'iframe après impression setTimeout(function() { printFrame.remove(); }, 1000); @@ -1151,231 +1485,102 @@ function printInvoiceContent(avanceId) { }); } -// ✅ Nettoyer l'iframe si le modal est fermé $('#viewModal').on('hidden.bs.modal', function() { $('#print-frame').remove(); }); -// Remplacer la partie concernant le bouton View par : -function buildViewButton(value) { - var session = get('user')); ?>; - var isCaissier = ; - - if (isCaissier && ) { - return ' '; - } - return ''; -} - -// ✅ MODIFICATION -function editFunc(id) { - $('#update_avance_form')[0].reset(); - - $.ajax({ - url: base_url + 'avances/fetchSingleAvance/' + id, - type: 'GET', - dataType: 'json', - success: function(r) { - // ✅ Vérifier si l'avance était imprimée - var wasPrinted = r.is_printed == 1; - - if (wasPrinted) { - Swal.fire({ - icon: 'info', - title: 'Information', - html: '

Cette avance a déjà été imprimée.

' + - '

Après modification, elle devra être imprimée à nouveau.

', - confirmButtonText: 'Compris', - confirmButtonColor: '#007bff' - }).then(() => { - populateEditModal(r, id); - }); - } else { - populateEditModal(r, id); - } - }, - error: function() { - Swal.fire({ - icon: 'error', - title: 'Erreur', - text: 'Impossible de récupérer les données de l\'avance' - }); - } - }); -} - -// ✅ FONCTION AMÉLIORÉE pour remplir le modal d'édition -function populateEditModal(r, id) { - - $('#avance_id_edit').val(r.avance_id || r.id || id); - $('#customer_name_avance_edit').val(r.customer_name || ''); - $('#customer_phone_avance_edit').val(r.customer_phone || ''); - $('#customer_address_avance_edit').val(r.customer_address || r.customer_adress || ''); - $('#customer_cin_avance_edit').val(r.customer_cin || ''); - $('#type_avance_edit').val(r.type_avance || ''); - $('#type_payment_edit').val(r.type_payment || ''); - - // ✅ CORRECTION : Récupérer le prix depuis la BDD et FORMATER - var grossAmount = r.gross_amount || r.product_price || 0; - $('#gross_amount_edit').val(formatNumber(grossAmount)); - brutEdit = parseFloat(grossAmount); - - // ✅ FORMATER les valeurs d'avance et reste à payer - $('#avance_amount_edit').val(formatNumber(r.avance_amount || '')); - $('#amount_due_edit').val(formatNumber(r.amount_due || '')); - $('#commentaire_edit').val(r.commentaire || ''); - - // ✅ Gérer le produit selon le type - if (r.type_avance === 'mere') { - var productNameMer = r.product_name || ''; - $('#product_name_text_edit').val(productNameMer); - } else if (r.type_avance === 'terre') { - var productId = r.product_id || r.id_product; - - if (productId) { - setTimeout(function() { - $('#id_product_edit').val(productId); - - if ($('#id_product_edit option:selected').val() == productId) { - console.log('Produit trouvé et sélectionné:', productId); - } else { - console.warn('Produit non trouvé dans le select:', productId); - var productNameFromDB = r.product_name_db || 'Produit #' + productId; - $('#id_product_edit').append( - $('') - .attr('value', productId) - .attr('selected', 'selected') - .text(productNameFromDB + ' (Actuellement sélectionné)') - ); - } - }, 100); - } - } - - $('#updateModal').modal('show'); - - // ✅ Déclencher le change pour afficher les bons champs - $('#updateModal').on('shown.bs.modal', function(e) { - $('#type_avance_edit').trigger('change'); - $(this).off('shown.bs.modal'); - }); - - // ✅ GESTIONNAIRE DE SOUMISSION AMÉLIORÉ AVEC CONVERSION AUTOMATIQUE - $('#update_avance_form').off('submit').on('submit', function(e) { - e.preventDefault(); - - var $form = $(this); - var $submitBtn = $form.find('button[type="submit"]'); - var typeAvance = $('#type_avance_edit').val(); - var avance = unformatNumber($('#avance_amount_edit').val()); - var brut = unformatNumber($('#gross_amount_edit').val()); - - // ✅ VALIDATION pour avance sur terre - if (typeAvance === 'terre') { - var minAvance = brutEdit * 0.5; - if (avance < minAvance) { - Swal.fire({ - icon: 'error', - title: 'Avance insuffisante', - html: `L'avance doit être au minimum de 50% du prix du produit (${formatNumber(minAvance.toFixed(0))} Ar).`, - confirmButtonText: 'OK', - confirmButtonColor: '#d33' - }); - return; - } - } - - // ✅ VALIDATION pour avance sur mer - if (typeAvance === 'mere') { - if (!$('#product_name_text_edit').val() || brut === 0 || avance === 0) { - Swal.fire({ - icon: 'warning', - title: 'Champs manquants', - text: 'Veuillez remplir tous les champs : produit, prix et avance.' - }); - return; - } - } - - $submitBtn.prop('disabled', true).text('Modification...'); - - // Désformater les valeurs avant envoi - var formData = new FormData(this); - formData.set('gross_amount_edit', brut); - formData.set('avance_amount_edit', avance); - formData.set('amount_due_edit', unformatNumber($('#amount_due_edit').val())); +// ===================================================== +// 🔥 VALIDATION AVANCE (CAISSIÈRE) +// ===================================================== - $.ajax({ - url: base_url + 'avances/updateAvance', - type: 'POST', - data: formData, - processData: false, - contentType: false, - success: function(res) { - if (res.success === true) { - $('#updateModal').modal('hide'); - - // ✅ NOUVEAU : Gestion de la conversion automatique - if (res.converted === true && res.redirect_url) { - Swal.fire({ - icon: 'success', - title: '🎉 Conversion automatique !', - html: '
' + - '

✅ Avance modifiée avec succès !

' + - '

🔄 L\'avance sur terre étant complète, elle a été automatiquement convertie en commande.

' + - '
' + - '

N° Commande : ' + (res.bill_no || 'N/A') + '

' + - '

Vous allez être redirigé vers la commande...

' + - '
', - timer: 4000, - timerProgressBar: true, - showConfirmButton: false, - allowOutsideClick: false - }).then(() => { - window.location.href = res.redirect_url; - }); - } else { - // ✅ Modification simple sans conversion +window.validateAvanceFunc = function(avance_id) { + Swal.fire({ + title: 'Valider cette avance ?', + html: '

En validant cette avance :

' + + '
    ' + + '
  • ✅ Le montant sera ajouté à la caisse
  • ' + + '
  • ✅ Le commercial sera notifié
  • ' + + '
  • ✅ L\'avance ne pourra plus être modifiée par le commercial
  • ' + + '
', + icon: 'question', + showCancelButton: true, + confirmButtonText: ' Valider', + cancelButtonText: 'Annuler', + confirmButtonColor: '#28a745', + cancelButtonColor: '#6c757d' + }).then((result) => { + if (result.isConfirmed) { + $.ajax({ + url: base_url + 'avances/validateAvance', + type: 'POST', + data: { avance_id: avance_id }, + dataType: 'json', + success: function(response) { + if (response.success) { Swal.fire({ icon: 'success', - title: 'Succès', - text: res.messages, + title: 'Validée !', + text: response.messages, timer: 2000, showConfirmButton: false }).then(() => { location.reload(); }); + } else { + Swal.fire({ + icon: 'error', + title: 'Erreur', + text: response.messages + }); } - } else { + }, + error: function() { Swal.fire({ icon: 'error', - title: 'Erreur', - text: res.messages + title: 'Erreur de connexion', + text: 'Impossible de valider l\'avance' }); } - }, - error: function(xhr, status, error) { - console.error('Erreur AJAX:', error); - Swal.fire({ - icon: 'error', - title: 'Erreur de connexion', - text: 'Impossible de modifier l\'avance' - }); - }, - complete: function() { - $submitBtn.prop('disabled', false).text('Modifier'); + }); + } + }); +}; + +// ===================================================== +// 🔥 CHARGER COMPTEUR AVANCES EN ATTENTE +// ===================================================== + +function loadPendingCount() { + $.ajax({ + url: base_url + 'avances/fetchPendingValidation', + type: 'POST', // ✅ CORRECTION + dataType: 'json', + success: function(response) { + var count = response.data ? response.data.length : 0; + $('#pending-count').text(count); + + if (count > 0) { + $('#pending-count').removeClass('badge-light').addClass('badge-danger'); + $('#avance_pending').addClass('btn-pulse'); + } else { + $('#pending-count').removeClass('badge-danger').addClass('badge-light'); + $('#avance_pending').removeClass('btn-pulse'); } - }); + }, + error: function() { + console.error('Erreur chargement compteur avances en attente'); + } }); } -// ✅ NOUVELLE FONCTION : Traiter les avances expirées +// ===================================================== +// 🔥 TRAITER AVANCES EXPIRÉES (ADMIN) +// ===================================================== + function processExpiredAvances() { var btn = $('#btnProcessExpired'); var originalText = btn.html(); - // Confirmation avec SweetAlert Swal.fire({ title: 'Traiter les avances expirées ?', html: '
' + @@ -1401,14 +1606,9 @@ function processExpiredAvances() { }); } -/** - * ✅ Exécuter le traitement des avances expirées - */ function executeProcessExpired(btn, originalText) { - // Désactiver le bouton et afficher un loader btn.prop('disabled', true).html(' Traitement en cours...'); - // Appel AJAX $.ajax({ url: base_url + 'avances/processExpiredAvances', type: 'POST', @@ -1421,7 +1621,6 @@ function executeProcessExpired(btn, originalText) { message += '
⚠️ ' + response.errors + ' erreur(s) rencontrée(s)'; } - // Afficher les détails dans un tableau var detailsHtml = ''; if (response.details && response.details.length > 0) { detailsHtml = '
' + @@ -1450,7 +1649,6 @@ function executeProcessExpired(btn, originalText) { detailsHtml += '
ClientTéléphoneAdresseProduitPrixAvanceReste à payerDateAction
ClientTéléphoneAdresseProduitPrixAvanceReste à payerDateAction
#ProduitAvanceReste à payerDateAction
#ProduitAvanceReste à payerDateAction
' + header + '
'; } - // Afficher le résultat Swal.fire({ icon: 'success', title: '✅ Traitement terminé !', @@ -1459,7 +1657,6 @@ function executeProcessExpired(btn, originalText) { confirmButtonColor: '#28a745', width: '700px' }).then(() => { - // Rafraîchir la page pour voir les changements location.reload(); }); @@ -1481,20 +1678,25 @@ function executeProcessExpired(btn, originalText) { }); }, complete: function() { - // Réactiver le bouton btn.prop('disabled', false).html(originalText); } }); } - -// ✅ TEST DU FORMATAGE AU CHARGEMENT -$(document).ready(function() { - console.log('🧪 Test formatage - 1000:', formatNumber(1000)); // Doit afficher "1 000" - console.log('🧪 Test formatage - 25000:', formatNumber(25000)); // Doit afficher "25 000" - console.log('🧪 Test désformatage - "1 000":', unformatNumber("1 000")); // Doit afficher 1000 -}); - \ No newline at end of file diff --git a/app/Views/dashboard.php b/app/Views/dashboard.php index d5ddd411..17df9e38 100644 --- a/app/Views/dashboard.php +++ b/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; + } +} @@ -79,28 +304,110 @@
-
-
-
- -
- Total Orders (Ventes complètes) - Ar +
+
+
+
+

+ Résumé de Trésorerie +

-
-
- -
-
- -
- Total Avances (Paiements partiels) - Ar + +
+ +
+ +
+
+ 💰 Total Brut +
+ Ar +
+ Orders + Avances +
+
+ + +
+
+
+ + +
+
+ 💸 Décaissements +
+ Ar +
+ Espèce + MVOLA + Virement +
+
+ + +
+
=
+
+ + +
+
+ ✅ Solde Net +
+ Ar +
+ Disponible en caisse +
+
+
+ + +
+ + +
+ +
+
+
+ +
+
+ Ar +
+
💵 Espèce Disponible
+
+
+ + +
+
+
+ +
+
+ Ar +
+
📱 MVOLA Disponible
+
+
+ + +
+
+
+ +
+
+ Ar +
+
🏦 Banque Disponible
+
+
+
-
@@ -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 @@
-
+ +
+
+
+
+
+
+

+ Performances des commerciaux +

+
+
-
-
-
-
-

- Performances des commercials -

-
-
- -
-
-
- - - -
-
- - -
-
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - - -
Nom et prénomEmailMotos vendueDate de ventePrix d'achatPrix de ventePoint de ventesBénefices
Total :
-
-
+ +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + +
Nom et prénomEmailMotos vendueDate de ventePrix d'achatPrix de ventePoint de ventesBénéfices
Total :
+
+
+
+
+
+
@@ -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'); diff --git a/app/Views/groups/edit.php b/app/Views/groups/edit.php index 8266ee31..4f32b39f 100644 --- a/app/Views/groups/edit.php +++ b/app/Views/groups/edit.php @@ -459,7 +459,33 @@ } } ?>> - + + Autres encaissements + > + > + > + > + Remise Ajouter une réparation

+ + +
+
+

Filtres

+
+
+
+
+ + +
+
+ + +
+ +
+ + +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
+ Total Réparations + 0 +
+
+
+
+
+ +
+ En Cours + 0 +
+
+
+
+
+ +
+ Réparées + 0 +
+
+
+
+
+ +
+ Non Réparées + 0 +
+
+
+
+ +

Gérer les Réparations

@@ -45,7 +129,8 @@ Image Motos - Username + Mécanicien + Magasin Statut Observation Date de début @@ -55,273 +140,325 @@ -
-
- - - -