verifyRole('viewAvance'); $data['page_title'] = $this->pageTitle; $Products = new Products(); $session = session(); $users = $session->get('user'); $store_id = $users['store_id']; $data['products'] = $Products->getProductDataStore($store_id); return $this->render_template('avances/avance', $data); } private function isAdmin($user) { return in_array($user['group_name'], ['Conseil', 'Direction']); } private function isCommerciale($user) { return in_array($user['group_name'], ['COMMERCIALE']); } private function isCaissier($user) { return in_array($user['group_name'], ['Caissière']); } /** * Modifier la méthode buildActionButtons pour ajouter l'icône œil pour la Direction */ private function buildActionButtons($value, $isAdmin, $isOwner, $isCaissier = false) { $session = session(); $users = $session->get('user'); $isDirection = in_array($users['group_name'], ['Direction', 'Conseil']); $buttons = ''; // ✅ Bouton Voir pour Caissière (toujours visible) if ($isCaissier && in_array('viewAvance', $this->permission)) { $buttons .= ' '; } // ✅ Bouton Voir pour Direction (toujours visible, sans impression) if ($isDirection && in_array('viewAvance', $this->permission) && !$isCaissier) { $buttons .= ' '; } // ✅ MODIFIÉ : Bouton Modifier - Le caissier peut maintenant modifier toutes les avances if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) { $buttons .= ' '; } // ✅ MODIFIÉ : Bouton Supprimer - Le caissier peut maintenant supprimer toutes les avances if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) { $buttons .= ' '; } return $buttons; } private function buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons) { $date_time = date('d-m-Y h:i a', strtotime($value['avance_date'])); // ✅ Afficher product_name si disponible, sinon récupérer depuis la BDD $productName = !empty($value['product_name']) ? $value['product_name'] : $product->getProductNameById($value['product_id']); if ($isAdmin) { return [ $value['customer_name'], $value['customer_phone'], $value['customer_address'], $productName, number_format((int)$value['gross_amount'], 0, ',', ' '), number_format((int)$value['avance_amount'], 0, ',', ' '), number_format((int)$value['amount_due'], 0, ',', ' '), $date_time, $buttons, ]; } elseif ($isCommerciale || $isCaissier) { return [ $value['avance_id'], $productName, number_format((int)$value['avance_amount'], 0, ',', ' '), number_format((int)$value['amount_due'], 0, ',', ' '), $date_time, $buttons, ]; } return []; } private function fetchAvanceDataGeneric($methodName = 'getAllAvanceData') { helper(['url', 'form']); $Avance = new Avance(); $product = new Products(); $result = ['data' => []]; $data = $Avance->$methodName(); $session = session(); $users = $session->get('user'); $isAdmin = $this->isAdmin($users); $isCommerciale = $this->isCommerciale($users); $isCaissier = $this->isCaissier($users); foreach ($data as $key => $value) { $isOwner = $users['id'] === $value['user_id']; // ✅ MODIFIÉ : Passer $isCaissier aux boutons d'action $buttons = $this->buildActionButtons($value, $isAdmin, $isOwner, $isCaissier); $row = $this->buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons); if (!empty($row)) { $result['data'][] = $row; } } return $this->response->setJSON($result); } public function fetchAvanceData() { return $this->fetchAvanceDataGeneric('getIncompleteAvances'); } public function fetchAvanceBecameOrder() { return $this->fetchAvanceDataGeneric('getCompletedAvances'); } public function fetchExpiredAvance() { return $this->fetchAvanceDataGeneric('getAllAvanceData2'); } /** * Méthode pour vérifier et envoyer des emails d'alerte 3 jours avant deadline * À exécuter via CRON job quotidiennement */ public function checkDeadlineAlerts() { try { $Avance = new Avance(); $Products = new Products(); // Récupérer toutes les avances actives non converties en commandes $avances = $Avance->getAvancesNearDeadline(3); // 3 jours avant deadline if (!empty($avances)) { foreach ($avances as $avance) { // Vérifier si l'email n'a pas déjà été envoyé pour cette avance if (!$this->hasEmailBeenSent($avance['avance_id'])) { $this->sendDeadlineAlert($avance, $Products); $this->markEmailAsSent($avance['avance_id']); } } } return $this->response->setJSON([ 'success' => true, 'messages' => 'Vérification des alertes terminée', 'alerts_sent' => count($avances) ]); } catch (\Exception $e) { log_message('error', "Erreur vérification deadline: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de la vérification des deadlines' ]); } } /** * Envoyer un email d'alerte au DAF et à la Directrice */ private function sendDeadlineAlert($avance, $Products) { try { $email = \Config\Services::email(); // Configuration email (à adapter selon votre config) $email->setFrom('noreply@yourcompany.com', 'Système de Gestion des Avances'); // Récupérer les emails du DAF et de la Directrice $recipients = $this->getDAFAndDirectriceEmails($avance['store_id']); $email->setTo($recipients); $email->setSubject('⚠️ ALERTE: Avance arrive à échéance dans 3 jours'); // Récupérer le nom du produit $productName = $Products->getProductNameById($avance['product_id']); // Calcul des jours restants $deadline = new \DateTime($avance['deadline']); $today = new \DateTime(); $daysRemaining = $today->diff($deadline)->days; // Corps de l'email $message = $this->buildEmailMessage($avance, $productName, $daysRemaining); $email->setMessage($message); // Envoyer l'email if ($email->send()) { log_message('info', "Email d'alerte envoyé pour l'avance ID: " . $avance['avance_id']); return true; } else { log_message('error', "Échec envoi email pour avance ID: " . $avance['avance_id'] . " - " . $email->printDebugger()); return false; } } catch (\Exception $e) { log_message('error', "Erreur envoi email alerte: " . $e->getMessage()); return false; } } /** * Récupérer les emails du DAF et de la Directrice */ private function getDAFAndDirectriceEmails($store_id) { $User = new User(); $emails = []; // Récupérer les utilisateurs avec les rôles DAF et Direction pour le store donné $dafUsers = $User->getUsersByRole('DAF', $store_id); $directionUsers = $User->getUsersByRole('Direction', $store_id); // Extraire les emails foreach ($dafUsers as $user) { if (!empty($user['email'])) { $emails[] = $user['email']; } } foreach ($directionUsers as $user) { if (!empty($user['email'])) { $emails[] = $user['email']; } } // Si aucun email trouvé, utiliser des emails par défaut (à configurer) if (empty($emails)) { $emails = [ 'daf@yourcompany.com', 'direction@yourcompany.com' ]; } return array_unique($emails); // Éviter les doublons } /** * Construire le message de l'email */ private function buildEmailMessage($avance, $productName, $daysRemaining) { $typeAvance = strtoupper($avance['type_avance']); $deadlineFormatted = date('d/m/Y', strtotime($avance['deadline'])); $avanceDateFormatted = date('d/m/Y à H:i', strtotime($avance['avance_date'])); $amountDueFormatted = number_format($avance['amount_due'], 0, ',', ' ') . ' FCFA'; $urgencyClass = $daysRemaining <= 1 ? 'style="color: red; font-weight: bold;"' : ''; return "

⚠️ ALERTE DEADLINE AVANCE

Une avance arrive à échéance dans {$daysRemaining} jour(s) !

Détails de l'avance :

Action requise :

Veuillez contacter le client pour régulariser le paiement avant l'échéance ou prendre les mesures appropriées.

"; } /** * Vérifier si un email a déjà été envoyé pour cette avance */ private function hasEmailBeenSent($avance_id) { $db = \Config\Database::connect(); $query = $db->query("SELECT id FROM email_alerts WHERE avance_id = ? AND alert_type = 'deadline_3days'", [$avance_id]); return $query->getNumRows() > 0; } /** * Marquer l'email comme envoyé */ private function markEmailAsSent($avance_id) { $db = \Config\Database::connect(); $data = [ 'avance_id' => $avance_id, 'alert_type' => 'deadline_3days', 'sent_date' => date('Y-m-d H:i:s'), 'status' => 'sent' ]; $db->table('email_alerts')->insert($data); } public function createAvance() { $this->verifyRole('createAvance'); if ($this->request->getMethod() !== 'post') { return $this->response->setJSON([ 'success' => false, 'messages' => 'Méthode non autorisée' ]); } try { $session = session(); $users = $session->get('user'); $Avance = new Avance(); $Products = new Products(); $Notification = new NotificationController(); // ✅ Récupérer le type AVANT la validation $type_avance = $this->request->getPost('type_avance'); // ✅ Validation conditionnelle $validation = \Config\Services::validation(); $baseRules = [ 'customer_name_avance' => 'required|min_length[2]', 'customer_phone_avance' => 'required', 'customer_address_avance' => 'required', 'customer_cin_avance' => 'required', 'gross_amount' => 'required|numeric|greater_than[0]', 'avance_amount' => 'required|numeric|greater_than[0]', 'type_avance' => 'required|in_list[terre,mere]', 'type_payment' => 'required' ]; if ($type_avance === 'mere') { $baseRules['product_name_text'] = 'required|min_length[2]'; } else { $baseRules['id_product'] = 'required|numeric'; } $validation->setRules($baseRules); if (!$validation->withRequest($this->request)->run()) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Données invalides: ' . implode(', ', $validation->getErrors()) ]); } $avance_date = date('Y-m-d H:i:s'); // Calcul de la deadline if ($type_avance === 'terre') { $deadline = date('Y-m-d', strtotime($avance_date . ' +15 days')); } elseif ($type_avance === 'mere') { $deadline = date('Y-m-d', strtotime($avance_date . ' +2 months')); } else { $deadline = null; } // Préparer les données communes $data = [ 'type_avance' => $type_avance, 'type_payment' => $this->request->getPost('type_payment'), 'customer_name' => $this->request->getPost('customer_name_avance'), 'customer_address' => $this->request->getPost('customer_address_avance'), 'customer_phone' => $this->request->getPost('customer_phone_avance'), 'customer_cin' => $this->request->getPost('customer_cin_avance'), 'avance_date' => $avance_date, 'deadline' => $deadline, 'user_id' => $users['id'], 'store_id' => $users['store_id'], 'gross_amount' => (float)$this->request->getPost('gross_amount'), 'avance_amount' => (float)$this->request->getPost('avance_amount'), 'amount_due' => (float)$this->request->getPost('gross_amount') - (float)$this->request->getPost('avance_amount'), 'is_order' => 0, 'active' => 1, ]; // ✅ Ajouter le produit selon le type if ($type_avance === 'mere') { $data['product_name'] = $this->request->getPost('product_name_text'); $data['commentaire'] = $this->request->getPost('commentaire'); } else { $data['product_id'] = (int)$this->request->getPost('id_product'); $data['product_name'] = null; $data['commentaire'] = null; } if ($avance_id = $Avance->createAvance($data)) { // ✅ Marquer le produit comme vendu UNIQUEMENT pour "terre" if ($type_avance === 'terre' && !empty($data['product_id'])) { $Products->update($data['product_id'], ['product_sold' => 1]); } // ✅ NOUVELLE FONCTIONNALITÉ : Envoyer notification au Conseil $Notification->createNotification( 'Une nouvelle avance a été créée', "DAF", (int)$users['store_id'], 'avances' ); // ✅ NOUVELLE FONCTIONNALITÉ : Envoyer notification à la Caissière si l'utilisateur est COMMERCIALE if ($this->isCommerciale($users)) { $Notification->createNotification( 'Une nouvelle avance a été créée par un commercial', "Caissière", (int)$users['store_id'], 'avances' ); } return $this->response->setJSON([ 'success' => true, 'messages' => 'Avance créée avec succès !', 'avance_id' => $avance_id ]); } else { return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de la création de l\'avance' ]); } } catch (\Exception $e) { log_message('error', "Erreur création avance: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Une erreur interne est survenue: ' . $e->getMessage() ]); } } public function updateAvance() { $this->verifyRole('updateAvance'); if ($this->request->getMethod() !== 'post') { return $this->response->setJSON([ 'success' => false, 'messages' => 'Méthode non autorisée' ]); } try { $session = session(); $users = $session->get('user'); $Avance = new Avance(); $Products = new Products(); $Notification = new NotificationController(); $type_avance = $this->request->getPost('type_avance_edit'); $validation = \Config\Services::validation(); $baseRules = [ 'id' => 'required|numeric', 'customer_name_avance_edit' => 'required|min_length[2]', 'customer_phone_avance_edit' => 'required', 'customer_address_avance_edit' => 'required', 'customer_cin_avance_edit' => 'required', 'gross_amount_edit' => 'required|numeric|greater_than[0]', 'avance_amount_edit' => 'required|numeric|greater_than[0]', 'type_avance_edit' => 'required|in_list[terre,mere]', 'type_payment_edit' => 'required' ]; if ($type_avance === 'mere') { $baseRules['product_name_text_edit'] = 'required|min_length[2]'; } else { $baseRules['id_product_edit'] = 'required|numeric'; } $validation->setRules($baseRules); if (!$validation->withRequest($this->request)->run()) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Données invalides: ' . implode(', ', $validation->getErrors()) ]); } $avance_id = $this->request->getPost('id'); $existingAvance = $Avance->fetchSingleAvance($avance_id); if (!$existingAvance) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Avance non trouvée' ]); } $isAdmin = $this->isAdmin($users); $isOwner = $users['id'] === $existingAvance['user_id']; $isCaissier = $this->isCaissier($users); // ✅ MODIFIÉ : Le caissier peut maintenant modifier toutes les avances if (!$isAdmin && !$isOwner && !$isCaissier) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Vous n\'avez pas les droits pour modifier cette avance' ]); } $current_deadline = $existingAvance['deadline']; if ($type_avance !== $existingAvance['type_avance']) { if ($type_avance === 'terre') { $current_deadline = date('Y-m-d', strtotime($existingAvance['avance_date'] . ' +15 days')); } elseif ($type_avance === 'mere') { $current_deadline = date('Y-m-d', strtotime($existingAvance['avance_date'] . ' +2 months')); } } $old_product_id = $existingAvance['product_id']; $data = [ 'type_avance' => $type_avance, 'type_payment' => $this->request->getPost('type_payment_edit'), 'customer_name' => $this->request->getPost('customer_name_avance_edit'), 'customer_address' => $this->request->getPost('customer_address_avance_edit'), 'customer_phone' => $this->request->getPost('customer_phone_avance_edit'), 'customer_cin' => $this->request->getPost('customer_cin_avance_edit'), 'deadline' => $current_deadline, 'gross_amount' => (float)$this->request->getPost('gross_amount_edit'), 'avance_amount' => (float)$this->request->getPost('avance_amount_edit'), 'amount_due' => (float)$this->request->getPost('amount_due_edit') ]; if ($type_avance === 'mere') { $data['product_name'] = $this->request->getPost('product_name_text_edit'); $data['product_id'] = null; $data['commentaire'] = $this->request->getPost('commentaire_edit'); $new_product_id = null; } else { $new_product_id = (int)$this->request->getPost('id_product_edit'); $data['product_id'] = $new_product_id; $data['product_name'] = null; $data['commentaire'] = null; } if ($Avance->updateAvance($avance_id, $data)) { if ($type_avance === 'terre') { if ($old_product_id && $old_product_id !== $new_product_id) { $Products->update($old_product_id, ['product_sold' => 0]); } if ($new_product_id) { $Products->update($new_product_id, ['product_sold' => 1]); } } else { if ($old_product_id && $existingAvance['type_avance'] === 'terre') { $Products->update($old_product_id, ['product_sold' => 0]); } } // ✅ Notification simplifiée $Notification->createNotification( 'Une avance a été modifiée', "Caissière", (int)$users['store_id'], 'avances' ); return $this->response->setJSON([ 'success' => true, 'messages' => 'Avance modifiée avec succès !' ]); } else { return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de la modification de l\'avance' ]); } } catch (\Exception $e) { log_message('error', "Erreur modification avance: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Une erreur interne est survenue: ' . $e->getMessage() ]); } } public function removeAvance() { $this->verifyRole('deleteAvance'); try { $session = session(); $users = $session->get('user'); $avance_id = $this->request->getPost('avance_id'); $product_id = $this->request->getPost('product_id'); if (!$avance_id || !$product_id) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Données manquantes pour la suppression' ]); } $Avance = new Avance(); $Products = new Products(); // ✅ AJOUT : Vérifier les permissions pour la suppression $existingAvance = $Avance->fetchSingleAvance($avance_id); if (!$existingAvance) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Avance non trouvée' ]); } $isAdmin = $this->isAdmin($users); $isOwner = $users['id'] === $existingAvance['user_id']; $isCaissier = $this->isCaissier($users); // ✅ MODIFIÉ : Le caissier peut maintenant supprimer toutes les avances if (!$isAdmin && !$isOwner && !$isCaissier) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Vous n\'avez pas les droits pour supprimer cette avance' ]); } if ($Avance->removeAvance($avance_id)) { $Products->update($product_id, ['product_sold' => 0]); return $this->response->setJSON([ 'success' => true, 'messages' => "Avance supprimée avec succès. Le produit peut être réservé à nouveau." ]); } else { return $this->response->setJSON([ 'success' => false, 'messages' => "Erreur lors de la suppression de l'avance" ]); } } catch (\Exception $e) { log_message('error', "Erreur suppression avance: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Une erreur interne est survenue' ]); } } public function fetchSingleAvance($avance_id) { $this->verifyRole('updateAvance'); try { if (!$avance_id || !is_numeric($avance_id)) { return $this->response->setStatusCode(400)->setJSON([ 'error' => 'ID d\'avance invalide' ]); } $avanceModel = new Avance(); $data = $avanceModel->fetchSingleAvance($avance_id); if (!$data) { return $this->response->setStatusCode(404)->setJSON([ 'error' => 'Avance non trouvée' ]); } return $this->response->setJSON($data); } catch (\Exception $e) { log_message('error', "Erreur récupération avance: " . $e->getMessage()); return $this->response->setStatusCode(500)->setJSON([ 'error' => 'Erreur interne lors de la récupération de l\'avance' ]); } } /** * Méthode pour l'impression directe (si vous l'utilisez encore) */ public function printInvoice($avance_id) { $this->verifyRole('viewAvance'); try { $session = session(); $users = $session->get('user'); if (!$this->isCaissier($users)) { return redirect()->back()->with('error', 'Seule la caissière peut imprimer les factures'); } $Avance = new Avance(); $Products = new Products(); $avance = $Avance->fetchSingleAvance($avance_id); if (!$avance) { return redirect()->back()->with('error', 'Avance non trouvée'); } if ($avance['store_id'] !== $users['store_id']) { return redirect()->back()->with('error', 'Accès non autorisé'); } // ✅ CORRECTION SIMPLIFIÉE if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) { $productName = $avance['product_name']; $productDetails = [ 'marque' => $avance['product_name'], 'numero_moteur' => '', 'puissance' => '' ]; } else { $product = $Products->find($avance['product_id']); if (!$product) { return redirect()->back()->with('error', 'Produit non trouvé'); } $productName = $product['name'] ?? 'N/A'; // ✅ Récupérer le nom de la marque $brandName = 'N/A'; if (!empty($product['marque'])) { $db = \Config\Database::connect(); $brandQuery = $db->table('brands') ->select('name') ->where('id', $product['marque']) ->get(); $brandResult = $brandQuery->getRowArray(); if ($brandResult) { $brandName = $brandResult['name']; } } $productDetails = [ 'marque' => $brandName, 'numero_moteur' => $product['numero_de_moteur'] ?? '', 'puissance' => $product['puissance'] ?? '' ]; } $html = $this->generatePrintableInvoiceHTML($avance, $productName, $productDetails); return $this->response->setBody($html); } catch (\Exception $e) { log_message('error', "Erreur impression facture: " . $e->getMessage()); return redirect()->back()->with('error', 'Erreur lors de l\'impression'); } } // private function generateInvoiceHTML($avance, $productName, $productDetails) // { // $avanceDate = date('d/m/Y', strtotime($avance['avance_date'])); // $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); // $customerName = strtoupper(esc($avance['customer_name'])); // $customerPhone = esc($avance['customer_phone']); // $grossAmount = number_format($avance['gross_amount'], 0, ',', ' '); // $avanceAmount = number_format($avance['avance_amount'], 0, ',', ' '); // $amountDue = number_format($avance['amount_due'], 0, ',', ' '); // $marque = esc($productDetails['marque']) ?: $productName; // $numeroMoteur = esc($productDetails['numero_moteur']); // $puissance = esc($productDetails['puissance']); // return << // // // // // Facture Avance - KELY SCOOTERS // // // // //
// //
//

KELY SCOOTERS

//
//
//
NIF: 401 840 5554
//
STAT: 46101 11 2024 00317
//
//
//
Contact: +261 34 27 946 35 / +261 34 07 079 69
//
Antsakaviro en face WWF
//
//
//
// //
//
//

FACTURE

//
Date: {$avanceDate}
//
N°: {$avanceNumber}CI 2025
//
//
DOIT ORIGINAL
//
// //
//
NOM: {$customerName} ({$customerPhone})
//
PC: {$grossAmount} Ar
//
AVANCE: {$avanceAmount} Ar
//
RAP: {$amountDue} Ar
//
// // // // // // // // // // // // // // // // // // //
MARQUEN°MOTEURPUISSANCERAP (Ariary)
{$marque}{$numeroMoteur}{$puissance}{$amountDue}
// //
//

FIFANEKENA ARA-BAROTRA (Réservations)

//

Ry mpanjifa hajaina,

//

Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.

//
// Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA //

Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).

