diff --git a/app/Config/Routes.php b/app/Config/Routes.php
index 86209a30..7126fd64 100644
--- a/app/Config/Routes.php
+++ b/app/Config/Routes.php
@@ -56,6 +56,7 @@ $routes->group('', ['filter' => 'auth'], function ($routes) {
$routes->get('/ventes/show/(:num)', [Auth::class, 'getSingle']);
$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) {
- $routes->get('/', [AvanceController::class, 'index']);
- $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->post('createAvance', [AvanceController::class, 'createAvance']);
- $routes->post('deleteAvance', [AvanceController::class, 'removeAvance']);
- $routes->post('updateAvance/(:num)', [AvanceController::class, 'updateAvance']);
- });
+// 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('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('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');
diff --git a/app/Controllers/AdminController.php b/app/Controllers/AdminController.php
index ef5f6eca..897b7cb9 100644
--- a/app/Controllers/AdminController.php
+++ b/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'))) {
@@ -20,15 +19,15 @@ abstract class AdminController extends BaseController
} else {
$userIfo = session()->get('user');
$userId = $userIfo['id'];
-
+
$Groups = new Groups();
$group_data = $Groups->getUserGroupByUserId($userId);
-
+
$this->permission = unserialize($group_data['permission']);
+
}
}
-
/**
* finction to verify role of users
* @return mixed
diff --git a/app/Controllers/AvanceController.php b/app/Controllers/AvanceController.php
index 69bb6394..ed911e2b 100644
--- a/app/Controllers/AvanceController.php
+++ b/app/Controllers/AvanceController.php
@@ -41,114 +41,127 @@ class AvanceController extends AdminController
private function isCaissier($user)
{
- return in_array($user['group_name'], ['Caissier']);
+ return in_array($user['group_name'], ['Caissière']);
}
-
- private function buildActionButtons($value, $isAdmin, $isOwner)
- {
- $buttons = '';
-
- if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner)) {
- $buttons .= ' ';
- }
-
- if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner)) {
- $buttons .= ' ';
- }
-
- if (in_array('viewAvance', $this->permission) && !$isAdmin) {
- $buttons .= ''
- . '';
- }
-
-
- return $buttons;
+/**
+ * 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']));
+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
+ // ✅ 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, // ✅ Utiliser la variable
- 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, // ✅ Utiliser la variable
- number_format((int)$value['avance_amount'], 0, ',', ' '),
- number_format((int)$value['amount_due'], 0, ',', ' '),
- $date_time,
- $buttons,
- ];
- }
-
- return [];
+ ? $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' => []];
+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'];
- $data = $Avance->$methodName();
- $session = session();
- $users = $session->get('user');
+ // ✅ MODIFIÉ : Passer $isCaissier aux boutons d'action
+ $buttons = $this->buildActionButtons($value, $isAdmin, $isOwner, $isCaissier);
- $isAdmin = $this->isAdmin($users);
- $isCommerciale = $this->isCommerciale($users);
- $isCaissier = $this->isCaissier($users);
-
- foreach ($data as $key => $value) {
- $isOwner = $users['id'] === $value['user_id'];
-
- $buttons = $this->buildActionButtons($value, $isAdmin, $isOwner);
-
- $row = $this->buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons);
-
- if (!empty($row)) {
- $result['data'][] = $row;
- }
+ $row = $this->buildDataRow($value, $product, $isAdmin, $isCommerciale, $isCaissier, $buttons);
+
+ if (!empty($row)) {
+ $result['data'][] = $row;
}
-
- return $this->response->setJSON($result);
}
- public function fetchAvanceData()
- {
- // Avances incomplètes (reste à payer > 0)
- return $this->fetchAvanceDataGeneric('getIncompleteAvances');
- }
-
- public function fetchAvanceBecameOrder()
- {
- // Avances complètes (reste à payer = 0)
- return $this->fetchAvanceDataGeneric('getCompletedAvances');
- }
-
+ 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');
+}
- public function fetcheExpiredAvance()
- {
- return $this->fetchAvanceDataGeneric('getAllAvanceData2');
- }
/**
* Méthode pour vérifier et envoyer des emails d'alerte 3 jours avant deadline
@@ -441,7 +454,6 @@ class AvanceController extends AdminController
// ✅ Ajouter le produit selon le type
if ($type_avance === 'mere') {
$data['product_name'] = $this->request->getPost('product_name_text');
- // $data['product_id'] = null;
$data['commentaire'] = $this->request->getPost('commentaire');
} else {
$data['product_id'] = (int)$this->request->getPost('id_product');
@@ -456,13 +468,24 @@ class AvanceController extends AdminController
$Products->update($data['product_id'], ['product_sold' => 1]);
}
+ // ✅ NOUVELLE FONCTIONNALITÉ : Envoyer notification au Conseil
$Notification->createNotification(
'Une nouvelle avance a été créée',
- "Conseil",
+ "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 !',
@@ -482,6 +505,7 @@ class AvanceController extends AdminController
]);
}
}
+
public function updateAvance()
{
@@ -502,10 +526,8 @@ class AvanceController extends AdminController
$Products = new Products();
$Notification = new NotificationController();
- // ✅ Récupérer le type AVANT la validation
$type_avance = $this->request->getPost('type_avance_edit');
- // ✅ Validation conditionnelle selon le type
$validation = \Config\Services::validation();
$baseRules = [
@@ -537,7 +559,6 @@ class AvanceController extends AdminController
$avance_id = $this->request->getPost('id');
- // Vérifier si l'avance existe
$existingAvance = $Avance->fetchSingleAvance($avance_id);
if (!$existingAvance) {
return $this->response->setJSON([
@@ -546,18 +567,18 @@ class AvanceController extends AdminController
]);
}
- // Vérifier les permissions (admin ou propriétaire)
$isAdmin = $this->isAdmin($users);
$isOwner = $users['id'] === $existingAvance['user_id'];
+ $isCaissier = $this->isCaissier($users);
- if (!$isAdmin && !$isOwner) {
+ // ✅ 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'
]);
}
- // Recalculer la deadline si le type d'avance a changé
$current_deadline = $existingAvance['deadline'];
if ($type_avance !== $existingAvance['type_avance']) {
@@ -570,7 +591,6 @@ class AvanceController extends AdminController
$old_product_id = $existingAvance['product_id'];
- // Préparer les données communes
$data = [
'type_avance' => $type_avance,
'type_payment' => $this->request->getPost('type_payment_edit'),
@@ -581,13 +601,12 @@ class AvanceController extends AdminController
'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'),
+ 'amount_due' => (float)$this->request->getPost('amount_due_edit')
];
-
- // ✅ Gérer le produit selon le type
+
if ($type_avance === 'mere') {
$data['product_name'] = $this->request->getPost('product_name_text_edit');
- $data['product_id'] = null; // ⚠️ Sera NULL si la colonne l'accepte
+ $data['product_id'] = null;
$data['commentaire'] = $this->request->getPost('commentaire_edit');
$new_product_id = null;
} else {
@@ -597,29 +616,25 @@ class AvanceController extends AdminController
$data['commentaire'] = null;
}
- // Mettre à jour l'avance
if ($Avance->updateAvance($avance_id, $data)) {
- // ✅ Gérer le changement de produit UNIQUEMENT pour "terre"
if ($type_avance === 'terre') {
if ($old_product_id && $old_product_id !== $new_product_id) {
- // Libérer l'ancien produit
$Products->update($old_product_id, ['product_sold' => 0]);
}
if ($new_product_id) {
- // Marquer le nouveau produit comme vendu
$Products->update($new_product_id, ['product_sold' => 1]);
}
} else {
- // Si on passe de "terre" à "mere", libérer l'ancien produit
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',
- "Conseil",
+ "Caissière",
(int)$users['store_id'],
'avances'
);
@@ -635,7 +650,7 @@ class AvanceController extends AdminController
'messages' => 'Erreur lors de la modification de l\'avance'
]);
}
-
+
} catch (\Exception $e) {
log_message('error', "Erreur modification avance: " . $e->getMessage());
return $this->response->setJSON([
@@ -651,6 +666,9 @@ class AvanceController extends AdminController
$this->verifyRole('deleteAvance');
try {
+ $session = session();
+ $users = $session->get('user');
+
$avance_id = $this->request->getPost('avance_id');
$product_id = $this->request->getPost('product_id');
@@ -663,6 +681,27 @@ class AvanceController extends AdminController
$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]);
@@ -715,6 +754,1425 @@ class AvanceController extends AdminController
}
}
+/**
+ * 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
+//
+//
+//
+//
+
+//
+//
+//
+
+//
+//
+//
+//
FACTURE
+//
Date: {$avanceDate}
+//
N°: {$avanceNumber}CI 2025
+//
+//
DOIT ORIGINAL
+//
+
+//
+//
+//
NOM: {$customerName} ({$customerPhone})
+//
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;
+// }
+
+/**
+ * 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;
+ }
+
+
+
+
+
+
+
+
+
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
+
+
+
+
+
+
+
+
+
+
+
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()
+ ]);
+ }
+}
+
}
\ No newline at end of file
diff --git a/app/Controllers/Dashboard.php b/app/Controllers/Dashboard.php
index f7266b7d..8aa0d864 100644
--- a/app/Controllers/Dashboard.php
+++ b/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());
diff --git a/app/Controllers/NotificationController.php b/app/Controllers/NotificationController.php
index b271602b..cd06a01d 100644
--- a/app/Controllers/NotificationController.php
+++ b/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']);
+}
}
diff --git a/app/Controllers/OrderController.php b/app/Controllers/OrderController.php
index b206a79c..1faa286d 100644
--- a/app/Controllers/OrderController.php
+++ b/app/Controllers/OrderController.php
@@ -41,23 +41,25 @@ class OrderController extends AdminController
helper(['url', 'form']);
$Orders = new Orders();
$result = ['data' => []];
-
+
$data = $Orders->getOrdersData();
$session = session();
$users = $session->get('user');
-
+
+ // ========================================
+ // POUR CAISSIÈRE
+ // ========================================
if ($users['group_name'] == "Caissière") {
foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
-
+
$buttons = '';
-
+
// Bouton imprimer (sauf pour SECURITE)
if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
$buttons .= '';
}
-
-
+
// Bouton voir
if (in_array('viewOrder', $this->permission)) {
$buttons .= '
@@ -70,13 +72,15 @@ class OrderController extends AdminController
';
}
-
- // Bouton modifier
- if (in_array('updateOrder', $this->permission) && $users["store_id"] == $value['store_id']) {
+
+ // ✅ Bouton modifier pour statuts 0 (Refusé) et 2 (En Attente)
+ if (in_array('updateOrder', $this->permission)
+ && $users["store_id"] == $value['store_id']
+ && in_array($value['paid_status'], [0, 2])) {
$buttons .= ' ';
}
-
- // Statut de paiement
+
+ // Statut de paiement
if ($value['paid_status'] == 1) {
$paid_status = 'Validé';
} elseif ($value['paid_status'] == 2) {
@@ -86,13 +90,13 @@ class OrderController extends AdminController
} else {
$paid_status = 'Refusé';
}
-
- // Calcul délai (SANS notification ici)
+
+ // Calcul délai
$date1 = new DateTime($date_time);
$date2 = new DateTime();
$interval = $date1->diff($date2);
$daysPassed = $interval->days;
-
+
$statuDate = '';
if ($value['paid_status'] == 2) {
if ($daysPassed < 8) {
@@ -103,10 +107,7 @@ class OrderController extends AdminController
$statuDate = ' depuis ' . $daysPassed . ' Jours';
}
}
-
- // $Orders_items = new OrderItems();
- // $sum_order_item = $Orders_items->getSumOrdersItemData($value['id']);
-
+
$result['data'][$key] = [
$value['product_names'],
$value['user_name'],
@@ -119,31 +120,32 @@ class OrderController extends AdminController
}
return $this->response->setJSON($result);
}
-
+
+ // ========================================
+ // POUR DIRECTION OU DAF
+ // ========================================
elseif($users['group_name'] == "Direction" || $users['group_name'] == "DAF"){
foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
-
+
$buttons = '';
-
- // Bouton imprimer (sauf pour SECURITE)
- if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
+
+ // Bouton imprimer
+ if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
$buttons .= '';
}
-
-
- if (in_array('updateOrder', $this->permission)) {
+
+ // ✅ Bouton modifier pour statuts 0 (Refusé) et 2 (En Attente)
+ if (in_array('updateOrder', $this->permission) && in_array($value['paid_status'], [0, 2])) {
$buttons .= ' ';
}
-
- if (in_array('deleteOrder', $this->permission)) {
+
+ // ✅ Bouton supprimer pour statuts 0 et 2
+ if (in_array('deleteOrder', $this->permission) && in_array($value['paid_status'], [0, 2])) {
$buttons .= ' ';
}
-
-
-
-
- // Statut de paiement
+
+ // Statut de paiement
if ($value['paid_status'] == 1) {
$paid_status = 'Validé';
} elseif ($value['paid_status'] == 2) {
@@ -153,13 +155,13 @@ class OrderController extends AdminController
} else {
$paid_status = 'Refusé';
}
-
- // Calcul délai (SANS notification)
+
+ // Calcul délai
$date1 = new DateTime($date_time);
$date2 = new DateTime();
$interval = $date1->diff($date2);
$daysPassed = $interval->days;
-
+
$statuDate = '';
if ($value['paid_status'] == 2) {
if ($daysPassed < 8) {
@@ -170,10 +172,7 @@ class OrderController extends AdminController
$statuDate = ' depuis ' . $daysPassed . ' Jours';
}
}
-
- // $Orders_items= new OrderItems();
- // $sum_order_item = $Orders_items->getSumOrdersItemData($value['id']);
-
+
$result['data'][$key] = [
$value['bill_no'],
$value['customer_name'],
@@ -187,23 +186,29 @@ class OrderController extends AdminController
}
return $this->response->setJSON($result);
}
+
+ // ========================================
+ // POUR LES AUTRES UTILISATEURS (COMMERCIALE, SECURITE, etc.)
+ // ========================================
else {
foreach ($data as $key => $value) {
$date_time = date('d-m-Y h:i a', strtotime($value['date_time']));
-
+
$buttons = '';
-
- // Bouton imprimer (sauf pour SECURITE)
- if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
+
+ // Bouton imprimer
+ if (in_array('viewOrder', $this->permission) && $users['group_name'] != "SECURITE" && $users['group_name'] != "COMMERCIALE") {
$buttons .= '';
}
- if (in_array('updateOrder', $this->permission) && $users["id"] == $value['user_id']) {
-
- $buttons .= ' ';
-
+ // ✅ Bouton modifier pour statuts 0 et 2, ET si c'est l'utilisateur créateur
+ if (in_array('updateOrder', $this->permission)
+ && $users["id"] == $value['user_id']
+ && in_array($value['paid_status'], [0, 2])) {
+ $buttons .= ' ';
}
-
+
+ // Bouton voir
if (in_array('viewOrder', $this->permission)) {
$buttons .= '
';
}
-
- if (in_array('deleteOrder', $this->permission) && $users["id"] == $value['user_id']) {
+
+ // ✅ Bouton supprimer pour statuts 0 et 2, ET si c'est l'utilisateur créateur
+ if (in_array('deleteOrder', $this->permission)
+ && $users["id"] == $value['user_id']
+ && in_array($value['paid_status'], [0, 2])) {
$buttons .= ' ';
}
-
-
- // Statut de paiement
+
+ // Statut de paiement
if ($value['paid_status'] == 1) {
$paid_status = 'Validé';
} elseif ($value['paid_status'] == 2) {
@@ -231,13 +238,13 @@ class OrderController extends AdminController
} else {
$paid_status = 'Refusé';
}
-
- // Calcul délai (SANS notification)
+
+ // Calcul délai
$date1 = new DateTime($date_time);
$date2 = new DateTime();
$interval = $date1->diff($date2);
$daysPassed = $interval->days;
-
+
$statuDate = '';
if ($value['paid_status'] == 2) {
if ($daysPassed < 8) {
@@ -248,10 +255,7 @@ class OrderController extends AdminController
$statuDate = ' depuis ' . $daysPassed . ' Jours';
}
}
-
- // $Orders_items= new OrderItems();
- // $sum_order_item = $Orders_items->getSumOrdersItemData($value['id']);
-
+
$result['data'][$key] = [
$value['product_names'],
$value['user_name'],
@@ -362,7 +366,7 @@ class OrderController extends AdminController
return redirect()->back()
->withInput()
->with('errors', [
- "⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est inférieur au prix minimal autorisé de {$prixMinimalFormatted} Ar."
+ "⚠️ Commande bloquée : Le rabais de {$discountFormatted} Ar pour « {$productData['name']} » est trop élevé."
]);
}
}
@@ -477,10 +481,8 @@ class OrderController extends AdminController
// Redirection selon le rôle
if ($users["group_name"] != "COMMERCIALE") {
$this->checkProductisNull($posts, $users['store_id']);
- return redirect()->to('orders/update/' . $order_id);
- } else {
- return redirect()->to('orders/');
}
+ return redirect()->to('orders/');
} else {
session()->setFlashdata('errors', 'Error occurred!!');
@@ -626,6 +628,22 @@ public function markAsDelivered()
$data['page_title'] = $this->pageTitle;
$validation = \Config\Services::validation();
+ // ✅ NOUVELLE VÉRIFICATION : Bloquer UNIQUEMENT si statut = Validé (1) ou Validé et Livré (3)
+ $Orders = new Orders();
+ $current_order = $Orders->getOrdersData($id);
+
+ if (!$current_order) {
+ session()->setFlashData('errors', 'Commande introuvable.');
+ return redirect()->to('orders/');
+ }
+
+ // ✅ Bloquer UNIQUEMENT les statuts 1 (Validé) et 3 (Validé et Livré)
+ // Le statut 0 (Refusé) et 2 (En Attente) restent modifiables
+ if (in_array($current_order['paid_status'], [1, 3])) {
+ session()->setFlashData('errors', 'Cette commande ne peut plus être modifiée car elle est déjà validée ou livrée.');
+ return redirect()->to('orders/');
+ }
+
// Règles de validation
$validation->setRules([
'product' => 'required'
@@ -635,7 +653,6 @@ public function markAsDelivered()
'product' => $this->request->getPost('product')
];
- $Orders = new Orders();
$Company = new Company();
$Products = new Products();
$OrderItems = new OrderItems();
@@ -646,12 +663,18 @@ public function markAsDelivered()
if ($this->request->getMethod() === 'post' && $validation->run($validationData)) {
- $current_order = $Orders->getOrdersData($id);
+ // ✅ DOUBLE VÉRIFICATION avant l'update
+ $current_order_check = $Orders->getOrdersData($id);
+ if (in_array($current_order_check['paid_status'], [1, 3])) {
+ session()->setFlashData('errors', 'Cette commande ne peut plus être modifiée car elle est déjà validée ou livrée.');
+ return redirect()->to('orders/');
+ }
+
$old_paid_status = $current_order['paid_status'];
// ✅ Statut payé pour COMMERCIALE reste toujours "En attente"
if ($role === 'COMMERCIALE') {
- $paid_status = 2; // ← mettre la valeur correspondant à "En attente" dans votre table
+ $paid_status = 2;
} else {
$paid_status = $this->request->getPost('paid_status');
}
@@ -745,7 +768,7 @@ public function markAsDelivered()
}
session()->setFlashData('success', 'Commande mise à jour avec succès.');
- return redirect()->to('orders/update/' . $id);
+ return redirect()->to('orders/');
} else {
session()->setFlashData('errors', 'Une erreur est survenue lors de la mise à jour.');
return redirect()->to('orders/update/' . $id);
@@ -760,6 +783,9 @@ public function markAsDelivered()
$orders_data = $Orders->getOrdersData($id);
+ // ✅ Ajouter un flag pour désactiver le formulaire UNIQUEMENT pour les statuts 1 et 3
+ $data['is_editable'] = !in_array($orders_data['paid_status'], [1, 3]);
+
// Montant tranches
$orders_data['montant_tranches'] = (!empty($orders_data['discount']) && $orders_data['discount'] > 0)
? $orders_data['discount']
@@ -778,9 +804,8 @@ public function markAsDelivered()
return $this->render_template('orders/edit', $data);
}
-
-
+
public function lookOrder(int $id)
{
$this->verifyRole('viewOrder');
@@ -1546,10 +1571,7 @@ public function print5(int $id)
// Modèles
$Orders = new Orders();
$Company = new Company();
- $Products = new Products();
$OrderItems = new OrderItems();
- $Brand = new Brands();
- $Category = new Category();
// Récupération des données
$order = $Orders->getOrdersData($id);
@@ -1557,17 +1579,23 @@ public function print5(int $id)
$company = $Company->getCompanyData(1);
$today = date('d/m/Y');
- // ✅ LOGIQUE DE REMISE: Si discount existe, il devient le prix de vente
+ // ✅ Vérifier si c'est une avance "sur mer"
+ $isAvanceMere = false;
+ foreach ($items as $item) {
+ if (!empty($item['product_name']) && empty($item['product_id'])) {
+ $isAvanceMere = true;
+ break;
+ }
+ }
+
+ // Calculs
$discount = (float) $order['discount'];
$grossAmount = (float) $order['gross_amount'];
-
- // Si remise existe, elle devient le montant TTC, sinon on prend gross_amount
$totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalHT = $totalTTC / 1.20;
$tva = $totalTTC - $totalHT;
$inWords = $this->numberToWords((int) round($totalTTC));
- // Statut paiement
$paidLabel = $order['paid_status'] == 1 ? 'Payé' : 'Non payé';
// Début du HTML
@@ -1612,13 +1640,54 @@ public function print5(int $id)
CIN : '.esc($order['customer_cin']).'
Téléphone : '.esc($order['customer_phone'] ?? '').'
Antananarivo, le '.$today.'
-
+';
+
+ // ✅ TABLEAU ADAPTÉ SELON LE TYPE
+ if ($isAvanceMere) {
+ // ========================================
+ // TABLE SIMPLIFIÉE POUR AVANCE "SUR MER"
+ // ========================================
+ $html .= '
+
+
+
+ | Produit |
+ Prix (Ar) |
+
+
+ ';
+ foreach ($items as $it) {
+ $details = $this->getOrderItemDetails($it);
+
+ if (!$details) continue;
+
+ $prixAffiche = ($discount > 0) ? $discount : $details['prix'];
+
+ $html .= '
+
+ '.esc($details['product_name']);
+
+ // Afficher le commentaire s'il existe
+ if (!empty($details['commentaire'])) {
+ $html .= ' '.esc($details['commentaire']).'';
+ }
+
+ $html .= ' |
+ '.number_format($prixAffiche, 0, '', ' ').' |
+
';
+ }
+
+ } else {
+ // ========================================
+ // TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE
+ // ========================================
+ $html .= '
- | Désignation |
MARQUE |
+ Désignation |
N° Moteur |
N° Châssis |
Puissance (CC) |
@@ -1627,33 +1696,26 @@ public function print5(int $id)
';
-foreach ($items as $it) {
- $p = $Products->getProductData($it['product_id']);
-
- // ✅ Récupérer le nom de la marque correctement
- $brandName = 'Non définie';
- if (!empty($p['marque'])) {
- $brandData = $Brand->find($p['marque']);
- if ($brandData && isset($brandData['name'])) {
- $brandName = $brandData['name'];
- }
- }
-
- // ✅ LOGIQUE: Si discount existe pour la commande, on l'affiche comme prix
- $prixAffiche = ($discount > 0) ? $discount : $p['prix_vente'];
-
- $html .= '
+ foreach ($items as $it) {
+ $details = $this->getOrderItemDetails($it);
+
+ if (!$details) continue;
+
+ $prixAffiche = ($discount > 0) ? $discount : $details['prix'];
+
+ $html .= '
- | '.esc($p['name']).' |
- '.esc($brandName).' |
- '.esc($p['numero_de_moteur']).' |
- '.esc($p['chasis'] ?? '').' |
- '.esc($p['puissance']).' |
+ '.esc($details['marque']).' |
+ '.esc($details['product_name']).' |
+ '.esc($details['numero_moteur']).' |
+ '.esc($details['numero_chassis']).' |
+ '.esc($details['puissance']).' |
'.number_format($prixAffiche, 0, '', ' ').' |
';
-}
+ }
+ }
-$html .= '
+ $html .= '
@@ -1704,7 +1766,6 @@ $html .= '
return $this->response->setBody($html);
}
-
/**
* Convertit un nombre entier en texte (français, sans décimales).
* Usage basique, pour Ariary.
@@ -1773,24 +1834,100 @@ private function numberToWords(int $num): string
return trim($words) . ' ariary';
}
-// ====================================
-// PRINT7 - Bon de commande
-// ====================================
+/**
+ * ✅ NOUVELLE MÉTHODE : Vérifier si une commande provient d'une avance "sur mer"
+ */
+private function isFromAvanceMere($order_id)
+{
+ $db = \Config\Database::connect();
+
+ // Vérifier s'il existe une avance "sur mer" liée à cette commande
+ $avance = $db->table('avances')
+ ->select('type_avance, product_name')
+ ->where('is_order', 1)
+ ->where('customer_name',
+ $db->table('orders')
+ ->select('customer_name')
+ ->where('id', $order_id)
+ ->get()
+ ->getRow()->customer_name ?? ''
+ )
+ ->where('type_avance', 'mere')
+ ->get()
+ ->getRowArray();
+
+ return $avance !== null;
+}
+
+/**
+ * ✅ NOUVELLE MÉTHODE : Récupérer les détails d'un item de commande
+ * Gère à la fois les produits normaux et ceux des avances "sur mer"
+ */
+private function getOrderItemDetails($item)
+{
+ $Products = new Products();
+ $Brand = new Brands();
+
+ // Si l'item a un product_name (avance sur mer), on l'utilise directement
+ if (!empty($item['product_name']) && empty($item['product_id'])) {
+ return [
+ 'type' => 'mere',
+ 'product_name' => $item['product_name'],
+ 'marque' => $item['marque'] ?? $item['product_name'],
+ 'numero_moteur' => $item['numero_moteur'] ?? '',
+ 'numero_chassis' => $item['numero_chassis'] ?? '',
+ 'puissance' => $item['puissance'] ?? '',
+ 'commentaire' => $item['commentaire'] ?? '',
+ 'prix' => (float)$item['amount']
+ ];
+ }
+
+ // Sinon, récupérer depuis la table products (avance sur terre ou commande normale)
+ if (empty($item['product_id'])) {
+ return null;
+ }
+
+ $product = $Products->getProductData($item['product_id']);
+
+ if (!$product) {
+ return null;
+ }
+
+ // Récupérer le nom de la marque
+ $brandName = 'N/A';
+ if (!empty($product['marque'])) {
+ $brandData = $Brand->find($product['marque']);
+ if ($brandData && isset($brandData['name'])) {
+ $brandName = $brandData['name'];
+ }
+ }
+
+ return [
+ 'type' => 'terre',
+ 'product_name' => $product['name'],
+ 'marque' => $brandName,
+ 'numero_moteur' => $product['numero_de_moteur'] ?? '',
+ 'numero_chassis' => $product['chasis'] ?? $product['sku'] ?? '',
+ 'puissance' => $product['puissance'] ?? '',
+ 'commentaire' => '',
+ 'prix' => (float)$item['amount']
+ ];
+}
+/**
+ * ✅ PRINT7 - Bon de commande adapté pour avances "sur mer" et "sur terre"
+ */
public function print7(int $id)
{
$this->verifyRole('viewOrder');
- if (! $id) {
+ if (!$id) {
throw new \CodeIgniter\Exceptions\PageNotFoundException();
}
// Modèles
$Orders = new Orders();
$Company = new Company();
- $Products = new Products();
$OrderItems = new OrderItems();
- $Brand = new Brands();
- $Category = new Category();
// Récupération des données
$order = $Orders->getOrdersData($id);
@@ -1798,8 +1935,19 @@ public function print7(int $id)
$company = $Company->getCompanyData(1);
$today = date('d/m/Y');
- // Calculs totaux
- $totalTTC = (float) $order['net_amount'];
+ // ✅ VÉRIFIER SI C'EST UNE AVANCE "SUR MER"
+ $isAvanceMere = false;
+ foreach ($items as $item) {
+ if (!empty($item['product_name']) && empty($item['product_id'])) {
+ $isAvanceMere = true;
+ break;
+ }
+ }
+
+ // ✅ LOGIQUE DE REMISE
+ $discount = (float) $order['discount'];
+ $grossAmount = (float) $order['gross_amount'];
+ $totalTTC = ($discount > 0) ? $discount : $grossAmount;
$totalHT = $totalTTC / 1.20;
$tva = $totalTTC - $totalHT;
@@ -1827,6 +1975,7 @@ public function print7(int $id)
.conditions { page-break-before: always; padding:20px; line-height:1.5; }
@media print {
@page { margin:1cm; }
+ * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
@@ -1851,8 +2000,51 @@ public function print7(int $id)
Téléphone : '.esc($order['customer_phone']).'
CIN : '.esc($order['customer_cin']).'
Antananarivo, le '.$today.'
-
+ ';
+
+ // ========================================
+ // ✅ TABLEAU ADAPTÉ SELON LE TYPE
+ // ========================================
+ if ($isAvanceMere) {
+ // --- TABLE SIMPLIFIÉE POUR AVANCE "SUR MER" (2-3 COLONNES) ---
+ $html .= '
+
+
+
+ | Produit |
+ Prix Unitaire (Ar) |
+
+
+ ';
+
+ foreach ($items as $item) {
+ $details = $this->getOrderItemDetails($item);
+
+ if (!$details) continue;
+
+ $prixAffiche = ($discount > 0) ? $discount : $details['prix'];
+
+ $html .= '
+
+ '.esc($details['product_name']);
+
+ // Afficher le commentaire s'il existe
+ if (!empty($details['commentaire'])) {
+ $html .= ' Remarque : '.esc($details['commentaire']).'';
+ }
+
+ $html .= ' |
+ '.number_format($prixAffiche, 0, '', ' ').' |
+
';
+ }
+
+ $html .= '
+
+
';
+ } else {
+ // --- TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE (7 COLONNES) ---
+ $html .= '
@@ -1867,42 +2059,50 @@ public function print7(int $id)
';
- foreach ($items as $item) {
- $p = $Products->getProductData($item['product_id']);
-
- // ✅ Récupérer le nom de la marque correctement
- $brandName = 'Non définie';
- if (!empty($p['marque'])) {
- $brandData = $Brand->find($p['marque']);
- if ($brandData && isset($brandData['name'])) {
- $brandName = $brandData['name'];
- }
- }
-
- // ✅ Récupérer le nom de la catégorie
- $categoryName = 'Non définie';
- if (!empty($p['categorie_id'])) {
- $categoryData = $Category->find($p['categorie_id']);
- if ($categoryData && isset($categoryData['name'])) {
- $categoryName = $categoryData['name'];
+ $Products = new Products();
+ $Brand = new Brands();
+ $Category = new Category();
+
+ foreach ($items as $item) {
+ $details = $this->getOrderItemDetails($item);
+
+ if (!$details) continue;
+
+ $prixAffiche = ($discount > 0) ? $discount : $details['prix'];
+
+ // Récupérer la catégorie si c'est un produit terre
+ $categoryName = 'Non définie';
+ if ($details['type'] === 'terre' && !empty($item['product_id'])) {
+ $p = $Products->getProductData($item['product_id']);
+ if (!empty($p['categorie_id'])) {
+ $categoryData = $Category->find($p['categorie_id']);
+ if ($categoryData && isset($categoryData['name'])) {
+ $categoryName = $categoryData['name'];
+ }
+ }
}
+
+ $html .= '
+
+ | '.esc($details['product_name']).' |
+ '.esc($details['marque']).' |
+ '.esc($categoryName).' |
+ '.esc($details['numero_moteur']).' |
+ '.esc($details['numero_chassis']).' |
+ '.esc($details['puissance']).' |
+ '.number_format($prixAffiche, 0, '', ' ').' |
+
';
}
-
- $html .= '
- | '.esc($p['name']).' |
- '.esc($brandName).' |
- '.esc($categoryName).' |
- '.esc($p['numero_de_moteur']).' |
- '.esc($p['chasis'] ?? '').' |
- '.esc($p['puissance']).' |
- '.number_format($p['prix_vente'], 0, '', ' ').' |
-
';
- }
- $html .= '
+ $html .= '
-
+
';
+ }
+ // ========================================
+ // TABLEAU RÉCAPITULATIF (IDENTIQUE POUR TOUS)
+ // ========================================
+ $html .= '
| Total HT : |
@@ -1921,13 +2121,30 @@ public function print7(int $id)
'.$paidLabel.' |
';
- if (! empty($order['order_payment_mode'])) {
- $html .= '
+ if (!empty($order['order_payment_mode'])) {
+ $html .= '
+
| Mode de paiement : |
'.esc($order['order_payment_mode']).' |
';
}
+ if (!empty($order['tranche_1'])) {
+ $html .= '
+
+ | Tranche 1 : |
+ '.number_format((float)$order['tranche_1'], 0, '', ' ').' Ar |
+
';
+ }
+
+ if (!empty($order['tranche_2'])) {
+ $html .= '
+
+ | Tranche 2 : |
+ '.number_format((float)$order['tranche_2'], 0, '', ' ').' Ar |
+
';
+ }
+
$html .= '
@@ -1959,9 +2176,8 @@ public function print7(int $id)
';
- echo $html;
+ return $this->response->setBody($html);
}
-
// ====================================
// PRINT31 - Facture + Bon de commande (pages séparées)
// ====================================
@@ -1969,7 +2185,7 @@ public function print31(int $id)
{
$this->verifyRole('viewOrder');
- if (! $id) {
+ if (!$id) {
return $this->response->setStatusCode(400, 'ID manquant');
}
@@ -1984,38 +2200,39 @@ public function print31(int $id)
$items = $OrderItems->getOrdersItemData($id);
$company_info = $Company->getCompanyData(1);
+ // ✅ Vérifier si c'est une avance "sur mer"
+ $isAvanceMere = false;
+ foreach ($items as $item) {
+ if (!empty($item['product_name']) && empty($item['product_id'])) {
+ $isAvanceMere = true;
+ break;
+ }
+ }
+
$paid_status = $order_data['paid_status'] === 1
? "Validé"
: "Refusé";
+ // Calculs globaux
+ $discount = (float) $order_data['discount'];
+ $grossAmount = (float) $order_data['gross_amount'];
+ $totalTTC = ($discount > 0) ? $discount : $grossAmount;
+ $totalHT = $totalTTC / 1.20;
+ $tva = $totalTTC - $totalHT;
+
// --- FACTURES : Une par produit ---
foreach ($items as $item) {
- $p = $Products->getProductData($item['product_id']);
- $unitPrice = (float) $item['amount'];
+ // ✅ Utiliser getOrderItemDetails au lieu de getProductData directement
+ $details = $this->getOrderItemDetails($item);
+
+ if (!$details) continue;
+
+ $unitPrice = $details['prix'];
$quantity = isset($item['qty']) ? (int) $item['qty'] : 1;
$subtotal = $unitPrice * $quantity;
- $vatAmount = $subtotal * 0.2;
- $discount = (float) $order_data['discount'];
- $totalNet = $subtotal + $vatAmount - $discount;
- $inWords = $this->numberToWords((int) round($subtotal));
-
- // ✅ Récupérer le nom de la marque
- $brandName = 'Non définie';
- if (!empty($p['marque'])) {
- $brandData = $Brand->find($p['marque']);
- if ($brandData && isset($brandData['name'])) {
- $brandName = $brandData['name'];
- }
- }
- // ✅ Récupérer le nom de la catégorie
- $categoryName = 'Non définie';
- if (!empty($p['categorie_id'])) {
- $categoryData = $Category->find($p['categorie_id']);
- if ($categoryData && isset($categoryData['name'])) {
- $categoryName = $categoryData['name'];
- }
- }
+ // ✅ Pour avance sur mer avec remise, utiliser la remise comme prix
+ $prixAffiche = ($discount > 0 && $isAvanceMere) ? $discount : $unitPrice;
echo '';
echo '';
@@ -2034,9 +2251,11 @@ public function print31(int $id)
.signature { display:flex; justify-content:space-between; margin-top:50px; }
.signature div { text-align:center; }
.page-break { page-break-after: always; }
+ .to-fill { border-bottom: 1px dotted #000; min-width: 150px; display: inline-block; }
@media print {
body { font-size: 14px; }
@page { margin: 1cm; }
+ * { -webkit-print-color-adjust: exact; print-color-adjust: exact; }
}
";
echo '';
@@ -2057,34 +2276,203 @@ public function print31(int $id)
echo '';
echo '';
- echo '
Client : ' . esc($order_data['customer_name']) . '
';
+ echo '
DOIT : ' . esc($order_data['customer_name']) . '
';
echo '
Adresse : ' . esc($order_data['customer_address']) . '
';
echo '
Téléphone : ' . esc($order_data['customer_phone']) . '
';
echo '
CIN : ' . esc($order_data['customer_cin']) . '
';
echo '
';
+ // ✅ TABLEAU ADAPTÉ SELON LE TYPE
+ if ($isAvanceMere) {
+ // TABLE SIMPLIFIÉE POUR AVANCE "SUR MER"
+ echo '';
+ echo '| Désignation | ';
+ echo 'Produit à compléter | ';
+ echo 'Prix (Ar) | ';
+ echo '
';
+ echo '';
+ echo '| ' . esc($details['product_name']) . ' | ';
+ echo ' | ';
+ echo '' . number_format($prixAffiche, 0, '', ' ') . ' | ';
+ echo '
';
+
+ // Afficher commentaire si existant
+ if (!empty($details['commentaire'])) {
+ echo '| Remarque : ' . esc($details['commentaire']) . ' |
';
+ }
+
+ echo '
';
+ } else {
+ // TABLE COMPLÈTE POUR AVANCE "SUR TERRE" OU COMMANDE NORMALE
+ // Récupérer catégorie
+ $categoryName = 'Non définie';
+ if ($details['type'] === 'terre' && !empty($item['product_id'])) {
+ $p = $Products->getProductData($item['product_id']);
+ if (!empty($p['categorie_id'])) {
+ $categoryData = $Category->find($p['categorie_id']);
+ if ($categoryData && isset($categoryData['name'])) {
+ $categoryName = $categoryData['name'];
+ }
+ }
+ }
+
+ echo '';
+ echo '| Nom | Marque | Catégorie | N° Moteur | Châssis | Puissance (CC) | Prix Unit. (Ar) | ';
+ echo '
';
+ echo '';
+ echo '| ' . esc($details['product_name']) . ' | ';
+ echo '' . esc($details['marque']) . ' | ';
+ echo '' . esc($categoryName) . ' | ';
+ echo '' . esc($details['numero_moteur']) . ' | ';
+ echo '' . esc($details['numero_chassis']) . ' | ';
+ echo '' . esc($details['puissance']) . ' | ';
+ echo '' . number_format($prixAffiche, 0, '', ' ') . ' | ';
+ echo '
';
+ echo '
';
+ }
+
+ // Récapitulatif pour cette facture
+ $itemHT = $prixAffiche / 1.20;
+ $itemTVA = $prixAffiche - $itemHT;
+
+ echo '';
+ echo '| Total HT : | ' . number_format($itemHT, 0, '', ' ') . ' Ar |
';
+ echo '| TVA (20%) : | ' . number_format($itemTVA, 0, '', ' ') . ' Ar |
';
+ echo '| Total TTC : | ' . number_format($prixAffiche, 0, '', ' ') . ' Ar |
';
+ echo '| Statut : | ' . $paid_status . ' |
';
+
+ if (!empty($order_data['order_payment_mode'])) {
+ echo '| Mode de paiement : | ' . esc($order_data['order_payment_mode']) . ' |
';
+ }
+
+ if (!empty($order_data['tranche_1'])) {
+ echo '| Tranche 1 : | ' . number_format((float)$order_data['tranche_1'], 0, '', ' ') . ' Ar |
';
+ }
+
+ if (!empty($order_data['tranche_2'])) {
+ echo '| Tranche 2 : | ' . number_format((float)$order_data['tranche_2'], 0, '', ' ') . ' Ar |
';
+ }
+
+ echo '
';
+
+ echo '';
+ echo '
L\'Acheteur
__________________
';
+ echo '
Le Vendeur
__________________
';
+ echo '
';
+
+ echo ''; // fin page-break
+ echo '