Sarobidy22 1 month ago
parent
commit
67b33ad2e6
  1. 39
      app/Config/Routes.php
  2. 3
      app/Controllers/AdminController.php
  3. 1560
      app/Controllers/AvanceController.php
  4. 110
      app/Controllers/Dashboard.php
  5. 21
      app/Controllers/NotificationController.php
  6. 651
      app/Controllers/OrderController.php
  7. 12
      app/Controllers/ProductCOntroller.php
  8. 194
      app/Helpers/alerts_helper.php
  9. 372
      app/Models/Avance.php
  10. 124
      app/Models/Products.php
  11. 885
      app/Views/avances/avance.php
  12. 275
      app/Views/dashboard.php
  13. 239
      app/Views/login.php
  14. 238
      app/Views/orders/createbyid.php
  15. 474
      app/Views/orders/edit.php
  16. 2
      app/Views/products/create.php
  17. 5
      app/Views/products/index.php
  18. 1
      app/Views/templates/header.php
  19. 40
      app/Views/templates/header_menu.php

39
app/Config/Routes.php

@ -57,6 +57,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->post('/ventes/moreimage/(:num)', [Auth::class, 'uploadImagePub']);
$routes->post('/ventes/moreimage/supp/(:num)', [Auth::class, 'delete']);
/**
* route to logout
*/
@ -176,6 +177,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
// $routes->get('generateqrcode/(:num)', [QrCodeController::class, 'generate']);
$routes->post('assign_store', [ProductCOntroller::class, 'assign_store']);
$routes->post('createByExcel', [ProductCOntroller::class, 'createByExcel']);
});
/**
@ -261,6 +263,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->group('/notifications', function ($routes) {
$routes->get('/', [NotificationController::class, 'getNotification']);
$routes->post('markAsRead/(:num)', [NotificationController::class, 'markAsRead']);
$routes->post('markAllAsRead', [NotificationController::class, 'markAllAsRead']);
});
// routes for sortie caisse
$routes->group('/sortieCaisse', function ($routes) {
@ -288,19 +291,37 @@ $routes->group('/sortieCaisse', function ($routes) {
$routes->post('updateRemise/(:num)', [RemiseController::class, 'updateRemise']);
});
// avance
$routes->group('/avances', function ($routes) {
// avance
$routes->group('/avances', function ($routes) {
$routes->get('/', [AvanceController::class, 'index']);
// ✅ Routes pour récupérer les données (GET)
$routes->get('fetchAvanceData', [AvanceController::class, 'fetchAvanceData']);
$routes->get('fetchCompletedAvances', [AvanceController::class, 'fetchCompletedAvances']);
$routes->get('fetchIncompleteAvances', [AvanceController::class, 'fetchIncompleteAvances']);
$routes->get('fetchAvanceBecameOrder', 'AvanceController::fetchAvanceBecameOrder');
$routes->get('fetchExpiredAvance', [AvanceController::class, 'fetcheExpiredAvance']);
$routes->get('fetchSingleAvance/(:num)', [AvanceController::class, 'fetchSingleAvance']);
$routes->get('fetchAvanceBecameOrder', [AvanceController::class, 'fetchAvanceBecameOrder']);
$routes->get('fetchExpiredAvance', [AvanceController::class, 'fetchExpiredAvance']);
// Routes pour une 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
$routes->post('createAvance', [AvanceController::class, 'createAvance']);
$routes->post('updateAvance', [AvanceController::class, 'updateAvance']);
$routes->post('deleteAvance', [AvanceController::class, 'removeAvance']);
$routes->post('updateAvance/(:num)', [AvanceController::class, 'updateAvance']);
});
$routes->post('notifyPrintInvoice', [AvanceController::class, 'notifyPrintInvoice']);
// ✅ AJOUTER CETTE ROUTE MANQUANTE
$routes->post('processExpiredAvances', [AvanceController::class, 'processExpiredAvances']);
// ✅ Route CRON (optionnel)
$routes->get('checkDeadlineAlerts', [AvanceController::class, 'checkDeadlineAlerts']);
$routes->post('payAvance', 'AvanceController::payAvance');
$routes->get('forceConvertToOrder/(:num)', 'AvanceController::forceConvertToOrder/$1');
$routes->post('checkAndConvertCompleted', 'AvanceController::checkAndConvertCompleted');
});
// historique
$routes->group('historique', ['filter' => 'auth'], static function ($routes) {
$routes->get('/', 'HistoriqueController::index');

3
app/Controllers/AdminController.php

@ -12,7 +12,6 @@ use CodeIgniter\Logger\LoggerInterface;
abstract class AdminController extends BaseController
{
protected $permission = [];
public function __construct()
{
if (empty(session()->get('user'))) {
@ -25,10 +24,10 @@ abstract class AdminController extends BaseController
$group_data = $Groups->getUserGroupByUserId($userId);
$this->permission = unserialize($group_data['permission']);
}
}
/**
* finction to verify role of users
* @return mixed

1560
app/Controllers/AvanceController.php

File diff suppressed because it is too large

110
app/Controllers/Dashboard.php

@ -21,6 +21,10 @@ class Dashboard extends AdminController
public function index()
{
// === 🔥 Récupérer l'utilisateur en premier ===
$session = session();
$user_id = $session->get('user');
$productModel = new Products();
$orderModel = new Orders();
$userModel = new Users();
@ -65,49 +69,82 @@ 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 ===
$total_mvola = $mv1_orders + $mv2_orders + $mv_avances;
$total_espece = $es1_orders + $es2_orders + $es_avances;
$total_vb = $vb1_orders + $vb2_orders + $vb_avances;
$total = $total_orders + $total_avances;
// === AJUSTEMENTS AVEC RECOUVREMENTS ET SORTIES (PAR MODE DE PAIEMENT) ===
$total_mvola_final = $total_mvola -
$me -
$mb +
$bm -
$total_sortie_mvola;
$total_espece_final = $total_espece +
$me +
$be -
$total_sortie_espece;
$total_virement_bancaire_final = $total_vb -
$be -
$bm +
$mb -
$total_sortie_virement;
// === 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;
// === AJUSTEMENTS AVEC RECOUVREMENTS ET SORTIES ===
$total_mvola_final = $total_mvola_brut - $me - $mb + $bm - $total_sortie_mvola;
$total_espece_final = $total_espece_brut + $me + $be - $total_sortie_espece;
$total_virement_bancaire_final = $total_vb_brut - $be - $bm + $mb - $total_sortie_virement;
// === CALCUL DU TOTAL GÉNÉRAL ===
// ✅ CORRECTION: Ne PAS additionner les recouvrements au total
// Les recouvrements sont des transferts internes, pas des entrées d'argent
$total_sortie_global = $total_sortie_espece + $total_sortie_mvola + $total_sortie_virement;
$total_final = $total - $total_sortie_global;
// check avance expired
$avance = new Avance();
$avance->checkExpiredAvance();
$total_final = $total_brut - $total_sortie_global;
// ✅ MODIFICATION : La caissière voit EXACTEMENT les mêmes montants que Direction/Conseil
$data = [
// === POUR DIRECTION/CONSEIL (AVEC TOUS LES AJUSTEMENTS) ===
'total' => $total_final,
'total_mvola' => $total_mvola_final,
'total_espece' => $total_espece_final,
'total_virement_bancaire' => $total_virement_bancaire_final,
'user_permission' => $this->permission,
// === POUR CAISSIÈRE (MÊME CALCUL QUE DIRECTION) ===
'total_caisse' => $total_final, // ← Identique à 'total'
'total_mvola_caisse' => $total_mvola_final, // ← Identique à 'total_mvola'
'total_espece_caisse' => $total_espece_final, // ← Identique à 'total_espece'
'total_vb_caisse' => $total_virement_bancaire_final, // ← Identique à 'total_virement_bancaire'
// === DÉTAIL POUR LA CAISSIÈRE ===
'total_orders_only' => $total_orders, // Ventes complètes uniquement
'total_avances' => $total_avances, // Avances uniquement
// ✅ Détails des sorties
'total_sorties' => $total_sortie_global,
'total_sortie_espece' => $total_sortie_espece,
'total_sortie_mvola' => $total_sortie_mvola,
'total_sortie_virement' => $total_sortie_virement,
// ✅ Détails des recouvrements
'recouvrement_me' => $me, // Mvola → Espèce
'recouvrement_be' => $be, // Banque → Espèce
'recouvrement_bm' => $bm, // Banque → Mvola
'recouvrement_mb' => $mb, // Mvola → Banque
'total_recouvrements' => $me + $be + $bm + $mb,
// Détail avances par mode de paiement
'total_avances_mvola' => $mv_avances,
'total_avances_espece' => $es_avances,
'total_avances_virement' => $vb_avances,
// Détail orders par mode de paiement
'total_mvola_orders' => $mv1_orders + $mv2_orders,
'total_espece_orders' => $es1_orders + $es2_orders,
'total_vb_orders' => $vb1_orders + $vb2_orders,
// ✅ Montants bruts (avant recouvrements et sorties)
'total_brut' => $total_brut,
'total_mvola_brut' => $total_mvola_brut,
'total_espece_brut' => $total_espece_brut,
'total_vb_brut' => $total_vb_brut,
];
$data['total_products'] = $productModel->countTotalProducts();
// === ✅ Compter les produits selon le store de l'utilisateur ===
$data['total_products'] = $productModel->countProductsByUserStore();
// === ✅ Récupérer le nom du store pour l'affichage ===
$isAdmin = in_array($user_id['group_name'], ['DAF', 'Direction']);
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';
} else {
$data['store_name'] = 'Tous les magasins';
}
$data['total_paid_orders'] = $orderModel->countTotalPaidOrders();
$data['total_users'] = $userModel->countTotalUsers();
$data['total_stores'] = $storeModel->countTotalStores();
@ -175,16 +212,14 @@ class Dashboard extends AdminController
$data['count_id'] = $countId;
// Check if the user is an Conseil
$session = session();
$user_id = $session->get('user');
$data['is_admin'] = false;
$data['isCommercial'] = false;
$data['isChef'] = false;
$data['isCaissier'] = false;
$data['isMecanicien'] = false;
$data['isSecurite'] = false;
if ($user_id['group_name'] == "Direction" || $user_id['group_name'] == "Conseil") {
if ($user_id['group_name'] == "Direction" || $user_id['group_name'] == "DAF") {
$data['is_admin'] = true;
}
@ -203,6 +238,9 @@ class Dashboard extends AdminController
if ($user_id['group_name'] == "MECANICIEN") {
$data['isMecanicien'] = true;
}
if ($user_id['group_name'] == "Sécurité" || $user_id['group_name'] == "SECURITE") {
$data['isSecurite'] = true;
}
$data['page_title'] = 'Dashboard';
$data['marques_total'] = json_encode($orderModel->getTotalProductvente());

21
app/Controllers/NotificationController.php

@ -43,4 +43,25 @@ class NotificationController extends AdminController
$Notification->insertNotification($data);
}
// Marquer toutes les notifications comme lues pour l'utilisateur connecté
public function markAllAsRead()
{
$Notification = new Notification();
$session = session();
$users = $session->get('user');
// Mettre à jour toutes les notifications non lues pour ce store et ce groupe
$builder = $Notification->builder();
$builder->where('store_id', $users['store_id'])
->groupStart()
->where('forgroup', $users['group_name'])
->orWhere('forgroup', strtolower('TOUS'))
->groupEnd()
->where('is_read', 0)
->set(['is_read' => 1])
->update();
return $this->response->setJSON(['status' => 'success']);
}
}

651
app/Controllers/OrderController.php

File diff suppressed because it is too large

12
app/Controllers/ProductCOntroller.php

@ -82,7 +82,15 @@ class ProductCOntroller extends AdminController
return "$name";
}
$data = $Products->getProductData();
// ✅ Utiliser la nouvelle méthode qui filtre automatiquement par rôle et store
$data = $Products->getProductDataByRole();
// ✅ Debug : Logs pour vérifier le filtrage
$session = session();
$user = $session->get('user');
log_message('debug', '=== fetchProductData ===');
log_message('debug', 'User: ' . ($user['username'] ?? 'N/A') . ' (Group: ' . ($user['group_name'] ?? 'N/A') . ', Store: ' . ($user['store_id'] ?? 'N/A') . ')');
log_message('debug', 'Products found: ' . count($data));
foreach ($data as $key => $value) {
// Gestion du nom du magasin
@ -139,7 +147,7 @@ class ProductCOntroller extends AdminController
'<div class="no-image">Aucune image</div>';
$result['data'][$key] = [
$imageHtml, // Correction : utiliser $imageHtml au lieu de $value['image']
$imageHtml,
convertString($value['sku']),
$value['name'],
$value['prix_vente'],

194
app/Helpers/alerts_helper.php

@ -4,13 +4,14 @@ use App\Models\Users;
use App\Models\Avance;
use App\Models\AlertMail;
/**
* Vérifier les deadlines et envoyer des alertes email
*/
function checkDeadlineAlerts()
{
log_message('info', "=== DÉBUT checkDeadlineAlerts ===");
$cacheFile = WRITEPATH . 'cache/check_deadline_last_run.txt';
// On enlève la vérification de 24h pour s'assurer que le script tourne quotidiennement
file_put_contents($cacheFile, time());
$avanceModel = new Avance();
@ -20,15 +21,16 @@ function checkDeadlineAlerts()
$today = date('Y-m-d');
log_message('info', "Date du jour: {$today}");
// Modification pour vérifier les avances dans 0-3 jours
// Récupération des avances dans 0-3 jours
$avances = $avanceModel
->where('DATE(deadline) >=', $today) // Inclut le jour même
->where('DATE(deadline) >=', $today)
->where('DATE(deadline) <=', date('Y-m-d', strtotime('+3 days')))
->where('active', 1)
->findAll();
log_message('info', "Nombre d'avances trouvées (0-3 jours): " . count($avances));
// Récupération des utilisateurs DAF
$users = $usersModel->select('users.email, users.firstname, users.lastname')
->join('user_group', 'user_group.user_id = users.id')
->join('groups', 'groups.id = user_group.group_id')
@ -56,7 +58,7 @@ function checkDeadlineAlerts()
log_message('info', "Avance ID: {$avance['avance_id']}, Deadline: {$deadline}, Jours restants: {$daysLeft}");
// Modification des types d'alerte pour 0, 1, 2, 3 jours
// Détermination du type d'alerte
$alertType = match($daysLeft) {
3 => 'deadline_3_days',
2 => 'deadline_2_days',
@ -83,10 +85,12 @@ function checkDeadlineAlerts()
continue;
}
// Message modifié pour inclure le cas du jour même
// Construction du message
$urgencyText = $daysLeft === 0 ? "ÉCHÉANCE AUJOURD'HUI" : "{$daysLeft} jour(s) restant(s)";
$message = "
<h3>⚠️ URGENT : Avance approchant de la deadline</h3>
<html>
<body style='font-family: Arial, sans-serif; color: #333;'>
<h3 style='color: #d9534f;'>⚠️ URGENT : Avance approchant de la deadline</h3>
<p><strong>ID Avance :</strong> {$avance['avance_id']}</p>
<p><strong>Client :</strong> {$avance['customer_name']}</p>
<p><strong>Montant avance :</strong> " . number_format($avance['avance_amount'], 0, ',', ' ') . " Ar</p>
@ -95,19 +99,21 @@ function checkDeadlineAlerts()
<p><strong>Statut :</strong> <span style='color: red; font-weight: bold;'>{$urgencyText}</span></p>
<p><strong>Téléphone client :</strong> {$avance['customer_phone']}</p>
<p><strong>Adresse client :</strong> {$avance['customer_address']}</p>
<hr>
<hr style='border: 1px solid #ddd;'>
<p><em>Cette avance " . ($daysLeft === 0 ? "arrive à échéance aujourd'hui" : "arrivera à échéance dans {$daysLeft} jour(s)") . ". Action requise immédiatement.</em></p>
</body>
</html>
";
$emailsSent = 0;
foreach ($emails as $to) {
log_message('info', "Tentative d'envoi email à: {$to}");
$subject = $daysLeft === 0
? "⚠️ AVANCE URGENTE - ÉCHÉANCE AUJOURD'HUI"
: "⚠️ AVANCE URGENTE - {$daysLeft} jour(s) restant(s)";
if (sendEmailInBackground($to, $subject, $message)) {
foreach ($emails as $to) {
log_message('info', "Tentative d'envoi email à: {$to}");
if (sendEmailWithBrevo($to, $subject, $message)) {
$emailsSent++;
log_message('info', "Email envoyé avec succès à: {$to}");
} else {
@ -115,6 +121,7 @@ function checkDeadlineAlerts()
}
}
// Enregistrement de l'alerte si au moins un email a été envoyé
if ($emailsSent > 0) {
log_message('info', "Insertion alerte pour avance_id={$avance['avance_id']} avec type {$alertType}");
$alertMailModel->insert([
@ -128,49 +135,182 @@ function checkDeadlineAlerts()
log_message('error', "Aucun email envoyé pour avance_id={$avance['avance_id']} avec type {$alertType}");
}
}
checkAndConvertCompletedAvances();
log_message('info', "=== FIN checkDeadlineAlerts ===");
// ✅ NOUVELLE FONCTIONNALITÉ : Gérer les avances expirées
handleExpiredAvances();
}
function sendEmailInBackground($to, $subject, $message)
/**
* ✅ NOUVELLE FONCTION : Gérer automatiquement les avances expirées
* - Libérer les produits (remettre en stock)
* - Désactiver les avances
*/
function handleExpiredAvances()
{
log_message('info', "=== DÉBUT handleExpiredAvances ===");
$avanceModel = new Avance();
$productsModel = new \App\Models\Products();
$today = date('Y-m-d');
log_message('info', "Vérification des avances expirées au: {$today}");
// Récupérer les avances expirées et encore actives
$expiredAvances = $avanceModel
->where('DATE(deadline) <', $today)
->where('active', 1)
->where('is_order', 0)
->findAll();
log_message('info', "Nombre d'avances expirées trouvées: " . count($expiredAvances));
if (empty($expiredAvances)) {
log_message('info', "Aucune avance expirée à traiter");
log_message('info', "=== FIN handleExpiredAvances ===");
return;
}
$processedCount = 0;
$errorCount = 0;
foreach ($expiredAvances as $avance) {
try {
log_message('info', "Préparation envoi email à: {$to}");
log_message('info', "Traitement avance expirée ID: {$avance['avance_id']}, Client: {$avance['customer_name']}, Deadline: {$avance['deadline']}");
// ✅ Désactiver l'avance
$updateResult = $avanceModel->update($avance['avance_id'], ['active' => 0]);
if (!$updateResult) {
log_message('error', "Échec désactivation avance ID: {$avance['avance_id']}");
$errorCount++;
continue;
}
log_message('info', "Avance ID {$avance['avance_id']} désactivée avec succès");
// ✅ Libérer le produit UNIQUEMENT pour les avances "sur terre" avec product_id
if ($avance['type_avance'] === 'terre' && !empty($avance['product_id'])) {
$productUpdateResult = $productsModel->update($avance['product_id'], ['product_sold' => 0]);
if ($productUpdateResult) {
log_message('info', "Produit ID {$avance['product_id']} libéré (remis en stock)");
} else {
log_message('warning', "Échec libération produit ID: {$avance['product_id']}");
}
} elseif ($avance['type_avance'] === 'mere') {
log_message('info', "Avance 'sur mer' - Pas de produit à libérer (product_name: {$avance['product_name']})");
} else {
log_message('info', "Pas de product_id à libérer pour avance ID: {$avance['avance_id']}");
}
$processedCount++;
} catch (\Exception $e) {
log_message('error', "Erreur traitement avance expirée ID {$avance['avance_id']}: " . $e->getMessage());
$errorCount++;
}
}
log_message('info', "Avances expirées traitées: {$processedCount}, Erreurs: {$errorCount}");
log_message('info', "=== FIN handleExpiredAvances ===");
}
/**
* ✅ Vérifier et convertir automatiquement les avances complètes en commandes
* À appeler dans checkDeadlineAlerts() ou via un cron job
*/
function checkAndConvertCompletedAvances()
{
log_message('info', "=== DÉBUT checkAndConvertCompletedAvances ===");
$avanceModel = new \App\Models\Avance();
// Récupérer toutes les avances complètes non encore converties
$completedAvances = $avanceModel
->where('amount_due', 0)
->where('is_order', 0) // Pas encore converties
->where('active', 1) // Encore actives
->findAll();
log_message('info', "Avances complètes trouvées : " . count($completedAvances));
$convertedCount = 0;
$errorCount = 0;
foreach ($completedAvances as $avance) {
log_message('info', "Traitement avance complète ID: {$avance['avance_id']}, Client: {$avance['customer_name']}");
$order_id = $avanceModel->convertToOrder($avance['avance_id']);
if ($order_id) {
$convertedCount++;
log_message('info', "✅ Avance {$avance['avance_id']} convertie en commande {$order_id}");
} else {
$errorCount++;
log_message('error', "❌ Échec conversion avance {$avance['avance_id']}");
}
}
log_message('info', "Avances converties : {$convertedCount}, Erreurs : {$errorCount}");
log_message('info', "=== FIN checkAndConvertCompletedAvances ===");
}
/**
* Envoyer un email via Brevo
*/
function sendEmailWithBrevo($to, $subject, $message)
{
try {
log_message('info', "Préparation envoi email via Brevo à: {$to}");
$email = \Config\Services::email();
// Configuration Brevo depuis le fichier .env
$config = [
'protocol' => 'smtp',
'SMTPHost' => 'smtp.gmail.com',
'SMTPUser' => 'rey342505@gmail.com',
'SMTPPass' => 'loirqovmfuxnasrm',
'SMTPPort' => 587,
'SMTPCrypto' => 'tls',
'protocol' => env('email.protocol', 'smtp'),
'SMTPHost' => env('email.SMTPHost', 'smtp-relay.brevo.com'),
'SMTPUser' => env('email.SMTPUser'),
'SMTPPass' => env('email.SMTPPass'),
'SMTPPort' => env('email.SMTPPort', 587),
'SMTPCrypto' => env('email.SMTPCrypto', 'tls'),
'mailType' => 'html',
'charset' => 'utf-8',
'newline' => "\r\n"
'newline' => "\r\n",
'wordWrap' => true,
'validation' => true
];
log_message('info', "Configuration Brevo - Host: {$config['SMTPHost']}, Port: {$config['SMTPPort']}, User: {$config['SMTPUser']}");
$email->initialize($config);
$email->setFrom('rey342505@gmail.com', 'Système Motorbike - Alertes Avances');
// Utilisation de l'email configuré dans .env
$fromEmail = env('email.fromEmail', 'noreply@motorbike.mg');
$fromName = env('email.fromName', 'Système Motorbike - Alertes');
$email->setFrom($fromEmail, $fromName);
$email->setTo($to);
$email->setSubject($subject);
$email->setMessage($message);
log_message('info', "Configuration email terminée, tentative d'envoi...");
log_message('info', "Configuration email Brevo terminée, tentative d'envoi...");
if (!$email->send()) {
$debugInfo = $email->printDebugger(['headers']);
log_message('error', "Erreur email à {$to}: " . print_r($debugInfo, true));
$debugInfo = $email->printDebugger(['headers', 'subject', 'body']);
log_message('error', "Erreur email Brevo à {$to}: " . print_r($debugInfo, true));
return false;
}
log_message('info', "Email envoyé avec succès à: {$to}");
log_message('info', "Email envoyé avec succès via Brevo à: {$to}");
return true;
} catch (\Exception $e) {
log_message('error', "Exception email à {$to}: " . $e->getMessage());
log_message('error', "Exception email Brevo à {$to}: " . $e->getMessage());
log_message('error', "Stack trace: " . $e->getTraceAsString());
return false;
}
}

372
app/Models/Avance.php

@ -11,17 +11,15 @@ 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' // Ajout du champ type et deadline
'type_avance','type_payment', 'deadline','commentaire','product_name'
];
public function createAvance(array $data) {
try {
// Si la date de création n'est pas définie, on prend aujourd'hui
if (empty($data['avance_date'])) {
$data['avance_date'] = date('Y-m-d');
}
// Calcul de la deadline en fonction du type
if (!empty($data['type'])) {
if (strtolower($data['type']) === 'avance sur terre') {
$data['deadline'] = date('Y-m-d', strtotime($data['avance_date'] . ' +15 days'));
@ -43,7 +41,6 @@ class Avance extends Model {
return false;
}
try {
// Recalcul de la deadline si le type change
if (!empty($data['type']) && !empty($data['avance_date'])) {
if (strtolower($data['type']) === 'avance sur terre') {
$data['deadline'] = date('Y-m-d', strtotime($data['avance_date'] . ' +15 days'));
@ -58,7 +55,6 @@ class Avance extends Model {
}
}
// 📌 Le reste de tes fonctions restent inchangées
public function getAllAvanceData(int $id=null) {
$session = session();
$users = $session->get('user');
@ -115,48 +111,47 @@ class Avance extends Model {
}
public function fetchSingleAvance(int $avance_id){
return $this->where('avance_id',$avance_id)->first();
return $this->select('avances.*, products.name as product_name_db, products.prix_vente as product_price')
->join('products', 'products.id = avances.product_id', 'left')
->where('avances.avance_id', $avance_id)
->first();
}
public function removeAvance(int $avance_id){
return $this->delete($avance_id);
}
// ✅ CORRECTION : getTotalAvance pour la caissière
public function getTotalAvance() {
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']);
if($isAdmin) {
try {
return $this->select('SUM(avance_amount) AS ta')
$builder = $this->select('SUM(avance_amount) AS ta')
->where('is_order', 0)
->get()
->getRowObject();
} catch (\Exception $e) {
log_message('error', 'Erreur lors du total du montant des avances : ' . $e->getMessage());
return false;
->where('active', 1); // ✅ Ajout du filtre active
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']); // ✅ Filtre par store pour caissière
}
} else {
try {
return $this->select('SUM(avance_amount) AS ta')
->where('is_order', 0)
->where('store_id',$users['store_id'])
->get()
->getRowObject();
return $builder->get()->getRowObject();
} catch (\Exception $e) {
log_message('error', 'Erreur lors du total du montant des avances : ' . $e->getMessage());
return false;
}
return (object) ['ta' => 0]; // ✅ Retourner un objet avec ta = 0 en cas d'erreur
}
}
// ✅ CORRECTION PRINCIPALE : getPaymentModesAvance pour la caissière
public function getPaymentModesAvance()
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']);
if ($isAdmin) {
return $this->db->table('avances')
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,
@ -164,9 +159,17 @@ class Avance extends Model {
SUM(CASE WHEN LOWER(type_payment) = "virement bancaire" THEN avance_amount ELSE 0 END) AS total_virement_bancaire
')
->where('active', 1)
->get()
->getRowObject();
} else {
->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,
@ -174,9 +177,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();
@ -221,7 +234,9 @@ class Avance extends Model {
}
try {
return $this
->where('is_order',0)
->where('is_order',1) // ✅ Correction: devrait être 1, pas 0
->where('active',1) // ✅ Ajout du filtre active
->where('store_id',$users['store_id']) // ✅ Ajout du filtre store
->orderBy('avance_date', 'DESC')
->findAll();
} catch (\Exception $e) {
@ -297,15 +312,15 @@ class Avance extends Model {
foreach ($avances as $avance) {
$this->update($avance['avance_id'], ['active' => '0']);
if (!empty($avance['product_id'])) { // ✅ Vérifier que product_id existe
$productModel->update($avance['product_id'], ['product_sold' => 0]);
}
}
}
/**
* Récupérer les avances qui arrivent à échéance dans X jours
*/
public function getAvancesNearDeadline($days = 3)
{
}
public function getAvancesNearDeadline($days = 3)
{
$alertDate = date('Y-m-d', strtotime("+{$days} days"));
return $this->select('avances.*, users.store_id')
@ -315,10 +330,10 @@ public function getAvancesNearDeadline($days = 3)
->where('avances.amount_due >', 0)
->where('DATE(avances.deadline)', $alertDate)
->findAll();
}
// Avances incomplètes (reste à payer > 0 et non transformées en commande)
public function getIncompleteAvances(int $id = null)
{
}
public function getIncompleteAvances(int $id = null)
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']);
@ -336,11 +351,10 @@ public function getIncompleteAvances(int $id = null)
}
return $builder->orderBy('avance_date', 'DESC')->findAll();
}
}
// Avances complètes (reste à payer = 0 et non transformées en commande)
public function getCompletedAvances(int $id = null)
{
public function getCompletedAvances(int $id = null)
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']);
@ -358,9 +372,283 @@ public function getCompletedAvances(int $id = null)
}
return $builder->orderBy('avance_date', 'DESC')->findAll();
}
public function markAsPrinted($avance_id)
{
try {
return $this->update($avance_id, [
'is_printed' => 1
]);
} catch (\Exception $e) {
log_message('error', 'Erreur markAsPrinted: ' . $e->getMessage());
return false;
}
}
/**
* Marquer une avance comme non imprimée (quand elle est modifiée)
*/
public function markAsNotPrinted($avance_id)
{
try {
return $this->update($avance_id, [
'is_printed' => 0,
'last_modified_at' => date('Y-m-d H:i:s')
]);
} catch (\Exception $e) {
log_message('error', 'Erreur markAsNotPrinted: ' . $e->getMessage());
return false;
}
}
/**
* Vérifier si une avance a déjà été imprimée
*/
public function isPrinted($avance_id)
{
try {
$avance = $this->find($avance_id);
return $avance ? (bool)$avance['is_printed'] : false;
} catch (\Exception $e) {
log_message('error', 'Erreur isPrinted: ' . $e->getMessage());
return false;
}
}
/**
* Récupérer un produit avec le nom de sa marque
* @param int $product_id
* @return array|null
*/
public function getProductWithBrand($product_id)
{
try {
return $this->select('products.*, brands.name as brand_name')
->join('brands', 'brands.id = products.marque', 'left')
->where('products.id', $product_id)
->first();
} catch (\Exception $e) {
log_message('error', 'Erreur getProductWithBrand: ' . $e->getMessage());
return null;
}
}
// À ajouter dans App\Models\Avance.php
/**
* ✅ Convertir une avance complète en commande
* Appelé automatiquement quand amount_due atteint 0
*
* @param int $avance_id
* @return int|false ID de la commande créée ou false
*/
public function convertToOrder(int $avance_id)
{
try {
$avance = $this->find($avance_id);
if (!$avance) {
log_message('error', "Avance introuvable : {$avance_id}");
return false;
}
// ✅ MODIFICATION PRINCIPALE : Vérifier que c'est bien une avance sur TERRE
if ($avance['type_avance'] !== 'terre') {
log_message('info', "Avance {$avance_id} de type '{$avance['type_avance']}' - Conversion ignorée (seules les avances TERRE sont converties)");
return false;
}
// ✅ Vérifier que l'avance est bien complète
if ((float)$avance['amount_due'] > 0) {
log_message('warning', "Avance TERRE {$avance_id} non complète (amount_due={$avance['amount_due']})");
return false;
}
// ✅ Vérifier qu'elle n'a pas déjà été convertie
if ((int)$avance['is_order'] === 1) {
log_message('info', "Avance TERRE {$avance_id} déjà convertie en commande");
return false;
}
// ✅ Vérifier que le produit existe (obligatoire pour avance TERRE)
if (empty($avance['product_id'])) {
log_message('error', "Avance TERRE {$avance_id} sans product_id - Impossible de convertir");
return false;
}
$db = \Config\Database::connect();
$db->transStart();
// ✅ 1. Créer la commande
$orderModel = new \App\Models\Orders();
$bill_no = 'BILAV-' . strtoupper(substr(md5(uniqid(mt_rand(), true)), 0, 4));
$orderData = [
'bill_no' => $bill_no,
'customer_name' => $avance['customer_name'],
'customer_address' => $avance['customer_address'],
'customer_phone' => $avance['customer_phone'],
'customer_cin' => $avance['customer_cin'],
'date_time' => date('Y-m-d H:i:s'),
'gross_amount' => $avance['gross_amount'],
'net_amount' => $avance['gross_amount'],
'discount' => 0,
'paid_status' => 2, // En attente validation caissière
'user_id' => $avance['user_id'],
'store_id' => $avance['store_id'],
'service_charge_rate' => 0,
'vat_charge_rate' => 0,
'vat_charge' => 0,
'tranche_1' => $avance['avance_amount'],
'tranche_2' => null,
'order_payment_mode' => $avance['type_payment'] ?? 'En espèce',
'order_payment_mode_1' => null,
];
$order_id = $orderModel->insert($orderData);
if (!$order_id) {
throw new \Exception("Échec création commande pour avance TERRE {$avance_id}");
}
log_message('info', "✅ Commande {$bill_no} créée depuis avance TERRE {$avance_id}");
// ✅ 2. Créer les items de commande
$orderItemModel = new \App\Models\OrderItems();
$productModel = new \App\Models\Products();
$product = $productModel->find($avance['product_id']);
if ($product) {
$orderItemModel->insert([
'order_id' => $order_id,
'product_id' => $avance['product_id'],
'rate' => $avance['gross_amount'],
'qty' => 1,
'amount' => $avance['gross_amount'],
]);
log_message('info', "Item ajouté : produit {$avance['product_id']} (TERRE)");
// ✅ Le produit reste marqué comme vendu (product_sold = 1)
// Il sera géré dans la commande maintenant
} else {
log_message('warning', "Produit {$avance['product_id']} introuvable pour avance TERRE {$avance_id}");
}
// ✅ 3. Marquer l'avance comme convertie
$this->update($avance_id, [
'is_order' => 1,
'active' => 0, // Désactiver l'avance (elle devient commande)
]);
$db->transComplete();
if ($db->transStatus() === false) {
log_message('error', "Transaction échouée pour avance TERRE {$avance_id}");
return false;
}
// ✅ 4. Notification à la caissière
$notificationController = new \App\Controllers\NotificationController();
$notificationController->createNotification(
"Nouvelle commande issue d'une avance TERRE complète - {$bill_no}",
"Caissière",
(int)$avance['store_id'],
"orders"
);
log_message('info', "✅ Avance TERRE {$avance_id} convertie en commande {$order_id} ({$bill_no})");
return $order_id;
} catch (\Exception $e) {
log_message('error', "Erreur conversion avance TERRE→commande : " . $e->getMessage());
return false;
}
}
/**
* ✅ Hook appelé automatiquement lors du paiement d'une avance
* Intégrer ceci dans votre fonction de paiement existante
*/
public function afterPayment(int $avance_id)
{
$avance = $this->find($avance_id);
if (!$avance) {
return false;
}
// ✅ Si l'avance est maintenant complète ET que c'est une avance TERRE
if ((float)$avance['amount_due'] <= 0 && $avance['type_avance'] === 'terre') {
log_message('info', "💰 Avance TERRE {$avance_id} complète ! Conversion automatique en commande...");
return $this->convertToOrder($avance_id);
}
// ✅ Si c'est une avance MER complète, on ne fait rien (elle reste dans la liste)
if ((float)$avance['amount_due'] <= 0 && $avance['type_avance'] === 'mere') {
log_message('info', "💰 Avance MER {$avance_id} complète ! Elle reste dans la liste des avances.");
}
return true;
}
/**
* ✅ Générer un numéro de facture unique
*/
private function generateBillNumber($store_id)
{
$db = \Config\Database::connect();
// Récupérer le dernier numéro pour ce store
$query = $db->query(
"SELECT bill_no FROM orders WHERE store_id = ? ORDER BY id DESC LIMIT 1",
[$store_id]
);
$result = $query->getRow();
if ($result && preg_match('/(\d+)$/', $result->bill_no, $matches)) {
$lastNumber = intval($matches[1]);
$newNumber = $lastNumber + 1;
} else {
$newNumber = 1;
}
// Format: BILL-STORE{store_id}-{number}
return 'BILL-STORE' . $store_id . '-' . str_pad($newNumber, 5, '0', STR_PAD_LEFT);
}
/**
* ✅ Récupérer toutes les avances complètes non converties
*/
public function getCompletedNotConverted()
{
return $this->where('amount_due', 0)
->where('is_order', 0)
->where('active', 1)
->where('type_avance', 'terre') // ✅ Uniquement TERRE à convertir
->findAll();
}
/**
* ✅ NOUVELLE MÉTHODE : Récupérer les avances MER complètes (pour statistiques)
*/
public function getCompletedMerAvances()
{
$session = session();
$users = $session->get('user');
$isAdmin = in_array($users['group_name'], ['Conseil', 'Direction']);
$builder = $this->where('amount_due', 0)
->where('active', 1)
->where('type_avance', 'mere');
if (!$isAdmin) {
$builder->where('store_id', $users['store_id']);
}
return $builder->orderBy('avance_date', 'DESC')->findAll();
}
}

124
app/Models/Products.php

@ -6,14 +6,46 @@ use CodeIgniter\Model;
class Products extends Model
{
/**
* table products
* @var string
*/
protected $table = 'products';
protected $primaryKey = 'id';
protected $allowedFields = ['name', 'sku', 'price', 'product_sold', 'qty', 'image', 'description', 'numero_de_moteur', 'marque', 'chasis', 'store_id', 'availability', 'is_piece', 'prix_vente', 'date_arivage', 'puissance', 'cler', 'categorie_id', 'etats','type', 'infoManquekit', 'info', 'infoManque'];
/**
* ✅ NOUVELLE MÉTHODE : Récupérer les produits selon le rôle et le store de l'utilisateur
* @param int|null $id
* @return array|object|null
*/
public function getProductDataByRole(int $id = null)
{
$session = session();
$user = $session->get('user');
// Vérifier si l'utilisateur est admin (Conseil ou Direction)
$isAdmin = in_array($user['group_name'], ['Conseil', 'Direction']);
$builder = $this->where('is_piece', 0)
->where('product_sold', 0);
// ✅ Si pas admin ET a un store_id valide, filtrer par son magasin
if (!$isAdmin) {
// ✅ Si l'utilisateur n'a pas de store_id (NULL ou 0), ne retourner aucun produit
if (empty($user['store_id']) || $user['store_id'] == 0) {
// Retourner une requête impossible pour avoir 0 résultats
$builder->where('id', -1);
} else {
// Filtrer par le store_id de l'utilisateur
$builder->where('store_id', $user['store_id']);
}
}
// Si un ID spécifique est demandé
if ($id) {
return $builder->where('id', $id)->first();
}
return $builder->orderBy('id', 'DESC')->findAll();
}
/**
* get the brand data
* @param int $id
@ -22,7 +54,6 @@ class Products extends Model
public function getProductData(int $id = null)
{
if ($id) {
return $this->where('id', $id)->first();
}
@ -32,17 +63,12 @@ class Products extends Model
])->orderBy('id', 'DESC')->findAll();
}
public function getProductData2(int $id)
{
$builder = $this->where('is_piece', 0)
->where('product_sold', 0)
->where('store_id', $id);
// if ($id != 0) {
// $builder = $builder->where('store_id', $id);
// }
return $builder->join('brands', 'brands.id = products.marque')
->orderBy('products.id', 'DESC')
->select('brands.name as brand_name,COUNT( products.id) as total_product, products.store_id as store_id,products.*')
@ -50,15 +76,12 @@ class Products extends Model
->findAll();
}
public function getProductData3(int $id)
{
if ($id == 0) {
return $this->where('is_piece', 0)->orderBy('id', 'DESC')->findAll();
}
// Fetch all products, ordered by ID descending
return $this->where('is_piece', 0)->where('product_sold', 0)->where('store_id', $id)->orderBy('id', 'DESC')->findAll();
}
@ -80,7 +103,6 @@ class Products extends Model
$builder->where("id NOT IN ($subQuery)", null, false);
}
// Si on modifie et qu'on veut inclure le produit actuel dans la liste
if ($currentProductId) {
$builder->orWhere('id', $currentProductId);
}
@ -88,29 +110,14 @@ class Products extends Model
return $builder->orderBy('id', 'DESC')->findAll();
}
/**
* Get active products (availability = 1)
* @return array
*/
public function getActiveProductData()
{
return $this->where('is_piece', 0)->orderBy('id', 'DESC')->findAll();
}
/**
* Assigner un utilisateur à un magasin
*
* @param int|null $productid ID de l'utilisateur
* @param int|null $storeid ID du magasin
* @return bool Résultat de l'opération (true si success, false sinon)
*/
public function assignToStore($productid = null, $storeid = null)
{
// Vérifie si l'utilisateur et le magasin sont fournis
if (!is_null($productid) && !is_null($storeid)) {
// Mise à jour du champ store_id pour l'utilisateur spécifié
$this->db->table('products')
->where('id', $productid)
->update(['store_id' => $storeid]);
@ -118,36 +125,19 @@ class Products extends Model
return true;
}
// Si $userid ou $storeid est null, l'opération échoue
return false;
}
/**
* create new product
* @param array $data
* @return bool
*/
public function create(array $data)
{
return $this->insert($data) ? true : false;
}
/**
* update existing product
* @param array $data
* @param int $id
* @return bool
*/
public function updateProduct(array $data, int $id)
{
return $this->update($id, $data) ? true : false;
}
/**
* remove existing product
* @param int $id
* @return bool
*/
public function remove(int $id)
{
return $this->delete($id) ? true : false;
@ -157,23 +147,18 @@ class Products extends Model
{
$db = \Config\Database::connect();
// Sous-requête pour obtenir les product_id dans avances actives
$subQuery = $db->table('avances')
->select('product_id')
->where('active', 1)
->where('is_order', 0)
->getCompiledSelect();
// Compter les produits disponibles
return $this->where('is_piece', 0)
->where('product_sold', 0)
->where("id NOT IN ($subQuery)", null, false)
->countAllResults();
}
/**
* count all products including sold and reserved (méthode originale si besoin)
*/
public function countAllProductsIncludingSold()
{
return $this->countAll();
@ -185,7 +170,6 @@ class Products extends Model
$total = 0.0;
foreach ($productIds as $id) {
// Récupère le prix du produit courant
$row = $this->select('price')
->where('id', $id)
->first();
@ -197,7 +181,6 @@ class Products extends Model
return $total;
} catch (\Throwable $th) {
// Loger l’erreur ici si besoin : log_message('error', $th->getMessage());
return false;
}
}
@ -207,12 +190,45 @@ class Products extends Model
$product = $this->where('id', $id)->first();
if ($product && isset($product['name'])) {
return $product['name']; // ou un autre champ selon le vrai nom
return $product['name'];
}
return null;
}
/**
* Compter les produits par store selon le rôle de l'utilisateur
* @return int
*/
public function countProductsByUserStore()
{
$session = session();
$user = $session->get('user');
// Vérifier si l'utilisateur est admin
$isAdmin = in_array($user['group_name'], ['DAF', 'Direction']);
$db = \Config\Database::connect();
// Sous-requête pour exclure les produits en avance
$subQuery = $db->table('avances')
->select('product_id')
->where('active', 1)
->where('is_order', 0)
->getCompiledSelect();
$builder = $this->where('is_piece', 0)
->where('product_sold', 0)
->where("id NOT IN ($subQuery)", null, false);
// Si pas admin ET a un store_id valide, filtrer par son magasin
if (!$isAdmin && !empty($user['store_id']) && $user['store_id'] != 0) {
$builder->where('store_id', $user['store_id']);
} elseif (!$isAdmin) {
// Utilisateur sans store = 0 produits
return 0;
}
return $builder->countAllResults();
}
}

885
app/Views/avances/avance.php

File diff suppressed because it is too large

275
app/Views/dashboard.php

@ -54,6 +54,10 @@
</ol>
</section>
<!-- Main content -->
<section class="content">
<!-- Small boxes (Stat box) -->
@ -709,16 +713,13 @@
<?php if ($isCaissier === true): ?>
<!-- <div class="content-wrapper"> -->
<!-- <h5>Votre statistique de vente</h5> -->
<!-- performance content wraper: <div class="content-wrapper"> -->
<div class=" container-fluid row">
<!-- total en caisse -->
<!-- Section des totaux caisse -->
<div class="container-fluid row">
<!-- ✅ MODIFIÉ : Utiliser total_caisse au lieu de total -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box" style="background-color: #A9A9A9;">
<div class="inner">
<h2><?php echo number_format($total, 0, '.', ' '); ?>Ar</h2>
<h2><?php echo number_format($total_caisse, 0, '.', ' '); ?>Ar</h2>
<p>Totale CAISSE</p>
</div>
<div class="icon">
@ -726,12 +727,12 @@
</div>
</div>
</div>
<!-- total mvola -->
<!-- ✅ MODIFIÉ : Utiliser total_mvola_caisse -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box" style="background-color: #A9A9A9;">
<div class="inner">
<h2><?php echo number_format($total_mvola, 0, '.', ' '); ?>Ar</h2>
<h2><?php echo number_format($total_mvola_caisse, 0, '.', ' '); ?>Ar</h2>
<p>Totale MVOLA</p>
</div>
<div class="icon">
@ -739,25 +740,25 @@
</div>
</div>
</div>
<!-- Total en espece -->
<!-- ✅ MODIFIÉ : Utiliser total_espece_caisse -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box" style="background-color: #A9A9A9;">
<div class="inner">
<h2><?php echo number_format($total_espece, 0, '.', ' '); ?>Ar</h2>
<p>Totale en espece</p>
<h2><?php echo number_format($total_espece_caisse, 0, '.', ' '); ?>Ar</h2>
<p>Totale en espèce</p>
</div>
<div class="icon">
<i class="fa fa fa-usd"></i>
<i class="fa fa-usd"></i>
</div>
</div>
</div>
<!-- Total en virement bancaire -->
<!-- ✅ MODIFIÉ : Utiliser total_vb_caisse -->
<div class="col-lg-3 col-xs-6">
<!-- small box -->
<div class="small-box" style="background-color: #A9A9A9;">
<div class="inner">
<h2><?php echo number_format($total_virement_bancaire, 0, '.', ' '); ?>Ar</h2>
<h2><?php echo number_format($total_vb_caisse, 0, '.', ' '); ?>Ar</h2>
<p>Totale en banque</p>
</div>
<div class="icon">
@ -767,11 +768,67 @@
</div>
</div>
<!-- ✅ NOUVEAU : Afficher le détail Orders vs Avances -->
<div class="container-fluid row" style="margin-top: 10px;">
<div class="col-lg-6 col-xs-12">
<div class="info-box bg-aqua">
<span class="info-box-icon"><i class="fa fa-shopping-cart"></i></span>
<div class="info-box-content">
<span class="info-box-text">Total Orders (Ventes complètes)</span>
<span class="info-box-number"><?php echo number_format($total_orders_only, 0, '.', ' '); ?> Ar</span>
</div>
</div>
</div>
<div class="col-lg-6 col-xs-12">
<div class="info-box bg-yellow">
<span class="info-box-icon"><i class="fa fa-clock-o"></i></span>
<div class="info-box-content">
<span class="info-box-text">Total Avances (Paiements partiels)</span>
<span class="info-box-number"><?php echo number_format($total_avances, 0, '.', ' '); ?> Ar</span>
</div>
</div>
</div>
</div>
<!-- ✅ NOUVEAU : Détail des avances par mode de paiement -->
<div class="container-fluid row" style="margin-top: 10px; margin-bottom: 20px;">
<div class="col-lg-12">
<div class="box box-info">
<div class="box-header with-border">
<h3 class="box-title"><i class="fa fa-info-circle"></i> Détail des Avances par Mode de Paiement</h3>
</div>
<div class="box-body">
<div class="row">
<div class="col-sm-4">
<div class="description-block">
<h5 class="description-header"><?php echo number_format($total_avances_mvola, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">MVOLA</span>
</div>
</div>
<div class="col-sm-4">
<div class="description-block">
<h5 class="description-header"><?php echo number_format($total_avances_espece, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">ESPÈCE</span>
</div>
</div>
<div class="col-sm-4">
<div class="description-block">
<h5 class="description-header"><?php echo number_format($total_avances_virement, 0, '.', ' '); ?> Ar</h5>
<span class="description-text">BANQUE</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<section class="content-header">
<h1> Rapport de Performance du Caissier</h1>
<h1>Rapport de Performance du Caissier</h1>
<ol class="breadcrumb">
<li><a href="#"><i class="fa fa-home"></i> Accueil</a></li>
<li class="active" onclick="window.history.back()" style="cursor: pointer;"> Rapports</li>
<li class="active" onclick="window.history.back()" style="cursor: pointer;">Rapports</li>
</ol>
</section>
@ -781,11 +838,9 @@
<div class="col-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<!-- Product Details -->
<div class="row mt-4">
<div class="col-md-12 col-lg-12">
<div class="card shadow-sm border-0">
<div class="card-body">
<div class="row g-3 align-items-center mb-4" style="margin: 5px 0 5px 5px;">
<div class="col-md-3">
@ -798,10 +853,8 @@
</div>
<div class="col-md-3 d-flex align-items-end">
<br>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer
🔍</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter
📤</button>
<button id="filteredB1" class="btn btn-primary w-100">Filtrer 🔍</button>
<button id="ExportBTN1" class="btn btn-success w-100">Exporter 📤</button>
</div>
</div>
<table id="caissierperf" class="table table-hover table-striped">
@ -825,12 +878,10 @@
</div>
</div>
</section>
<div style="width: 80%; margin: auto;">
<canvas id="salesChart"></canvas>
</div>
<!-- /.content -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>
@ -910,6 +961,171 @@
<!-- </div> -->
<?php endif; ?>
<!-- securite -->
<!-- Dashboard pour Sécurité -->
<!-- Dashboard pour Sécurité -->
<?php if ($isSecurite === true): ?>
<!-- Styles spécifiques pour Sécurité -->
<style>
.security-dashboard {
margin: 0 !important;
padding: 0 !important;
}
.security-dashboard .content-header {
margin: 0 !important;
padding: 15px !important;
background-color: #f9f9f9;
}
.security-dashboard .content {
margin: 0 !important;
padding: 15px !important;
}
.security-dashboard .row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.security-dashboard .col-lg-3,
.security-dashboard .col-md-4,
.security-dashboard .col-sm-6,
.security-dashboard .col-xs-12 {
padding-left: 0 !important;
padding-right: 15px !important;
}
.security-dashboard .small-box {
border-radius: 12px;
padding: 20px;
color: white;
position: relative;
overflow: hidden;
box-shadow: 0px 4px 10px rgba(0, 0, 0, 0.15);
transition: transform 0.3s ease;
margin: 0 !important;
}
.security-dashboard .small-box:hover {
transform: translateY(-5px);
box-shadow: 0px 6px 15px rgba(0, 0, 0, 0.25);
}
.security-dashboard .small-box .inner {
position: relative;
z-index: 10;
}
.security-dashboard .small-box .inner h3 {
font-size: 38px;
font-weight: bold;
margin: 0;
}
.security-dashboard .small-box .inner p {
font-size: 16px;
margin-top: 5px;
}
.security-dashboard .small-box .icon {
position: absolute;
top: 10px;
right: 10px;
font-size: 70px;
opacity: 0.3;
z-index: 0;
}
.security-dashboard .small-box-footer {
display: block;
padding: 10px 0;
margin-top: 10px;
color: rgba(255, 255, 255, 0.8);
text-decoration: none;
border-top: 1px solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
}
.security-dashboard .small-box-footer:hover {
color: white;
background-color: rgba(0, 0, 0, 0.1);
text-decoration: none;
}
.security-dashboard .bg-aqua {
background: linear-gradient(135deg, #00c0ef 0%, #0099cc 100%);
}
.security-dashboard .box {
border-radius: 8px;
box-shadow: 0px 2px 8px rgba(0, 0, 0, 0.1);
margin-left: 0 !important;
margin-right: 0 !important;
}
.security-dashboard .box-header.with-border {
border-bottom: 2px solid #3c8dbc;
}
.security-dashboard .box-title {
font-weight: 600;
}
.security-dashboard .col-md-12 {
padding-left: 0 !important;
padding-right: 0 !important;
}
</style>
<!-- Main content -->
<section class="content">
<div class="row">
<div class="col-lg-3 col-md-4 col-sm-6 col-xs-12">
<!-- small box -->
<div class="small-box bg-aqua">
<div class="inner">
<h3><?php echo $total_products ?></h3>
<p>Total Produits</p>
</div>
<div class="icon">
<i class="ion ion-bag"></i>
</div>
<a href="<?php echo base_url('products/') ?>" class="small-box-footer">
Plus d'information <i class="fa fa-arrow-circle-right"></i>
</a>
</div>
</div>
<!-- ./col -->
</div>
<!-- /.row -->
<!-- Section informative pour la sécurité -->
<div class="row">
<div class="col-md-12">
<div class="box box-primary">
<div class="box-header with-border">
<h3 class="box-title">
<i class="fa fa-shield"></i> Informations Sécurité
</h3>
</div>
<div class="box-body">
<p>
<strong>Bienvenue sur votre tableau de bord sécurité.</strong>
</p>
<p>
Vous avez accès à la consultation du nombre total de produits en stock.
Pour plus de détails, cliquez sur "Plus d'information" dans la carte ci-dessus.
</p>
</div>
</div>
</div>
</div>
</section>
<?php endif; ?>
<!-- mecanicien -->
@ -972,6 +1188,7 @@
</div>
</div>
</section>
<!-- Scripts -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/xlsx/0.18.5/xlsx.full.min.js"></script>
<script>

239
app/Views/login.php

@ -1,44 +1,172 @@
<!DOCTYPE html>
<html>
<html lang="fr">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Log in</title>
<!-- Tell the browser to be responsive to screen width -->
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
<!-- Bootstrap 3.3.7 -->
<link rel="stylesheet" href="<?php echo base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') ?>">
<title>Connexion | Motorbike</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- Font Awesome -->
<!-- Bootstrap -->
<link rel="stylesheet" href="<?php echo base_url('assets/bower_components/bootstrap/dist/css/bootstrap.min.css') ?>">
<link rel="stylesheet" href="<?php echo base_url('assets/bower_components/font-awesome/css/font-awesome.min.css') ?>">
<link rel="stylesheet" href="<?php echo base_url('assets/dist/css/AdminLTE.min.css') ?>">
<!-- Google Font -->
<link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
<!-- Ionicons -->
<link rel="stylesheet" href="<?php echo base_url('assets/bower_components/Ionicons/css/ionicons.min.css') ?>">
<style>
body {
font-family: 'Poppins', sans-serif;
background: linear-gradient(135deg, #e9ecef, #dee2e6);
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
}
<!-- Theme style -->
<link rel="stylesheet" href="<?php echo base_url('assets/dist/css/AdminLTE.min.css') ?>">
<!-- iCheck -->
<link rel="stylesheet" href="<?php echo base_url('assets/plugins/iCheck/square/blue.css') ?>">
.login-card {
display: flex;
width: 720px;
max-width: 95%;
background: #fff;
border-radius: 15px;
overflow: hidden;
box-shadow: 0 6px 20px rgba(0,0,0,0.15);
animation: fadeIn 0.8s ease;
}
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
<!--[if lt IE 9]>
<script src="https://oss.maxcdn.com/html5shiv/3.7.3/html5shiv.min.js"></script>
<script src="https://oss.maxcdn.com/respond/1.4.2/respond.min.js"></script>
<![endif]-->
/* Partie gauche : image moto */
.login-image {
flex: 1;
background: url('https://lh3.googleusercontent.com/p/AF1QipN4iewRbD9iIfbsvyPTD2SGUkxyi952uG30pHD9=s1360-w1360-h1020') center/cover no-repeat;
/* Alternative possible :
https://images.unsplash.com/photo-1520975661595-6453be3f7070?auto=format&fit=crop&w=1000&q=80
(casque de moto)
*/
position: relative;
display: flex;
justify-content: center;
align-items: center;
color: #fff;
text-align: center;
}
<!-- Google Font -->
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,600,700,300italic,400italic,600italic">
.login-image::after {
content: "";
position: absolute;
inset: 0;
background: rgba(0, 0, 0, 0.55);
}
.login-image h1 {
position: relative;
z-index: 1;
font-size: 1.8em;
font-weight: 600;
color: #ffcc00;
}
.login-image p {
position: relative;
z-index: 1;
font-size: 1em;
max-width: 250px;
margin: 10px auto 0;
line-height: 1.4;
color: #eee;
}
/* Partie droite : formulaire */
.login-form {
flex: 1;
padding: 35px 30px;
display: flex;
flex-direction: column;
justify-content: center;
}
.login-form h2 {
font-weight: 600;
margin-bottom: 25px;
color: #007bff;
text-align: center;
}
.input-group {
margin-bottom: 15px;
}
.input-group .form-control {
border-radius: 30px;
padding: 10px 20px;
box-shadow: none;
border: 1px solid #ccc;
}
.input-group-addon {
border-radius: 30px 0 0 30px;
background-color: #f8f9fa;
border: 1px solid #ccc;
border-right: none;
color: #007bff;
}
.btn-primary {
border-radius: 30px;
background-color: #007bff;
border: none;
padding: 10px 20px;
font-weight: 600;
transition: 0.3s;
}
.btn-primary:hover {
background-color: #0056b3;
}
.checkbox label {
font-weight: 400;
color: #555;
}
.alert {
border-radius: 10px;
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@media (max-width: 768px) {
.login-card {
flex-direction: column;
width: 90%;
}
.login-image {
height: 160px;
}
.login-form {
padding: 25px;
}
}
</style>
</head>
<body class="hold-transition login-page">
<div class="login-box">
<div class="login-logo">
<a href=""><b>Login</b></a>
<body>
<div class="login-card">
<!-- Image + message -->
<div class="login-image">
<div>
<h1>Bienvenue chez Motorbike</h1>
<p>Gérez vos avances et opérations avec passion pour la moto.</p>
</div>
</div>
<!-- /.login-logo -->
<div class="login-box-body">
<p class="login-box-msg">Sign in to start your session</p>
<!-- Formulaire -->
<div class="login-form">
<h2>Connexion</h2>
<?php if (session()->getFlashdata('error')): ?>
<div class="alert alert-danger">
@ -47,50 +175,27 @@
<?php endif; ?>
<form action="<?= base_url('login') ?>" method="post">
<div class="form-group has-feedback">
<input type="email" class="form-control" name="email" id="email" placeholder="Email" autocomplete="off">
<span class="glyphicon glyphicon-envelope form-control-feedback"></span>
</div>
<div class="form-group has-feedback">
<input type="password" class="form-control" name="password" id="password" placeholder="Password" autocomplete="off">
<span class="glyphicon glyphicon-lock form-control-feedback"></span>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-envelope"></i></span>
<input type="email" class="form-control" name="email" placeholder="Email" required>
</div>
<div class="row">
<div class="col-xs-8">
<div class="checkbox icheck">
<label>
<input type="checkbox"> Remember Me
</label>
</div>
</div>
<!-- /.col -->
<div class="col-xs-4">
<button type="submit" class="btn btn-primary btn-block btn-flat">Sign In</button>
<div class="input-group">
<span class="input-group-addon"><i class="fa fa-lock"></i></span>
<input type="password" class="form-control" name="password" placeholder="Mot de passe" required>
</div>
<!-- /.col -->
<div class="checkbox">
<label><input type="checkbox"> Se souvenir de moi</label>
</div>
<button type="submit" class="btn btn-primary btn-block">Se connecter</button>
</form>
</div>
</div>
<!-- /.login-box-body -->
</div>
<!-- /.login-box -->
<!-- jQuery 3 -->
<script src="<?php echo base_url('assets/bower_components/jquery/dist/jquery.min.js') ?>"></script>
<!-- Bootstrap 3.3.7 -->
<script src="<?php echo base_url('assets/bower_components/bootstrap/dist/js/bootstrap.min.js') ?>"></script>
<!-- iCheck -->
<script src="<?php echo base_url('assets/plugins/iCheck/icheck.min.js') ?>"></script>
<script>
$(function () {
$('input').iCheck({
checkboxClass: 'icheckbox_square-blue',
radioClass: 'iradio_square-blue',
increaseArea: '20%' // optional
});
});
</script>
<script src="<?php echo base_url('assets/bower_components/jquery/dist/jquery.min.js') ?>"></script>
<script src="<?php echo base_url('assets/bower_components/bootstrap/dist/js/bootstrap.min.js') ?>"></script>
</body>
</html>

238
app/Views/orders/createbyid.php

@ -1,4 +1,10 @@
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
<style>
.border-danger {
border: 2px solid #d9534f !important;
box-shadow: 0 0 5px rgba(217, 83, 79, 0.5) !important;
}
</style>
<div class="content-wrapper">
<section class="content-header">
<h1>
@ -41,7 +47,7 @@
<?= esc($errors) ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php endif; ?>
<div class="box">
<div class="box-header">
<h3 class="box-title">Ajouter une commande</h3>
@ -53,7 +59,6 @@
<?= $validation->listErrors() ?>
</ul>
</div>
<?php endif; ?>
<div class="box-body">
<div class="form-group">
@ -99,10 +104,10 @@
</div>
<div class="form-group">
<label for="gross_amount" class="col-sm-5 control-label"
<label for="customer_cin" class="col-sm-5 control-label"
style="text-align:left;">CIN du client</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="customer_phone" name="customer_cin"
<input type="text" class="form-control" id="customer_cin" name="customer_cin"
value="<?= old('customer_cin') ?>" required placeholder="Entrer le CIN du client"
autocomplete="off">
</div>
@ -113,11 +118,8 @@
<thead>
<tr>
<th style="width:50%">Produit</th>
<!-- <th style="width:10%">Quantité</th> -->
<th style="width:10%">Prix unitaire</th>
<th style="width:20%">Montant</th>
<!-- <th style="width:10%"><button type="button" id="add_row"
class="btn btn-default"><i class="fa fa-plus"></i></button></th> -->
</tr>
</thead>
<tbody>
@ -134,15 +136,14 @@
<td>
<input type="text" name="rate[]" id="rate_1" class="form-control" disabled
autocomplete="off" value="<?= $pu ?>">
autocomplete="off" value="<?= $pu ?>" min="0">
<input type="hidden" name="rate_value[]" value="<?= $pu ?>"
id="rate_value_1" class="form-control" autocomplete="off">
<input type="hidden" id="min_price_1" name="min_price[]" value="">
</td>
<td>
<input type="text" name="amount[]" id="amount_1" class="form-control"
disabled autocomplete="off">
disabled autocomplete="off" min="0">
<input type="hidden" name="amount_value[]" id="amount_value_1"
class="form-control" autocomplete="off">
</td>
@ -151,47 +152,40 @@
</tr>
</tbody>
</table>
<input type="hidden" name="qty[]" value="1" id="qty_1" max="<//?= $totalqtt ?>"
<input type="hidden" name="qty[]" value="1" id="qty_1" min="1"
class="form-control valueqty" required onkeyup="getTotal(1)">
<br /> <br />
<div class="col-md-6 col-xs-12 pull pull-right">
<div class="form-group">
<label for="gross_amount" class="col-sm-5 control-label">Montant brut</label>
<label for="gross_amount" class="col-sm-5 control-label">Prix affiché</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="gross_amount" name="gross_amount"
disabled autocomplete="off">
disabled autocomplete="off" min="0">
<input type="hidden" class="form-control" id="gross_amount_value"
name="gross_amount_value" autocomplete="off">
</div>
</div>
<div class="form-group">
<label for="discount" class="col-sm-5 control-label">Rabais</label>
<label for="discount" class="col-sm-5 control-label">Prix demandé</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="discount" name="discount" placeholder="Discount"
onkeyup="subAmount()" autocomplete="off">
<div id="discount_error" class="alert alert-danger" style="display:none; padding: 8px; margin-bottom: 5px; font-size: 13px;">
<i class="fa fa-exclamation-triangle"></i> <span id="discount_error_text"></span>
</div>
<input type="number" class="form-control numeric-input" id="discount" name="discount"
placeholder="Prix demandé" min="0" step="0.01"
onkeyup="subAmount()" oninput="validatePrixDemande(this)" autocomplete="off">
</div>
</div>
<div class="form-group">
<label for="net_amount" class="col-sm-5 control-label">Montant Net</label>
<label for="net_amount" class="col-sm-5 control-label">Remise</label>
<div class="col-sm-7">
<input type="hidden" class="form-control" id="net_amount_value"
name="net_amount" hidden autocomplete="off">
<input type="text" class="form-control" id="net_amount" name="net_amount"
disabled autocomplete="off">
</div>
disabled autocomplete="off" min="0">
</div>
</div>
<!--
<div class="form-group">
<label for="paid_status" class="col-sm-5 control-label">Mode de paiement</label>
<div class="col-sm-7">
<select type="text" class="form-control" id="payment_mode" name="payment_mode">
<option value="1">MVOLA</option>
<option value="2">Virement Bancaire</option>
<option value="3">En espece</option>
</select>
</div>
</div> -->
<div class="box-footer">
<input type="hidden" name="service_charge_rate"
value="<?php echo $company_data['service_charge_value'] ?>" autocomplete="off">
@ -208,9 +202,71 @@
</div>
</section>
</div>
<script type="text/javascript">
var base_url = "<?php echo base_url(); ?>";
// ✅ Fonction pour valider les nombres positifs
function validatePositiveNumber(input) {
let value = parseFloat(input.value);
// Si la valeur est négative ou NaN, on la remet à 0
if (isNaN(value) || value < 0) {
input.value = '';
return false;
}
return true;
}
// ✅ Fonction pour afficher l'erreur sur le champ prix demandé
function showDiscountError(message) {
$('#discount_error_text').text(message);
$('#discount_error').slideDown(200);
$('#discount').addClass('border-danger');
}
// ✅ Fonction pour masquer l'erreur sur le champ prix demandé
function hideDiscountError() {
$('#discount_error').slideUp(200);
$('#discount').removeClass('border-danger');
}
// ✅ Fonction pour valider que le prix demandé ne dépasse pas le prix affiché
function validatePrixDemande(input) {
let prixDemande = parseFloat(input.value);
let prixAffiche = parseFloat($('#gross_amount').val()) || 0;
// Vérifier si négatif
if (isNaN(prixDemande) || prixDemande < 0) {
input.value = '';
showDiscountError('Le prix demandé ne peut pas être négatif.');
setTimeout(hideDiscountError, 3000);
return false;
}
// Vérifier si supérieur au prix affiché
if (prixDemande > prixAffiche) {
showDiscountError('Le prix demandé (' + prixDemande.toFixed(2) + ') ne peut pas être supérieur au prix affiché (' + prixAffiche.toFixed(2) + ').');
input.value = prixAffiche;
subAmount();
setTimeout(hideDiscountError, 4000);
return false;
}
// Si tout est valide, masquer l'erreur
hideDiscountError();
return true;
}
// ✅ Fonction pour empêcher la saisie de caractères négatifs
function preventNegativeInput(e) {
// Empêche la saisie du signe moins (-)
if (e.key === '-' || e.key === 'e' || e.key === 'E' || e.key === '+') {
e.preventDefault();
return false;
}
}
$(document).ready(function () {
getTotal(1);
$(".select_group").select2();
@ -218,6 +274,48 @@
$("#mainOrdersNav").addClass('active');
$("#addOrderNav").addClass('active');
// ✅ Appliquer la validation sur tous les champs numériques
$(document).on('keydown', '.numeric-input, input[type="number"]', function(e) {
preventNegativeInput(e);
});
$(document).on('input', '.numeric-input, input[type="number"]', function() {
validatePositiveNumber(this);
});
// ✅ Validation sur le champ discount
$("#discount").on('input', function() {
validatePrixDemande(this);
});
$("#discount").on('blur', function() {
validatePrixDemande(this);
checkMinimalPrice();
});
// Bloquer la soumission du formulaire
$('form').on('submit', function(e) {
// Vérifier tous les champs numériques avant soumission
let hasNegative = false;
$('.numeric-input, input[type="number"]').each(function() {
if (parseFloat($(this).val()) < 0) {
hasNegative = true;
$(this).val('');
}
});
if (hasNegative) {
alert('Les valeurs négatives ne sont pas autorisées.');
e.preventDefault();
return false;
}
if (!checkMinimalPrice()) {
e.preventDefault();
return false;
}
});
// Add new row in the table
$("#add_row").unbind('click').bind('click', function () {
var table = $("#product_info_table");
@ -239,8 +337,8 @@
html += '</select>' +
'</td>' +
'<td><input type="text" name="rate[]" id="rate_' + row_id + '" class="form-control" disabled><input type="hidden" name="rate_value[]" id="rate_value_' + row_id + '" class="form-control"><input type="hidden" id="min_price_' + row_id + '" name="min_price[]" value=""></td>' +
'<td><input type="text" name="amount[]" id="amount_' + row_id + '" class="form-control" disabled><input type="hidden" name="amount_value[]" id="amount_value_' + row_id + '" class="form-control"></td>' +
'<td><input type="text" name="rate[]" id="rate_' + row_id + '" class="form-control numeric-input" disabled min="0"><input type="hidden" name="rate_value[]" id="rate_value_' + row_id + '" class="form-control"><input type="hidden" id="min_price_' + row_id + '" name="min_price[]" value=""></td>' +
'<td><input type="text" name="amount[]" id="amount_' + row_id + '" class="form-control numeric-input" disabled min="0"><input type="hidden" name="amount_value[]" id="amount_value_' + row_id + '" class="form-control"></td>' +
'<td><button type="button" class="btn btn-default" onclick="removeRow(\'' + row_id + '\')"><i class="fa fa-close"></i></button></td>' +
'</tr>';
@ -257,24 +355,18 @@
return false;
});
// Vérifier lors de la saisie du rabais
$("#discount").on('blur', function() {
checkMinimalPrice();
});
// Bloquer la soumission du formulaire
$('form').on('submit', function(e) {
if (!checkMinimalPrice()) {
e.preventDefault();
return false;
}
});
}); // /document
function getTotal(row = null) {
if (row) {
var total = Number($("#rate_value_" + row).val()) * Number($("#qty_" + row).val());
var rate = Number($("#rate_value_" + row).val());
var qty = Number($("#qty_" + row).val());
// ✅ Vérifier que les valeurs sont positives
if (rate < 0) rate = 0;
if (qty < 0) qty = 0;
var total = rate * qty;
total = total.toFixed(2);
$("#amount_" + row).val(total);
$("#amount_value_" + row).val(total);
@ -306,15 +398,21 @@
},
dataType: 'json',
success: function (response) {
// setting the rate value into the rate input field
$("#rate_" + row_id).val(response.prix_vente);
$("#rate_value_" + row_id).val(response.prix_vente);
$("#min_price_" + row_id).val(response.prix_minimal); // ✅ Stockage du prix minimal
// ✅ S'assurer que les prix ne sont pas négatifs
var prixVente = parseFloat(response.prix_vente) || 0;
var prixMinimal = parseFloat(response.prix_minimal) || 0;
if (prixVente < 0) prixVente = 0;
if (prixMinimal < 0) prixMinimal = 0;
$("#rate_" + row_id).val(prixVente);
$("#rate_value_" + row_id).val(prixVente);
$("#min_price_" + row_id).val(prixMinimal);
$("#qty_" + row_id).val(1);
$("#qty_value_" + row_id).val(1);
var total = Number(response.prix_vente) * 1;
var total = prixVente * 1;
total = total.toFixed(2);
$("#amount_" + row_id).val(total);
$("#amount_value_" + row_id).val(total);
@ -337,7 +435,10 @@
var count = $(tr).attr('id');
count = count.substring(4);
totalSubAmount = Number(totalSubAmount) + Number($("#amount_" + count).val());
var amount = Number($("#amount_" + count).val());
// ✅ Vérifier que le montant n'est pas négatif
if (amount < 0) amount = 0;
totalSubAmount = Number(totalSubAmount) + amount;
}
totalSubAmount = totalSubAmount.toFixed(2);
@ -362,9 +463,17 @@
var totalAmount = (Number(totalSubAmount));
totalAmount = totalAmount.toFixed(2);
var discount = $("#discount").val();
var discount = Number($("#discount").val()) || 0;
// ✅ S'assurer que le rabais n'est pas négatif
if (discount < 0) {
discount = 0;
$("#discount").val('');
}
if (discount) {
var grandTotal = Number(totalAmount) - Number(discount);
// ✅ S'assurer que le total n'est pas négatif
if (grandTotal < 0) grandTotal = 0;
grandTotal = grandTotal.toFixed(2);
$("#net_amount").val(grandTotal);
$("#net_amount_value").val(grandTotal);
@ -378,6 +487,14 @@
function checkMinimalPrice() {
var discount = Number($("#discount").val()) || 0;
// ✅ Vérifier que le rabais n'est pas négatif
if (discount < 0) {
alert("Le prix demandé ne peut pas être négatif.");
$("#discount").val('');
subAmount();
return false;
}
// Si pas de rabais, pas de vérification nécessaire
if (discount === 0) return true;
@ -394,7 +511,7 @@
if (minPrice > 0 && discount < minPrice) {
error = true;
var productText = $("#product_" + rowId + " option:selected").text();
messages.push("Le rabais (" + discount + ") pour « " + productText + " » est inférieur au prix minimal (" + minPrice + ")");
messages.push("Le prix demandé (" + discount.toFixed(2) + ") pour « " + productText + " » est inférieur au prix minimal (" + minPrice.toFixed(2) + ")");
}
}
@ -430,9 +547,20 @@
net_amount.value = (gross_amount_value.value - remise.value);
net_amount_value.value = (gross_amount_value.value - remise.value);
remise.addEventListener('input', function () {
net_amount.value = (gross_amount_value.value - remise.value);
net_amount_value.value = (gross_amount_value.value - remise.value);
// ✅ Validation du prix demandé vs prix affiché
if (validatePrixDemande(this)) {
var discountValue = parseFloat(this.value) || 0;
var grossValue = parseFloat(gross_amount_value.value) || 0;
var netValue = grossValue - discountValue;
// ✅ S'assurer que le net amount n'est pas négatif
if (netValue < 0) netValue = 0;
net_amount.value = netValue.toFixed(2);
net_amount_value.value = netValue.toFixed(2);
subAmount();
}
})
</script>

474
app/Views/orders/edit.php

@ -22,26 +22,70 @@
<?php if (session()->getFlashdata('success')): ?>
<div class="alert alert-success alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<?php echo session()->getFlashdata('success'); ?>
</div>
<?php elseif (session()->getFlashdata('error')): ?>
<div class="alert alert-error alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close"><span aria-hidden="true">&times;</span></button>
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<?php echo session()->getFlashdata('error'); ?>
</div>
<?php endif; ?>
<?php if ($errors = session()->getFlashdata('errors')): ?>
<div class="alert alert-danger alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<?php if (is_array($errors)): ?>
<ul>
<?php foreach ($errors as $error): ?>
<li><?= esc($error) ?></li>
<?php endforeach; ?>
</ul>
<?php else: ?>
<?= esc($errors) ?>
<?php endif; ?>
</div>
<?php endif; ?>
<?php
// ✅ VÉRIFIER SI LA COMMANDE EST MODIFIABLE
// Seuls les statuts 1 (Validé) et 3 (Validé et Livré) sont NON modifiables
$is_editable = isset($is_editable) ? $is_editable : true;
$paid_status = $order_data['order']['paid_status'] ?? 2;
?>
<?php if (!$is_editable): ?>
<!-- ✅ ALERTE SI NON MODIFIABLE -->
<div class="alert alert-warning alert-dismissible" role="alert">
<button type="button" class="close" data-dismiss="alert" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
<strong><i class="fa fa-lock"></i> Cette commande ne peut plus être modifiée</strong><br>
Elle a été <?php echo ($paid_status == 1) ? 'validée' : 'validée et livrée'; ?>.
Vous pouvez uniquement consulter les informations.
</div>
<?php endif; ?>
<div class="box">
<div class="box-header">
<h3 class="box-title">Mise à jour de commande</h3>
<h3 class="box-title">
<?php echo $is_editable ? 'Mise à jour de commande' : 'Consultation de commande'; ?>
</h3>
</div>
<!-- /.box-header -->
<form role="form" action="<?php base_url('orders/create') ?>" method="post" class="form-horizontal">
<form role="form"
action="<?php base_url('orders/create') ?>"
method="post"
class="form-horizontal"
<?php echo !$is_editable ? 'onsubmit="return false;"' : ''; ?>>
<div class="box-body">
<div class="form-group">
<label for="date" class="col-sm-12 control-label">Date: <?php echo date('Y-m-d') ?></label>
</div>
@ -54,7 +98,7 @@
<div class="form-group">
<label for="types" class="col-sm-5 control-label" style="text-align:left;">Types</label>
<div class="col-sm-7">
<select name="" id="typesCommande" class="form-control">
<select name="" id="typesCommande" class="form-control" <?php echo !$is_editable ? 'disabled' : ''; ?>>
<option value="1">Facture</option>
<option value="2">Bon de Livraison & Facture</option>
<option value="3">Bon de Livraison</option>
@ -65,33 +109,60 @@
<div class="form-group">
<label for="customer_name" class="col-sm-5 control-label" style="text-align:left;">Nom du client</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="customer_name" name="customer_name" placeholder="Enter Customer Name" value="<?php echo $order_data['order']['customer_name'] ?>" autocomplete="off" />
<input type="text"
class="form-control"
id="customer_name"
name="customer_name"
placeholder="Enter Customer Name"
value="<?php echo $order_data['order']['customer_name'] ?>"
autocomplete="off"
<?php echo !$is_editable ? 'disabled readonly' : ''; ?> />
</div>
</div>
<div class="form-group">
<label for="gross_amount" class="col-sm-5 control-label" style="text-align:left;">Adresse du client</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="customer_address" name="customer_address" placeholder="Enter Customer Address" value="<?php echo $order_data['order']['customer_address'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="customer_address"
name="customer_address"
placeholder="Enter Customer Address"
value="<?php echo $order_data['order']['customer_address'] ?>"
autocomplete="off"
<?php echo !$is_editable ? 'disabled readonly' : ''; ?>>
</div>
</div>
<div class="form-group">
<label for="gross_amount" class="col-sm-5 control-label" style="text-align:left;">Téléphone du client</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="customer_phone" name="customer_phone" placeholder="Enter Customer Phone" value="<?php echo $order_data['order']['customer_phone'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="customer_phone"
name="customer_phone"
placeholder="Enter Customer Phone"
value="<?php echo $order_data['order']['customer_phone'] ?>"
autocomplete="off"
<?php echo !$is_editable ? 'disabled readonly' : ''; ?>>
</div>
</div>
<div class="form-group">
<label for="gross_amount" class="col-sm-5 control-label" style="text-align:left;">CIN du client</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="customer_cin" name="customer_cin" placeholder="Enter Customer CIN" value="<?php echo $order_data['order']['customer_cin'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="customer_cin"
name="customer_cin"
placeholder="Enter Customer CIN"
value="<?php echo $order_data['order']['customer_cin'] ?>"
autocomplete="off"
<?php echo !$is_editable ? 'disabled readonly' : ''; ?>>
</div>
</div>
</div>
<br /> <br />
<table class="table table-bordered" id="product_info_table">
<thead>
@ -100,38 +171,80 @@
<th style="width:10%">Quantité</th>
<th style="width:10%">Prix unitaire</th>
<th style="width:20%">Montant</th>
<th style="width:10%"><button type="button" id="add_row" class="btn btn-default"><i class="fa fa-plus"></i></button></th>
<th style="width:10%">
<?php if ($is_editable): ?>
<button type="button" id="add_row" class="btn btn-default"><i class="fa fa-plus"></i></button>
<?php endif; ?>
</th>
</tr>
</thead>
<tbody>
<?php if (isset($order_data['order_item'])): ?>
<?php $x = 1; ?>
<?php foreach ($order_data['order_item'] as $key => $val): ?>
<?php //print_r($v);
?>
<tr id="row_<?php echo $x; ?>">
<td>
<select class="form-control select_group product" data-row-id="row_<?php echo $x; ?>" id="product_<?php echo $x; ?>" name="product[]" style="width:100%;" onchange="getProductData(<?php echo $x; ?>)" required>
<select class="form-control select_group product"
data-row-id="row_<?php echo $x; ?>"
id="product_<?php echo $x; ?>"
name="product[]"
style="width:100%;"
onchange="getProductData(<?php echo $x; ?>)"
<?php echo !$is_editable ? 'disabled' : 'required'; ?>>
<option value=""></option>
<?php foreach ($products as $k => $v): ?>
<option value="<?php echo $v['id'] ?>" <?php if ($val['product_id'] == $v['id']) {
echo "selected='selected'";
} ?>><?php echo $v['name'] ?></option>
<option value="<?php echo $v['id'] ?>" <?php if ($val['product_id'] == $v['id']) { echo "selected='selected'"; } ?>><?php echo $v['name'] ?></option>
<?php endforeach ?>
</select>
</td>
<td><input type="hidden" name="qty[]" id="qty_<//?php echo $x; ?>" class="form-control" required onkeyup="getTotal(<?php echo $x; ?>)" value="<//?php echo $val['qty'] ?>" autocomplete="off"></td>
<td>
<input type="text" name="rate[]" id="rate_<?php echo $x; ?>" class="form-control" disabled value="<?php echo $val['rate'] ?>" autocomplete="off">
<input type="hidden" name="rate_value[]" id="rate_value_<?php echo $x; ?>" class="form-control" value="<?php echo $val['rate'] ?>" autocomplete="off">
<input type="number"
name="qty[]"
id="qty_<?php echo $x; ?>"
class="form-control"
onkeyup="getTotal(<?php echo $x; ?>)"
value="<?php echo $val['qty'] ?>"
autocomplete="off"
<?php echo !$is_editable ? 'disabled readonly' : 'required'; ?>>
</td>
<td>
<input type="text" name="amount[]" id="amount_<?php echo $x; ?>" class="form-control" disabled value="<?php echo $val['amount'] ?>" autocomplete="off">
<input type="hidden" name="amount_value[]" id="amount_value_<?php echo $x; ?>" class="form-control" value="<?php echo $val['amount'] ?>" autocomplete="off">
<input type="text"
name="rate[]"
id="rate_<?php echo $x; ?>"
class="form-control"
disabled
value="<?php echo $val['rate'] ?>"
autocomplete="off">
<input type="hidden"
name="rate_value[]"
id="rate_value_<?php echo $x; ?>"
class="form-control"
value="<?php echo $val['rate'] ?>"
autocomplete="off">
</td>
<td>
<input type="text"
name="amount[]"
id="amount_<?php echo $x; ?>"
class="form-control"
disabled
value="<?php echo $val['amount'] ?>"
autocomplete="off">
<input type="hidden"
name="amount_value[]"
id="amount_value_<?php echo $x; ?>"
class="form-control"
value="<?php echo $val['amount'] ?>"
autocomplete="off">
</td>
<td>
<?php if ($is_editable): ?>
<button type="button" class="btn btn-default" onclick="removeRow('<?php echo $x; ?>')">
<i class="fa fa-close"></i>
</button>
<?php endif; ?>
</td>
<td><button type="button" class="btn btn-default" onclick="removeRow('<?php echo $x; ?>')"><i class="fa fa-close"></i></button></td>
</tr>
<?php $x++; ?>
<?php endforeach; ?>
@ -144,63 +257,80 @@
<div class="col-md-6 col-xs-12 pull pull-right">
<div class="form-group">
<label for="gross_amount" class="col-sm-5 control-label">Montant brut</label>
<label for="gross_amount" class="col-sm-5 control-label">Prix affiché</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="gross_amount" name="gross_amount" disabled value="<?php echo $order_data['order']['gross_amount'] ?>" autocomplete="off">
<input type="hidden" class="form-control" id="gross_amount_value" name="gross_amount_value" value="<?php echo $order_data['order']['gross_amount'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="gross_amount"
name="gross_amount"
disabled
value="<?php echo $order_data['order']['gross_amount'] ?>"
autocomplete="off">
<input type="hidden"
class="form-control"
id="gross_amount_value"
name="gross_amount_value"
value="<?php echo $order_data['order']['gross_amount'] ?>"
autocomplete="off">
</div>
</div>
<!-- <//?php if ($is_service_enabled == true): ?>
<div class="form-group">
<label for="service_charge" class="col-sm-5 control-label">S-Charge <//?php echo $company_data['service_charge_value'] ?> %</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="service_charge" name="service_charge" disabled value="<//?php echo $order_data['order']['service_charge_rate'] ?>" autocomplete="off">
<input type="hidden" class="form-control" id="service_charge_value" name="service_charge_value" value="<//?php echo $order_data['order']['service_charge_rate'] ?>" autocomplete="off">
</div>
<!-- ✅ ALERTE VISUELLE INTÉGRÉE -->
<div id="price_alert" style="display: none; margin-bottom: 15px;">
<div class="col-sm-offset-5 col-sm-7">
<div style="background-color: #f8d7da; border: 1px solid #f5c6cb; color: #721c24; padding: 10px 15px; border-radius: 4px; animation: slideDown 0.3s ease-out;">
<i class="fa fa-exclamation-triangle" style="margin-right: 8px;"></i>
<strong>Attention !</strong> <span id="price_alert_message"></span>
</div>
<//?php endif; ?> -->
<!-- <//?php if ($is_vat_enabled == true): ?>
<div class="form-group">
<label for="vat_charge" class="col-sm-5 control-label">T.V.A <//?php echo $company_data['vat_charge_value'] ?> %</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="vat_charge" name="vat_charge" disabled value="<//?php echo $order_data['order']['vat_charge'] ?>" autocomplete="off">
<input type="hidden" class="form-control" id="vat_charge_value" name="vat_charge_value" value="<//?php echo $order_data['order']['vat_charge'] ?>" autocomplete="off">
</div>
</div>
<//?php endif; ?> -->
<div class="form-group">
<label for="discount" class="col-sm-5 control-label">Rabais</label>
<label for="discount" class="col-sm-5 control-label">Prix demandé</label>
<div class="col-sm-7">
<?php
$users = session()->get('user');
if($users && $users['group_name'] == 'COMMERCIALE'):
if($users && $users['group_name'] == 'COMMERCIALE' && $is_editable):
?>
<input type="text" class="form-control" id="discount" name="discount" placeholder="Discount" onkeyup="subAmount()" value="<?php echo $order_data['order']['discount'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="discount"
name="discount"
placeholder="Discount"
onkeyup="subAmount()"
value="<?php echo $order_data['order']['discount'] ?>"
autocomplete="off">
<?php else: ?>
<input type="text" class="form-control" id="discount" name="discount" readonly value="<?php echo $order_data['order']['discount'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="discount"
name="discount"
readonly
value="<?php echo $order_data['order']['discount'] ?>"
autocomplete="off">
<?php endif; ?>
</div>
</div>
<div class="form-group">
<label for="net_amount" class="col-sm-5 control-label">Montant net</label>
<label for="net_amount" class="col-sm-5 control-label">Remise</label>
<div class="col-sm-7">
<input type="text" class="form-control" id="net_amount" name="net_amount" disabled value="<?php echo $order_data['order']['net_amount'] ?>" autocomplete="off">
<input type="hidden" class="form-control" id="net_amount_value" name="net_amount_value" value="<?php echo $order_data['order']['net_amount'] ?>" autocomplete="off">
<input type="text"
class="form-control"
id="net_amount"
name="net_amount"
disabled
value="<?php echo $order_data['order']['net_amount'] ?>"
autocomplete="off">
<input type="hidden"
class="form-control"
id="net_amount_value"
name="net_amount_value"
value="<?php echo $order_data['order']['net_amount'] ?>"
autocomplete="off">
</div>
</div>
<!-- <div class="form-group">
<label for="paid_status" class="col-sm-5 control-label">Mode de paiement</label>
<div class="col-sm-7">
<select class="form-control" id="payment_mode" name="payment_mode">
<option value="MVOLA" <//?= ($order_data['order']['order_payment_mode'] == "MVOLA") ? 'selected' : '' ?>>MVOLA</option>
<option value="Virement Bancaire" <//?= ($order_data['order']['order_payment_mode'] == "Virement Bancaire") ? 'selected' : '' ?>>Virement Bancaire</option>
<option value="En espèce" <//?= ($order_data['order']['order_payment_mode'] == "En espèce") ? 'selected' : '' ?>>En espèce</option>
</select>
</div>
</div> -->
<?php
$users = session()->get('user');
if ($users && $users['group_name'] !== 'COMMERCIALE'):
@ -209,14 +339,16 @@
<div class="form-group">
<label for="paid_status" class="col-sm-5 control-label">Tranche de paiement</label>
<div class="col-sm-7">
<select class="form-control" id="payment_mode" name="payment_mode">
<select class="form-control"
id="payment_mode"
name="payment_mode"
<?php echo !$is_editable ? 'disabled' : ''; ?>>
<option value="1" selected>une tranche</option>
<option value="2">deux tranches</option>
</select>
</div>
</div>
<!-- ✅ AJOUTEZ CE CHAMP AVANT LES TRANCHES -->
<div class="form-group" id="montant_reference" style="display: none">
<label class="col-sm-5 control-label">Montant à répartir</label>
<div class="col-sm-7">
@ -230,84 +362,57 @@
<div class="form-group" id="paid_status_1" style="display: none">
<label for="paid_status_1" class="col-sm-5 control-label">Tranche 1</label>
<div class="col-sm-3">
<select class="form-control" id="payment_mode_1" name="order_payment_mode_1">
<select class="form-control"
id="payment_mode_1"
name="order_payment_mode_1"
<?php echo !$is_editable ? 'disabled' : ''; ?>>
<option value="MVOLA">MVOLA</option>
<option value="Virement Bancaire">Virement Bancaire</option>
<option value="En espèce">En espèce</option>
</select>
</div>
<div class="col-sm-4">
<input type="number" class="form-control" id="payment_amount_1"
name="tranche_1" placeholder="Montant" onkeyup="calculerTranche2()">
<input type="number"
class="form-control"
id="payment_amount_1"
name="tranche_1"
placeholder="Montant"
onkeyup="calculerTranche2()"
<?php echo !$is_editable ? 'disabled readonly' : ''; ?>>
</div>
</div>
<div class="form-group" id="paid_status_2" style="display: none">
<label for="paid_status_2" class="col-sm-5 control-label">Tranche 2 (Reste)</label>
<div class="col-sm-3">
<select class="form-control" id="payment_mode_2" name="order_payment_mode_2">
<select class="form-control"
id="payment_mode_2"
name="order_payment_mode_2"
<?php echo !$is_editable ? 'disabled' : ''; ?>>
<option value="MVOLA">MVOLA</option>
<option value="Virement Bancaire">Virement Bancaire</option>
<option value="En espèce">En espèce</option>
</select>
</div>
<div class="col-sm-4">
<input type="number" class="form-control" id="payment_amount_2"
name="tranche_2" placeholder="Montant" readonly>
<input type="number"
class="form-control"
id="payment_amount_2"
name="tranche_2"
placeholder="Montant"
readonly>
</div>
</div>
</div>
<?php endif; ?>
<script>
$(document).ready(function() {
var paymentTranche = 1;
var netAmount = parseFloat($('#net_amount_value').val()) || 0;
// Initialisation : définir la première tranche avec le montant total
$('#payment_amount_1').val(netAmount);
function addPaymentTranche(paymentTranche) {
if (parseInt(paymentTranche) === 2) {
$("#paid_status_1").show();
$("#paid_status_2").show();
// Calculer la deuxième tranche
var amount1 = parseFloat($('#payment_amount_1').val()) || 0;
var amount2 = netAmount - amount1;
$('#payment_amount_2').val(amount2);
} else {
$("#paid_status_1").show();
$("#paid_status_2").hide();
$('#payment_mode_2').val('');
}
}
// Écouter le changement de la sélection pour afficher les tranches
$("#payment_mode").on("change", function() {
addPaymentTranche($(this).val());
});
// Écouter la modification du montant de la première tranche
$('#payment_amount_1').on("input", function() {
var amount1 = parseFloat($(this).val()) || 0;
var amount2 = netAmount - amount1;
$('#payment_amount_2').val(amount2);
});
addPaymentTranche(paymentTranche);
});
</script>
<?php
$users = session()->get('user');
if ($users && $users['group_name'] !== 'COMMERCIALE'):
?>
<div class="form-group">
<label for="paid_status" class="col-sm-5 control-label">Statut payant</label>
<div class="col-sm-7">
<select type="text" class="form-control" id="paid_status" name="paid_status">
<select type="text"
class="form-control"
id="paid_status"
name="paid_status"
<?php echo !$is_editable ? 'disabled' : ''; ?>>
<option value="1">Validé</option>
<option value="2">Refusé</option>
</select>
@ -320,12 +425,15 @@
<!-- /.box-body -->
<div class="box-footer">
<input type="hidden" name="service_charge_rate" value="<?php echo $company_data['service_charge_value'] ?>" autocomplete="off">
<input type="hidden" name="vat_charge_rate" value="<?php echo $company_data['vat_charge_value'] ?>" autocomplete="off">
<a target="__blank" id="Imprimente" href="<?php echo base_url() . 'orders/printDiv/' . $order_data['order']['id'] ?>" class="btn btn-default">Imprimer</a>
<?php if ($is_editable): ?>
<button type="submit" class="btn btn-primary">Enregistrer</button>
<?php endif; ?>
<a href="<?php echo base_url('orders/') ?>" class="btn btn-warning">Retour</a>
</div>
</form>
@ -336,16 +444,42 @@
<!-- col-md-12 -->
</div>
<!-- /.row -->
</section>
<!-- /.content -->
</div>
<!-- /.content-wrapper -->
<style>
/* ✅ Animation pour l'alerte */
@keyframes slideDown {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* ✅ Style pour l'input en erreur */
.input-error {
border: 2px solid #dc3545 !important;
box-shadow: 0 0 8px rgba(220, 53, 69, 0.5) !important;
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0%, 100% { box-shadow: 0 0 8px rgba(220, 53, 69, 0.5); }
50% { box-shadow: 0 0 15px rgba(220, 53, 69, 0.8); }
}
</style>
<script type="text/javascript">
var base_url = "<?php echo base_url(); ?>";
var idData = "<?php echo $order_data['order']['id']; ?>";
var is_editable = <?php echo $is_editable ? 'true' : 'false'; ?>;
let Imprimente = document.getElementById('Imprimente');
let typesCommande = document.getElementById('typesCommande');
@ -362,14 +496,76 @@
}
});
// ============================================
// 🔹 FONCTION D'ALERTE VISUELLE
// ============================================
function showPriceAlert(message) {
const alertBox = $('#price_alert');
const discountInput = $('#discount');
$('#price_alert_message').text(message);
alertBox.slideDown(300);
discountInput.addClass('input-error');
setTimeout(function() {
alertBox.slideUp(300);
discountInput.removeClass('input-error');
}, 3500);
}
function hidePriceAlert() {
$('#price_alert').slideUp(300);
$('#discount').removeClass('input-error');
}
// ============================================
// 🔹 VALIDATION DU PRIX DEMANDÉ
// ============================================
function validateDiscount() {
if (!is_editable) return true; // ✅ Pas de validation si non éditable
var discount = parseFloat($('#discount').val());
var grossAmount = parseFloat($('#gross_amount_value').val()) || 0;
if (discount < 0) {
showPriceAlert("Le prix demandé ne peut pas être négatif !");
$('#discount').val('');
return false;
}
if (discount > grossAmount) {
showPriceAlert("Le prix demandé ne peut pas dépasser le prix affiché (" + grossAmount.toFixed(2) + ") !");
$('#discount').val(grossAmount.toFixed(2));
subAmount();
return false;
}
hidePriceAlert();
return true;
}
$(document).ready(function() {
$(".select_group").select2();
$("#mainOrdersNav").addClass('active');
$("#manageOrdersNav").addClass('active');
// ✅ Désactiver toutes les interactions si non éditable
if (!is_editable) {
$('input, select, textarea').prop('disabled', true);
$('#add_row').hide();
$('.btn-default[onclick^="removeRow"]').hide();
}
// ✅ Validation en temps réel du prix demandé
if (is_editable) {
$('#discount').on('keyup change', function() {
validateDiscount();
});
}
// ============================================
// 🔹 AJOUT : GESTION DES TRANCHES DE PAIEMENT
// 🔹 GESTION DES TRANCHES DE PAIEMENT
// ============================================
var paymentTranche = 1;
var netAmount = parseFloat($('#net_amount_value').val()) || 0;
@ -390,17 +586,19 @@
}
}
// Sélection du mode de paiement
$("#payment_mode").on("change", function() {
if (is_editable) {
addPaymentTranche($(this).val());
updateMontantTranches(); // ✅ pour mise à jour automatique
updateMontantTranches();
}
});
// Modification du montant de la première tranche
$('#payment_amount_1').on("input", function() {
if (is_editable) {
var amount1 = parseFloat($(this).val()) || 0;
var amount2 = netAmount - amount1;
$('#payment_amount_2').val(amount2);
}
});
addPaymentTranche(paymentTranche);
@ -408,6 +606,7 @@
// ============================================
// 🔹 TABLEAU DE PRODUITS
// ============================================
if (is_editable) {
$("#add_row").unbind('click').bind('click', function() {
var table = $("#product_info_table");
var count_table_tbody_tr = $("#product_info_table tbody tr").length;
@ -446,13 +645,15 @@
return false;
});
}
}); // /document.ready
// ============================================
// 🔹 CALCUL DU TOTAL
// ============================================
function getTotal(row = null) {
if (!is_editable) return; // ✅ Bloquer si non éditable
if (row) {
var total = Number($("#rate_value_" + row).val()) * Number($("#qty_" + row).val());
total = total.toFixed(2);
@ -466,6 +667,8 @@
// 🔹 OBTENIR LES DONNÉES PRODUIT
function getProductData(row_id) {
if (!is_editable) return; // ✅ Bloquer si non éditable
var product_id = $("#product_" + row_id).val();
if (product_id == "") {
$("#rate_" + row_id).val("");
@ -498,6 +701,8 @@
// 🔹 CALCUL DU MONTANT TOTAL (AVEC TVA, REMISE, ETC.)
// ============================================
function subAmount() {
if (!is_editable) return; // ✅ Bloquer si non éditable
var service_charge = <?php echo ($company_data['service_charge_value'] > 0) ? $company_data['service_charge_value'] : 0; ?>;
var vat_charge = <?php echo ($company_data['vat_charge_value'] > 0) ? $company_data['vat_charge_value'] : 0; ?>;
@ -529,10 +734,12 @@
var discount = $("#discount").val();
if (discount) {
if (validateDiscount()) {
var grandTotal = Number(totalAmount) - Number(discount);
grandTotal = grandTotal.toFixed(2);
$("#net_amount").val(grandTotal);
$("#net_amount_value").val(grandTotal);
}
} else {
$("#net_amount").val(totalAmount);
$("#net_amount_value").val(totalAmount);
@ -546,7 +753,6 @@
$("#remaining_value").val(remaning.toFixed(2));
}
// ✅ Mise à jour automatique des tranches à chaque recalcul
updateMontantTranches();
}
@ -554,6 +760,8 @@
// 🔹 AUTRES FONCTIONS
// ============================================
function paidAmount() {
if (!is_editable) return;
var grandTotal = $("#net_amount_value").val();
if (grandTotal) {
var dueAmount = Number($("#net_amount_value").val()) - Number($("#paid_amount").val());
@ -564,12 +772,14 @@
}
function removeRow(tr_id) {
if (!is_editable) return; // ✅ Bloquer si non éditable
$("#product_info_table tbody tr#row_" + tr_id).remove();
subAmount();
}
// ============================================
// 🔹 GESTION MONTANT DE TRANCHES (FONCTIONS NOUVELLES)
// 🔹 GESTION MONTANT DE TRANCHES
// ============================================
function getMontantPourTranches() {
var discount = parseFloat($("#discount").val()) || 0;
@ -578,6 +788,8 @@
}
function updateMontantTranches() {
if (!is_editable) return;
var montant = getMontantPourTranches();
var discount = parseFloat($("#discount").val()) || 0;
@ -596,6 +808,8 @@
}
function calculerTranche2() {
if (!is_editable) return;
var montantTotal = getMontantPourTranches();
var tranche1 = parseFloat($("#payment_amount_1").val()) || 0;
var tranche2 = montantTotal - tranche1;
@ -603,14 +817,16 @@
$("#payment_amount_2").val(tranche2.toFixed(2));
}
if (is_editable) {
$("#discount").on('keyup', function() {
updateMontantTranches();
});
}
const net_amount_value = document.getElementById('net_amount_value');
const net_amount = document.getElementById('net_amount');
const payment_amount_1 = document.getElementById('payment_amount_1');
if (payment_amount_1 && net_amount) {
payment_amount_1.value = net_amount.value;
}
</script>

2
app/Views/products/create.php

@ -395,4 +395,6 @@ document.querySelector('form').addEventListener('submit', function(e) {
}
});
</script>

5
app/Views/products/index.php

@ -160,9 +160,8 @@
<th>Prix</th>
<th>Magasin</th>
<th>Disponibilité</th>
<?php if (in_array('updateProduct', $user_permission) || in_array('deleteProduct', $user_permission)): ?>
<!-- ✅ MODIFICATION PRINCIPALE : Toujours afficher la colonne Action -->
<th>Action</th>
<?php endif; ?>
</tr>
</thead>
</table>
@ -336,7 +335,7 @@
},
{ data: 4 }, // Magasin
{ data: 5 }, // Disponibilité
{ data: 6 } // Actions
{ data: 6 } // Actions - ✅ Toujours présent maintenant
],
'columnDefs': [{
targets: 3,

1
app/Views/templates/header.php

@ -905,6 +905,7 @@
}
}

40
app/Views/templates/header_menu.php

@ -19,7 +19,12 @@
<span id="notificationCount" class="badge badge-warning navbar-badge"></span>
<div class="dropdown-menu dropdown-menu-lg dropdown-menu-right"
style="width: 400px; padding: 5%; max-height: 500px; overflow: auto; margin-right: 5px;">
<span class="dropdown-header" id="notificationHeader">0 Notifications</span>
<div style="display: flex; justify-content: space-between; align-items: center; padding: 0 10px;">
<span class="dropdown-header" id="notificationHeader" style="padding: 0;">0 Notifications</span>
<button id="markAllAsReadBtn" class="btn btn-sm btn-primary" style="font-size: 12px; padding: 4px 10px;">
<i class="fa fa-check"></i> Marquer tout comme lu
</button>
</div>
<div class="dropdown-divider"></div>
<div id="notificationList"></div>
<div class="dropdown-divider"></div>
@ -41,6 +46,15 @@
.icon-unread {
color: #007bff; /* bleu */
}
/* Style du bouton */
#markAllAsReadBtn {
white-space: nowrap;
}
#markAllAsReadBtn:hover {
background-color: #0056b3;
}
</style>
<script>
@ -86,8 +100,10 @@ function fetchNotifications() {
// Badge pour non lues
if (notificationCount > 0) {
$('#notificationCount').text(notificationCount).show();
$('#markAllAsReadBtn').show();
} else {
$('#notificationCount').hide();
$('#markAllAsReadBtn').hide();
}
// Ajouter l'événement clic pour marquer comme lu
@ -115,6 +131,28 @@ function fetchNotifications() {
});
}
// Fonction pour marquer toutes les notifications comme lues
function markAllAsRead() {
fetch("<?= base_url('notifications/markAllAsRead') ?>", {
method: "POST",
headers: { "Content-Type": "application/json" }
})
.then(response => response.json())
.then(data => {
console.log("All notifications marked as read:", data);
// Rafraîchir les notifications immédiatement
fetchNotifications();
})
.catch(error => console.error("Error:", error));
}
// Attacher l'événement au bouton
$(document).on('click', '#markAllAsReadBtn', function(e) {
e.preventDefault();
e.stopPropagation();
markAllAsRead();
});
// Rafraîchir toutes les 10 secondes
setInterval(fetchNotifications, 10000);
// Premier chargement

Loading…
Cancel
Save