//
//
// Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE) //

Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.

//

Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.

//
//
// Andininy faha-3: FAMERENANA VOLA //

Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.

//
//
// Andininy faha-4: FEPETRA FANAMPINY //
    //
  • Tsy misafidy raha toa ka mamafa no ifanarahana.
  • //
  • Tsy azo atao ny mamerina ny entana efa nofandrahana.
  • //
  • Tsy azo atao ny manakalo ny entana efa nofandrahana.
  • //
//
//
// //
//
//

NY MPAMANDRIKA

//
Signature
//
//
//

NY MPIVAROTRA

//
// KELY SCOOTERS
// NIF: 401 840 5554 //
//
//
//
// // // // HTML; // } /** * Récupérer la prévisualisation de la facture pour le modal */ /** * Récupérer la prévisualisation de la facture pour le modal */ public function getInvoicePreview($avance_id) { $this->verifyRole('viewAvance'); try { $session = session(); $users = $session->get('user'); $isCaissier = $this->isCaissier($users); $isDirection = in_array($users['group_name'], ['Direction', 'Conseil']); if (!$isCaissier && !$isDirection) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Accès non autorisé' ]); } $Avance = new Avance(); $Products = new Products(); $avance = $Avance->fetchSingleAvance($avance_id); if (!$avance) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Avance non trouvée' ]); } if (!$isDirection && $avance['store_id'] !== $users['store_id']) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Accès non autorisé à cette avance' ]); } // ✅ CORRECTION SIMPLIFIÉE: Récupérer le nom de la marque if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) { $productName = $avance['product_name']; $productDetails = [ 'marque' => $avance['product_name'], 'numero_moteur' => '', 'puissance' => '' ]; } else { // Récupérer le produit $product = $Products->find($avance['product_id']); if (!$product) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Produit non trouvé' ]); } $productName = $product['name'] ?? 'N/A'; // ✅ Récupérer le nom de la marque depuis la table brands $brandName = 'N/A'; if (!empty($product['marque'])) { $db = \Config\Database::connect(); $brandQuery = $db->table('brands') ->select('name') ->where('id', $product['marque']) ->get(); $brandResult = $brandQuery->getRowArray(); if ($brandResult) { $brandName = $brandResult['name']; } } $productDetails = [ 'marque' => $brandName, // ✅ Nom de la marque au lieu de l'ID 'numero_moteur' => $product['numero_de_moteur'] ?? '', 'puissance' => $product['puissance'] ?? '' ]; } $html = $this->generateSimplifiedInvoiceForModal($avance, $productName, $productDetails); return $this->response->setJSON([ 'success' => true, 'html' => $html, 'avance_id' => $avance_id, 'can_print' => $isCaissier ]); } catch (\Exception $e) { log_message('error', "Erreur prévisualisation facture: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de la prévisualisation : ' . $e->getMessage() ]); } } /** * Générer le HTML de la facture pour le modal (version simplifiée) */ /** * Générer un aperçu simplifié de la facture pour le modal */ private function generateSimplifiedInvoiceForModal($avance, $productName, $productDetails) { $avanceDate = date('d/m/Y', strtotime($avance['avance_date'])); $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); $customerName = strtoupper(esc($avance['customer_name'])); $customerPhone = esc($avance['customer_phone']); $customerCin = esc($avance['customer_cin']); $grossAmount = number_format($avance['gross_amount'], 0, ',', ' '); $avanceAmount = number_format($avance['avance_amount'], 0, ',', ' '); $amountDue = number_format($avance['amount_due'], 0, ',', ' '); $marque = esc($productDetails['marque']) ?: $productName; return << .simplified-invoice { font-family: Arial, sans-serif; max-width: 100%; background: white; padding: 20px; } .simplified-header { display: flex; justify-content: space-between; border-bottom: 2px solid #333; padding-bottom: 15px; margin-bottom: 20px; } .company-info h3 { margin: 0 0 10px 0; font-size: 20px; font-weight: bold; } .company-info p { margin: 3px 0; font-size: 11px; } .invoice-info { text-align: right; } .invoice-info h2 { margin: 0 0 10px 0; font-size: 28px; font-weight: bold; color: #333; } .invoice-info p { margin: 3px 0; font-size: 13px; } .doit-badge { display: inline-block; background: #000; color: #fff; padding: 5px 20px; font-weight: bold; transform: skewX(-10deg); margin-top: 10px; } .customer-section { background: #f8f8f8; padding: 15px; border: 1px solid #ddd; margin: 20px 0; } .customer-section h4 { margin: 0 0 10px 0; font-size: 14px; font-weight: bold; } .customer-details { font-size: 13px; line-height: 1.8; } .customer-details strong { display: inline-block; width: 80px; } .amounts-box { border: 2px solid #333; padding: 15px; margin: 20px 0; background: #fff; } .amount-row { display: flex; justify-content: space-between; padding: 8px 0; font-size: 14px; border-bottom: 1px solid #eee; } .amount-row:last-child { border-bottom: none; font-weight: bold; font-size: 16px; color: #d32f2f; } .amount-row strong { min-width: 120px; } .product-table { width: 100%; border-collapse: collapse; margin: 20px 0; } .product-table th, .product-table td { border: 1px solid #333; padding: 10px; text-align: left; } .product-table th { background: #f0f0f0; font-weight: bold; font-size: 13px; } .product-table td { font-size: 13px; }

KELY SCOOTERS

NIF: 401 840 5554

STAT: 46101 11 2024 00317

Contact: +261 34 27 946 35 / +261 34 07 079 69

Antsakaviro en face WWF

FACTURE

Date: {$avanceDate}

N°: {$avanceNumber}

DOIT ORIGINAL

INFORMATIONS CLIENT

NOM: {$customerName}
Téléphone: {$customerPhone}
CIN: {$customerCin}
PC (Prix Total): {$grossAmount} Ar
AVANCE: {$avanceAmount} Ar
RAP (Reste à payer): {$amountDue} Ar
MARQUE N°MOTEUR PUISSANCE RAP (Ariary)
{$marque} {$productDetails['numero_moteur']} {$productDetails['puissance']} {$amountDue}
HTML; } public function notifyPrintInvoice() { $this->verifyRole('viewAvance'); try { $session = session(); $users = $session->get('user'); if (!$this->isCaissier($users)) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Accès non autorisé' ]); } $avance_id = $this->request->getPost('avance_id'); if (!$avance_id) { return $this->response->setJSON([ 'success' => false, 'messages' => 'ID avance manquant' ]); } $Avance = new Avance(); $avance = $Avance->fetchSingleAvance($avance_id); if (!$avance) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Avance non trouvée' ]); } if ($avance['store_id'] !== $users['store_id']) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Accès non autorisé' ]); } // ✅ RETIRÉ : $Avance->markAsPrinted($avance_id); // Envoyer notification à la Direction $Notification = new NotificationController(); $customerName = $avance['customer_name']; $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); $Notification->createNotification( "La caissière a imprimé la facture N°{$avanceNumber} pour le client {$customerName}", "Direction", (int)$users['store_id'], 'avances' ); $Notification->createNotification( "Il y a une avance N°{$avanceNumber} pour le client {$customerName}", "DAF", (int)$users['store_id'], 'avances' ); return $this->response->setJSON([ 'success' => true, 'messages' => 'Facture imprimée avec succès ! Notification envoyée à la Direction.' ]); } catch (\Exception $e) { log_message('error', "Erreur notification impression: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de l\'envoi de la notification' ]); } } /** * Récupérer le HTML complet de la facture pour impression */ public function getFullInvoiceForPrint($avance_id) { $this->verifyRole('viewAvance'); try { $session = session(); $users = $session->get('user'); if (!$this->isCaissier($users)) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Seule la caissière peut imprimer les factures' ]); } $Avance = new Avance(); $Products = new Products(); $avance = $Avance->fetchSingleAvance($avance_id); if (!$avance) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Avance non trouvée' ]); } if ($avance['store_id'] !== $users['store_id']) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Accès non autorisé' ]); } // ✅ CORRECTION SIMPLIFIÉE: Récupérer le nom de la marque if ($avance['type_avance'] === 'mere' && !empty($avance['product_name'])) { $productName = $avance['product_name']; $productDetails = [ 'marque' => $avance['product_name'], 'numero_moteur' => '', 'puissance' => '' ]; } else { $product = $Products->find($avance['product_id']); if (!$product) { return $this->response->setJSON([ 'success' => false, 'messages' => 'Produit non trouvé' ]); } $productName = $product['name'] ?? 'N/A'; // ✅ Récupérer le nom de la marque depuis la table brands $brandName = 'N/A'; if (!empty($product['marque'])) { $db = \Config\Database::connect(); $brandQuery = $db->table('brands') ->select('name') ->where('id', $product['marque']) ->get(); $brandResult = $brandQuery->getRowArray(); if ($brandResult) { $brandName = $brandResult['name']; } } $productDetails = [ 'marque' => $brandName, // ✅ Nom de la marque 'numero_moteur' => $product['numero_de_moteur'] ?? '', 'puissance' => $product['puissance'] ?? '' ]; } $html = $this->generatePrintableInvoiceHTML($avance, $productName, $productDetails); return $this->response->setJSON([ 'success' => true, 'html' => $html ]); } catch (\Exception $e) { log_message('error', "Erreur récupération facture impression: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de la récupération de la facture' ]); } } /** * Générer le HTML optimisé pour l'impression (version identique à printInvoice) */ private function generatePrintableInvoiceHTML($avance, $productName, $productDetails) { $avanceDate = date('d/m/Y', strtotime($avance['avance_date'])); $avanceNumber = str_pad($avance['avance_id'], 5, '0', STR_PAD_LEFT); $customerName = strtoupper(esc($avance['customer_name'])); $customerPhone = esc($avance['customer_phone']); $customerCin = esc($avance['customer_cin']); $grossAmount = number_format($avance['gross_amount'], 0, ',', ' '); $avanceAmount = number_format($avance['avance_amount'], 0, ',', ' '); $amountDue = number_format($avance['amount_due'], 0, ',', ' '); $marque = esc($productDetails['marque']) ?: $productName; $numeroMoteur = esc($productDetails['numero_moteur']); $puissance = esc($productDetails['puissance']); return << Facture Avance - KELY SCOOTERS

KELY SCOOTERS

NIF: 401 840 5554
STAT: 46101 11 2024 00317
Contact: +261 34 27 946 35 / +261 34 07 079 69
Antsakaviro en face WWF

FACTURE

Date: {$avanceDate}
N°: {$avanceNumber}CI 2025
DOIT ORIGINAL
NOM: {$customerName} ({$customerPhone})
CIN: {$customerCin}
PC: {$grossAmount} Ar
AVANCE: {$avanceAmount} Ar
RAP: {$amountDue} Ar
MARQUE N°MOTEUR PUISSANCE RAP (Ariary)
{$marque} {$numeroMoteur} {$puissance} {$amountDue}

FIFANEKENA ARA-BAROTRA (Réservations)

Ry mpanjifa hajaina,

Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.

Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA

Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).

Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE)

Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.

Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.

Andininy faha-3: FAMERENANA VOLA

Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.

Andininy faha-4: FEPETRA FANAMPINY
  • Tsy misafidy raha toa ka mamafa no ifanarahana.
  • Tsy azo atao ny mamerina ny entana efa nofandrahana.
  • Tsy azo atao ny manakalo ny entana efa nofandrahana.

NY MPAMANDRIKA

Signature

NY MPIVAROTRA

KELY SCOOTERS
NIF: 401 840 5554
HTML; } /** * ✅ NOUVELLE MÉTHODE : Traiter manuellement les avances expirées * URL: /avances/processExpiredAvances * Accessible via bouton dans l'interface ou manuellement */ public function processExpiredAvances() { try { log_message('info', "=== DÉBUT processExpiredAvances (manuel) ==="); $Avance = new Avance(); $Products = new Products(); $today = date('Y-m-d'); // Récupérer les avances expirées et encore actives $expiredAvances = $Avance ->where('DATE(deadline) <', $today) ->where('active', 1) ->where('is_order', 0) ->findAll(); if (empty($expiredAvances)) { return $this->response->setJSON([ 'success' => true, 'messages' => 'Aucune avance expirée à traiter', 'processed' => 0 ]); } $processedCount = 0; $errorCount = 0; $details = []; foreach ($expiredAvances as $avance) { try { // Désactiver l'avance $Avance->update($avance['avance_id'], ['active' => 0]); $detail = [ 'avance_id' => $avance['avance_id'], 'customer' => $avance['customer_name'], 'deadline' => $avance['deadline'], 'product_freed' => false ]; // Libérer le produit si c'est une avance "sur terre" if ($avance['type_avance'] === 'terre' && !empty($avance['product_id'])) { $Products->update($avance['product_id'], ['product_sold' => 0]); $detail['product_freed'] = true; $detail['product_id'] = $avance['product_id']; } $details[] = $detail; $processedCount++; } catch (\Exception $e) { log_message('error', "Erreur traitement avance {$avance['avance_id']}: " . $e->getMessage()); $errorCount++; } } log_message('info', "=== FIN processExpiredAvances - Traités: {$processedCount}, Erreurs: {$errorCount} ==="); return $this->response->setJSON([ 'success' => true, 'messages' => "Avances expirées traitées avec succès", 'processed' => $processedCount, 'errors' => $errorCount, 'details' => $details ]); } catch (\Exception $e) { log_message('error', "Erreur processExpiredAvances: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors du traitement des avances expirées: ' . $e->getMessage() ]); } } /** * ✅ NOUVELLE MÉTHODE : Vérifier les avances qui vont expirer dans X jours * URL: /avances/checkExpiringAvances/{days} * Utile pour la Direction/DAF pour anticiper */ public function checkExpiringAvances($days = 0) { try { $Avance = new Avance(); $Products = new Products(); $targetDate = date('Y-m-d', strtotime("+{$days} days")); $expiringAvances = $Avance ->where('DATE(deadline)', $targetDate) ->where('active', 1) ->where('is_order', 0) ->findAll(); $result = []; foreach ($expiringAvances as $avance) { $productInfo = null; if ($avance['type_avance'] === 'terre' && !empty($avance['product_id'])) { $product = $Products->find($avance['product_id']); if ($product) { $productInfo = [ 'id' => $product['id'], 'name' => $product['name'], 'sku' => $product['sku'] ]; } } $result[] = [ 'avance_id' => $avance['avance_id'], 'customer_name' => $avance['customer_name'], 'customer_phone' => $avance['customer_phone'], 'deadline' => $avance['deadline'], 'amount_due' => $avance['amount_due'], 'type_avance' => $avance['type_avance'], 'product' => $productInfo ]; } return $this->response->setJSON([ 'success' => true, 'count' => count($result), 'target_date' => $targetDate, 'avances' => $result ]); } catch (\Exception $e) { log_message('error', "Erreur checkExpiringAvances: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur lors de la vérification: ' . $e->getMessage() ]); } } // Dans App\Controllers\AvanceController.php /** * ✅ Fonction de paiement d'avance (à modifier ou créer) */ public function payAvance() { $avance_id = $this->request->getPost('avance_id'); $montant_paye = (float)$this->request->getPost('montant_paye'); $avanceModel = new \App\Models\Avance(); $avance = $avanceModel->find($avance_id); if (!$avance) { return $this->response->setJSON([ 'success' => false, 'message' => 'Avance introuvable' ]); } // Calcul du nouveau montant dû $amount_due = max(0, (float)$avance['amount_due'] - $montant_paye); // ✅ Mise à jour de l'avance $avanceModel->update($avance_id, [ 'avance_amount' => (float)$avance['avance_amount'] + $montant_paye, 'amount_due' => $amount_due, ]); // ✅ NOUVEAU : Conversion automatique UNIQUEMENT pour avances TERRE if ($amount_due <= 0) { if ($avance['type_avance'] === 'terre') { // ✅ Avance TERRE complète → Conversion en commande log_message('info', "💰 Avance TERRE {$avance_id} complétée ! Conversion en commande..."); $order_id = $avanceModel->convertToOrder($avance_id); if ($order_id) { return $this->response->setJSON([ 'success' => true, 'message' => '✅ Paiement effectué ! L\'avance TERRE a été convertie en commande.', 'converted' => true, 'type' => 'terre', 'order_id' => $order_id, 'redirect_url' => site_url('orders/update/' . $order_id) ]); } else { log_message('error', "Échec conversion avance TERRE {$avance_id} en commande"); return $this->response->setJSON([ 'success' => true, 'message' => '⚠️ Paiement effectué mais erreur lors de la création de la commande.', 'converted' => false ]); } } else { // ✅ Avance MER complète → Reste dans la liste log_message('info', "💰 Avance MER {$avance_id} complétée ! Elle reste dans la liste des avances."); return $this->response->setJSON([ 'success' => true, 'message' => '✅ Paiement effectué ! L\'avance MER est maintenant complète.', 'converted' => false, 'type' => 'mere', 'status' => 'completed' ]); } } // ✅ Paiement partiel return $this->response->setJSON([ 'success' => true, 'message' => '✅ Paiement partiel enregistré avec succès', 'amount_due_remaining' => $amount_due, 'type' => $avance['type_avance'] ]); } /** * ✅ Conversion manuelle (optionnel - pour forcer la conversion) */ public function forceConvertToOrder($avance_id) { $this->verifyRole('updateAvance'); // Adapter selon vos permissions $avanceModel = new \App\Models\Avance(); $order_id = $avanceModel->convertToOrder($avance_id); if ($order_id) { session()->setFlashdata('success', 'Avance convertie en commande avec succès !'); return redirect()->to('orders/update/' . $order_id); } else { session()->setFlashdata('errors', 'Erreur lors de la conversion de l\'avance.'); return redirect()->back(); } } /** * ✅ Vérifier et convertir toutes les avances complètes * URL: /avances/checkAndConvertCompleted */ public function checkAndConvertCompleted() { try { $Avance = new Avance(); // ✅ Récupérer uniquement les avances TERRE complètes non converties $completedTerreAvances = $Avance->getCompletedNotConverted(); $convertedCount = 0; $errorCount = 0; $details = []; foreach ($completedTerreAvances as $avance) { $order_id = $Avance->convertToOrder($avance['avance_id']); if ($order_id) { $convertedCount++; $details[] = [ 'avance_id' => $avance['avance_id'], 'customer' => $avance['customer_name'], 'type' => 'terre', 'order_id' => $order_id, 'status' => 'success' ]; } else { $errorCount++; $details[] = [ 'avance_id' => $avance['avance_id'], 'customer' => $avance['customer_name'], 'type' => 'terre', 'status' => 'error' ]; } } return $this->response->setJSON([ 'success' => true, 'message' => 'Vérification terminée', 'converted' => $convertedCount, 'errors' => $errorCount, 'note' => 'Seules les avances TERRE sont converties. Les avances MER restent dans la liste.', 'details' => $details ]); } catch (\Exception $e) { log_message('error', "Erreur checkAndConvertCompleted: " . $e->getMessage()); return $this->response->setJSON([ 'success' => false, 'messages' => 'Erreur: ' . $e->getMessage() ]); } } }