You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

2317 lines
81 KiB

<?php
namespace App\Controllers;
use App\Models\Company;
use App\Models\Orders;
use App\Models\Products;
use App\Models\Avance;
use App\Models\User; // Ajout pour récupérer les emails DAF/Directrice
class AvanceController extends AdminController
{
private $pageTitle = 'Avances';
public function __construct()
{
parent::__construct();
}
public function index()
{
$this->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'], ['SuperAdmin','DAF','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', 'SuperAdmin','DAF']);
$buttons = '';
// ✅ Bouton Voir pour Caissière (toujours visible)
if ($isCaissier && in_array('viewAvance', $this->permission)) {
$buttons .= '<button type="button" class="btn btn-info" onclick="viewFunc(' . $value['avance_id'] . ')" title="Voir et imprimer la facture" data-user-role="caissiere">'
. '<i class="fa fa-eye"></i></button> ';
}
// ✅ Bouton Voir pour Direction (toujours visible, sans impression)
if ($isDirection && in_array('viewAvance', $this->permission) && !$isCaissier) {
$buttons .= '<button type="button" class="btn btn-info" onclick="viewFunc(' . $value['avance_id'] . ')" title="Voir la facture" data-user-role="direction">'
. '<i class="fa fa-eye"></i></button> ';
}
// ✅ MODIFIÉ : Bouton Modifier - Le caissier peut maintenant modifier toutes les avances
if (in_array('updateAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) {
$buttons .= '<button type="button" class="btn btn-default" onclick="editFunc(' . $value['avance_id'] . ')" title="Modifier">'
. '<i class="fa fa-pencil"></i></button> ';
}
// ✅ MODIFIÉ : Bouton Supprimer - Le caissier peut maintenant supprimer toutes les avances
if (in_array('deleteAvance', $this->permission) && ($isAdmin || $isOwner || $isCaissier)) {
$buttons .= '<button type="button" class="btn btn-danger" onclick="removeFunc(' . $value['avance_id'] . ',' . $value['product_id'] . ')" title="Supprimer">'
. '<i class="fa fa-trash"></i></button> ';
}
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 "
<html>
<head>
<style>
.container { font-family: Arial, sans-serif; max-width: 600px; margin: 0 auto; }
.header { background-color: #f8f9fa; padding: 20px; text-align: center; border-radius: 5px; }
.alert { background-color: #fff3cd; border: 1px solid #ffeaa7; padding: 15px; margin: 20px 0; border-radius: 5px; }
.details { background-color: #f8f9fa; padding: 15px; margin: 10px 0; border-radius: 5px; }
.urgent { color: #dc3545; font-weight: bold; }
.footer { margin-top: 30px; padding: 15px; background-color: #e9ecef; border-radius: 5px; font-size: 12px; }
</style>
</head>
<body>
<div class='container'>
<div class='header'>
<h2>⚠️ ALERTE DEADLINE AVANCE</h2>
</div>
<div class='alert'>
<p><strong " . $urgencyClass . ">Une avance arrive à échéance dans {$daysRemaining} jour(s) !</strong></p>
</div>
<div class='details'>
<h3>Détails de l'avance :</h3>
<ul>
<li><strong>ID Avance :</strong> #{$avance['avance_id']}</li>
<li><strong>Type d'avance :</strong> {$typeAvance}</li>
<li><strong>Client :</strong> {$avance['customer_name']}</li>
<li><strong>Téléphone :</strong> {$avance['customer_phone']}</li>
<li><strong>Adresse :</strong> {$avance['customer_address']}</li>
<li><strong>CIN :</strong> {$avance['customer_cin']}</li>
<li><strong>Produit :</strong> {$productName}</li>
<li><strong>Montant restant dû :</strong> <span class='urgent'>{$amountDueFormatted}</span></li>
<li><strong>Date avance :</strong> {$avanceDateFormatted}</li>
<li><strong>Date limite :</strong> <span class='urgent'>{$deadlineFormatted}</span></li>
</ul>
</div>
<div class='alert'>
<p><strong>Action requise :</strong></p>
<p>Veuillez contacter le client pour régulariser le paiement avant l'échéance ou prendre les mesures appropriées.</p>
</div>
<div class='footer'>
<p>Cet email a été généré automatiquement par le système de gestion des avances.</p>
<p>Date d'envoi : " . date('d/m/Y à H:i') . "</p>
</div>
</div>
</body>
</html>
";
}
/**
* 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]);
}
// ✅ MODIFICATION PRINCIPALE : Envoyer notifications à TOUS les stores
$db = \Config\Database::connect();
// Récupérer tous les stores
$storesQuery = $db->table('stores')->select('id')->get();
$allStores = $storesQuery->getResultArray();
$customerName = $this->request->getPost('customer_name_avance');
$avanceAmount = number_format((float)$this->request->getPost('avance_amount'), 0, ',', ' ');
$typeAvanceLabel = $type_avance === 'terre' ? 'SUR TERRE' : 'SUR MER';
$notificationMessage = "Nouvelle avance {$typeAvanceLabel} créée par {$users['firstname']} {$users['lastname']} - Client: {$customerName} - Montant: {$avanceAmount} Ar";
// ✅ Envoyer notification à DAF, Direction et SuperAdmin de TOUS les stores
foreach ($allStores as $store) {
$storeId = (int)$store['id'];
// Notification pour DAF
$Notification->createNotification(
$notificationMessage,
"DAF",
$storeId,
'avances'
);
// Notification pour Direction
$Notification->createNotification(
$notificationMessage,
"Direction",
$storeId,
'avances'
);
// Notification pour SuperAdmin
$Notification->createNotification(
$notificationMessage,
"SuperAdmin",
$storeId,
'avances'
);
}
// ✅ Notification à la Caissière UNIQUEMENT du store concerné (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'
);
}
log_message('info', "✅ Avance {$avance_id} créée - Notifications envoyées à DAF/Direction/SuperAdmin de tous les stores");
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 <<<HTML
// <!DOCTYPE html>
// <html lang="fr">
// <head>
// <meta charset="UTF-8">
// <meta name="viewport" content="width=device-width, initial-scale=1.0">
// <title>Facture Avance - KELY SCOOTERS</title>
// <style>
// @media print {
// .no-print { display: none !important; }
// body { margin: 0; padding: 10px; }
// }
// * { margin: 0; padding: 0; box-sizing: border-box; }
// body {
// font-family: Arial, sans-serif;
// font-size: 12px;
// line-height: 1.4;
// background: #f5f5f5;
// padding: 20px;
// }
// .invoice-container {
// max-width: 800px;
// margin: 0 auto;
// background: white;
// border: 2px solid #000;
// padding: 20px;
// }
// .header {
// text-align: center;
// border-bottom: 2px solid #000;
// padding-bottom: 10px;
// margin-bottom: 15px;
// }
// .header h1 {
// font-size: 24px;
// font-weight: bold;
// margin-bottom: 5px;
// }
// .header-info {
// display: flex;
// justify-content: space-between;
// margin-top: 10px;
// font-size: 11px;
// }
// .header-left, .header-right {
// text-align: left;
// }
// .invoice-title {
// display: flex;
// justify-content: space-between;
// align-items: center;
// margin: 15px 0;
// padding: 10px;
// border: 1px solid #000;
// }
// .invoice-title h2 {
// font-size: 18px;
// font-weight: bold;
// }
// .original-badge {
// background: #000;
// color: #fff;
// padding: 5px 15px;
// font-weight: bold;
// transform: skewX(-10deg);
// }
// .customer-info {
// margin: 15px 0;
// padding: 10px;
// border: 1px solid #000;
// }
// .customer-info div {
// margin: 5px 0;
// }
// .product-table {
// width: 100%;
// border-collapse: collapse;
// margin: 15px 0;
// }
// .product-table th, .product-table td {
// border: 1px solid #000;
// padding: 8px;
// text-align: left;
// }
// .product-table th {
// background: #f0f0f0;
// font-weight: bold;
// }
// .contract-section {
// margin-top: 20px;
// padding: 15px;
// border: 2px solid #000;
// font-size: 10px;
// }
// .contract-section h3 {
// text-align: center;
// font-size: 14px;
// margin-bottom: 10px;
// text-decoration: underline;
// }
// .contract-article {
// margin: 10px 0;
// }
// .contract-article strong {
// text-decoration: underline;
// }
// .signature-section {
// display: flex;
// justify-content: space-between;
// margin-top: 30px;
// padding: 20px 0;
// }
// .signature-box {
// text-align: center;
// width: 45%;
// }
// .signature-box p {
// font-weight: bold;
// margin-bottom: 50px;
// }
// .print-button {
// position: fixed;
// top: 20px;
// right: 20px;
// padding: 10px 20px;
// background: #007bff;
// color: white;
// border: none;
// border-radius: 5px;
// cursor: pointer;
// font-size: 14px;
// box-shadow: 0 2px 5px rgba(0,0,0,0.2);
// z-index: 1000;
// }
// .print-button:hover {
// background: #0056b3;
// }
// </style>
// </head>
// <body>
// <button class="print-button no-print" onclick="window.print()">🖨️ Imprimer</button>
// <div class="invoice-container">
// <!-- Header -->
// <div class="header">
// <h1>KELY SCOOTERS</h1>
// <div class="header-info">
// <div class="header-left">
// <div>NIF: 401 840 5554</div>
// <div>STAT: 46101 11 2024 00317</div>
// </div>
// <div class="header-right">
// <div>Contact: +261 34 27 946 35 / +261 34 07 079 69</div>
// <div>Antsakaviro en face WWF</div>
// </div>
// </div>
// </div>
// <!-- Invoice Title -->
// <div class="invoice-title">
// <div>
// <h2>FACTURE</h2>
// <div>Date: {$avanceDate}</div>
// <div>N°: {$avanceNumber}CI 2025</div>
// </div>
// <div class="original-badge">DOIT ORIGINAL</div>
// </div>
// <!-- Customer Info -->
// <div class="customer-info">
// <div><strong>NOM:</strong> {$customerName} ({$customerPhone})</div>
// <div><strong>PC:</strong> {$grossAmount} Ar</div>
// <div><strong>AVANCE:</strong> {$avanceAmount} Ar</div>
// <div><strong>RAP:</strong> {$amountDue} Ar</div>
// </div>
// <!-- Product Table -->
// <table class="product-table">
// <thead>
// <tr>
// <th>MARQUE</th>
// <th>N°MOTEUR</th>
// <th>PUISSANCE</th>
// <th>RAP (Ariary)</th>
// </tr>
// </thead>
// <tbody>
// <tr>
// <td>{$marque}</td>
// <td>{$numeroMoteur}</td>
// <td>{$puissance}</td>
// <td>{$amountDue}</td>
// </tr>
// </tbody>
// </table>
// <!-- Contract Section -->
// <div class="contract-section">
// <h3>FIFANEKENA ARA-BAROTRA (Réservations)</h3>
// <p><strong>Ry mpanjifa hajaina,</strong></p>
// <p>Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.</p>
// <div class="contract-article">
// <strong>Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA</strong>
// <p>Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).</p>
// </div>
// <div class="contract-article">
// <strong>Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE)</strong>
// <p>Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.</p>
// <p>Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.</p>
// </div>
// <div class="contract-article">
// <strong>Andininy faha-3: FAMERENANA VOLA</strong>
// <p>Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.</p>
// </div>
// <div class="contract-article">
// <strong>Andininy faha-4: FEPETRA FANAMPINY</strong>
// <ul style="margin-left: 20px;">
// <li>Tsy misafidy raha toa ka mamafa no ifanarahana.</li>
// <li>Tsy azo atao ny mamerina ny entana efa nofandrahana.</li>
// <li>Tsy azo atao ny manakalo ny entana efa nofandrahana.</li>
// </ul>
// </div>
// </div>
// <!-- Signatures -->
// <div class="signature-section">
// <div class="signature-box">
// <p>NY MPAMANDRIKA</p>
// <div style="border-top: 1px solid #000; padding-top: 5px;">Signature</div>
// </div>
// <div class="signature-box">
// <p>NY MPIVAROTRA</p>
// <div style="border-top: 1px solid #000; padding-top: 5px;">
// <strong>KELY SCOOTERS</strong><br>
// NIF: 401 840 5554
// </div>
// </div>
// </div>
// </div>
// <script>
// // Fonction pour imprimer automatiquement (optionnel)
// // window.onload = function() { window.print(); }
// // Fermer la fenêtre après impression (optionnel)
// window.onafterprint = function() {
// // window.close();
// }
// </script>
// </body>
// </html>
// 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', 'SuperAdmin','DAF']);
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 <<<HTML
<style>
.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;
}
</style>
<div class="simplified-invoice">
<!-- Header -->
<div class="simplified-header">
<div class="company-info">
<h3>KELY SCOOTERS</h3>
<p><strong>NIF:</strong> 401 840 5554</p>
<p><strong>STAT:</strong> 46101 11 2024 00317</p>
<p><strong>Contact:</strong> +261 34 27 946 35 / +261 34 07 079 69</p>
<p>Antsakaviro en face WWF</p>
</div>
<div class="invoice-info">
<h2>FACTURE</h2>
<p><strong>Date:</strong> {$avanceDate}</p>
<p><strong>N°:</strong> {$avanceNumber}</p>
<div class="doit-badge">DOIT ORIGINAL</div>
</div>
</div>
<!-- Customer Info -->
<div class="customer-section">
<h4>INFORMATIONS CLIENT</h4>
<div class="customer-details">
<div><strong>NOM:</strong> {$customerName}</div>
<div><strong>Téléphone:</strong> {$customerPhone}</div>
<div><strong>CIN:</strong> {$customerCin}</div>
</div>
</div>
<!-- Amounts -->
<div class="amounts-box">
<div class="amount-row">
<strong>PC (Prix Total):</strong>
<span>{$grossAmount} Ar</span>
</div>
<div class="amount-row">
<strong>AVANCE:</strong>
<span>{$avanceAmount} Ar</span>
</div>
<div class="amount-row">
<strong>RAP (Reste à payer):</strong>
<span>{$amountDue} Ar</span>
</div>
</div>
<!-- Product Table -->
<table class="product-table">
<thead>
<tr>
<th>MARQUE</th>
<th>N°MOTEUR</th>
<th>PUISSANCE</th>
<th>RAP (Ariary)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{$marque}</td>
<td>{$productDetails['numero_moteur']}</td>
<td>{$productDetails['puissance']}</td>
<td><strong>{$amountDue}</strong></td>
</tr>
</tbody>
</table>
</div>
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'
);
$Notification->createNotification(
"Il y a une avance N°{$avanceNumber} pour le client {$customerName}",
"SuperAdmin",
(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 <<<HTML
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Facture Avance - KELY SCOOTERS</title>
<style>
@media print {
body { margin: 0; padding: 0; }
.no-print { display: none !important; }
.page { page-break-after: always; }
.page:last-child { page-break-after: auto; }
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: Arial, sans-serif;
font-size: 12px;
line-height: 1.4;
background: #fff;
}
.page {
width: 210mm;
height: 297mm;
padding: 15mm;
margin: 0 auto;
background: white;
}
.invoice-container {
border: 2px solid #000;
padding: 20px;
height: 100%;
display: flex;
flex-direction: column;
}
.header {
text-align: center;
border-bottom: 2px solid #000;
padding-bottom: 10px;
margin-bottom: 15px;
}
.header h1 {
font-size: 24px;
font-weight: bold;
margin-bottom: 5px;
}
.header-info {
display: flex;
justify-content: space-between;
margin-top: 10px;
font-size: 11px;
}
.header-left, .header-right {
text-align: left;
}
.invoice-title {
display: flex;
justify-content: space-between;
align-items: center;
margin: 15px 0;
padding: 10px;
border: 1px solid #000;
}
.invoice-title h2 {
font-size: 18px;
font-weight: bold;
}
.original-badge {
background: #000;
color: #fff;
padding: 5px 15px;
font-weight: bold;
transform: skewX(-10deg);
}
.customer-info {
margin: 15px 0;
padding: 10px;
border: 1px solid #000;
}
.customer-info div {
margin: 5px 0;
}
.product-table {
width: 100%;
border-collapse: collapse;
margin: 15px 0;
}
.product-table th, .product-table td {
border: 1px solid #000;
padding: 8px;
text-align: left;
}
.product-table th {
background: #f0f0f0;
font-weight: bold;
}
.signature-section {
display: flex;
justify-content: space-between;
margin-top: auto;
padding: 20px 0;
}
.signature-box {
text-align: center;
width: 45%;
}
.signature-box p {
font-weight: bold;
margin-bottom: 50px;
}
/* Style spécifique pour le verso */
.contract-section {
margin-top: 20px;
padding: 15px;
border: 2px solid #000;
font-size: 10px;
}
.contract-section h3 {
text-align: center;
font-size: 14px;
margin-bottom: 10px;
text-decoration: underline;
}
.contract-article {
margin: 10px 0;
}
.contract-article strong {
text-decoration: underline;
}
.verso-content {
flex: 1;
display: flex;
flex-direction: column;
}
</style>
</head>
<body>
<!-- RECTO -->
<div class="page">
<div class="invoice-container">
<!-- Header -->
<div class="header">
<h1>KELY SCOOTERS</h1>
<div class="header-info">
<div class="header-left">
<div>NIF: 401 840 5554</div>
<div>STAT: 46101 11 2024 00317</div>
</div>
<div class="header-right">
<div>Contact: +261 34 27 946 35 / +261 34 07 079 69</div>
<div>Antsakaviro en face WWF</div>
</div>
</div>
</div>
<!-- Invoice Title -->
<div class="invoice-title">
<div>
<h2>FACTURE</h2>
<div>Date: {$avanceDate}</div>
<div>N°: {$avanceNumber}CI 2025</div>
</div>
<div class="original-badge">DOIT ORIGINAL</div>
</div>
<!-- Customer Info -->
<div class="customer-info">
<div><strong>NOM:</strong> {$customerName} ({$customerPhone})</div>
<div><strong>CIN:</strong> {$customerCin}</div>
<div><strong>PC:</strong> {$grossAmount} Ar</div>
<div><strong>AVANCE:</strong> {$avanceAmount} Ar</div>
<div><strong>RAP:</strong> {$amountDue} Ar</div>
</div>
<!-- Product Table -->
<table class="product-table">
<thead>
<tr>
<th>MARQUE</th>
<th>N°MOTEUR</th>
<th>PUISSANCE</th>
<th>RAP (Ariary)</th>
</tr>
</thead>
<tbody>
<tr>
<td>{$marque}</td>
<td>{$numeroMoteur}</td>
<td>{$puissance}</td>
<td>{$amountDue}</td>
</tr>
</tbody>
</table>
</div>
</div>
<!-- VERSO -->
<div class="page">
<div class="invoice-container">
<div class="invoice-title">
<div>
<h2>CONDITIONS GÉNÉRALES</h2>
<div>Date: {$avanceDate}</div>
<div>N°: {$avanceNumber}CI 2025</div>
<div class="verso-content">
<!-- Contract Section -->
<div class="contract-section">
<h3>FIFANEKENA ARA-BAROTRA (Réservations)</h3>
<p><strong>Ry mpanjifa hajaina,</strong></p>
<p>Natao ity fifanekena ity mba hialana amin'ny fivadihana hampitokisana amin'ny andaniny sy ankilany.</p>
<div class="contract-article">
<strong>Andininy faha-1: FAMANDRAHANA SY FANDOAVAM-BOLA</strong>
<p>Ny mpividy dia manao famandrahana amin'ny alalan'ny fandoavambola mihoatra ny 25 isan-jato amin'ny vidin'entana rehetra (avances).</p>
</div>
<div class="contract-article">
<strong>Andininy faha-2: FANDOAVAM-BOLA REHEFA TONGA NY ENTANA (ARRIVAGE)</strong>
<p>Rehefa tonga ny moto/pieces dia tsy maintsy mandoa ny 50 isan-jato ny vidin'entana ny mpamandrika.</p>
<p>Manana 15 andro kosa adoavana ny 25 isan-jato raha misy tsy fahafahana alohan'ny famoahana ny entana.</p>
</div>
<div class="contract-article">
<strong>Andininy faha-3: FAMERENANA VOLA</strong>
<p>Raha toa ka misy antony tsy hakana ny entana indray dia tsy mamerina ny vola efa voaloha (avance) ny société.</p>
</div>
<div class="contract-article">
<strong>Andininy faha-4: FEPETRA FANAMPINY</strong>
<ul style="margin-left: 20px;">
<li>Tsy misafidy raha toa ka mamafa no ifanarahana.</li>
<li>Tsy azo atao ny mamerina ny entana efa nofandrahana.</li>
<li>Tsy azo atao ny manakalo ny entana efa nofandrahana.</li>
</ul>
</div>
</div>
<!-- Additional space for notes -->
<div style="margin-top: 20px; padding: 10px; border: 1px solid #000; flex: 1;">
<strong>OBSERVATIONS / NOTES:</strong>
<div style="height: 100px; margin-top: 10px;"></div>
</div>
<!-- Signatures for verso -->
<div class="signature-section">
<div class="signature-box">
<p>NY MPAMANDRIKA</p>
<div style="border-top: 1px solid #000; padding-top: 5px;">Signature</div>
</div>
<div class="signature-box">
<p>NY MPIVAROTRA</p>
<div style="border-top: 1px solid #000; padding-top: 5px;">
<strong>KELY SCOOTERS</strong><br>
NIF: 401 840 5554
</div>
</div>
</div>
</div>
</div>
</div>
</body>
</html>
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');
if (!$avance_id || $montant_paye <= 0) {
return $this->response->setJSON([
'success' => false,
'message' => 'Données invalides'
]);
}
$avanceModel = new \App\Models\Avance();
$avance = $avanceModel->find($avance_id);
if (!$avance) {
return $this->response->setJSON([
'success' => false,
'message' => 'Avance introuvable'
]);
}
// ✅ Vérifier si déjà convertie
if ($avance['is_order'] == 1) {
return $this->response->setJSON([
'success' => false,
'message' => '⚠️ Cette avance a déjà été convertie en commande'
]);
}
// ✅ Calcul nouveau montant dû
$amount_due = max(0, (float)$avance['amount_due'] - $montant_paye);
// ✅ Mise à jour avance
$avanceModel->update($avance_id, [
'avance_amount' => (float)$avance['avance_amount'] + $montant_paye,
'amount_due' => $amount_due,
]);
log_message('info', "💰 Paiement {$montant_paye} Ar sur avance {$avance_id} (Type: {$avance['type_avance']})");
// ✅ CONVERSION si paiement complet
if ($amount_due <= 0) {
if ($avance['type_avance'] === 'terre') {
log_message('info', "🔄 Avance TERRE {$avance_id} complétée → Conversion en commande...");
$order_id = $avanceModel->convertToOrder($avance_id);
if ($order_id) {
log_message('info', "✅ Conversion réussie → Commande {$order_id}");
return $this->response->setJSON([
'success' => true,
'message' => '✅ Paiement effectué ! L\'avance 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 {$avance_id}");
return $this->response->setJSON([
'success' => false,
'message' => '⚠️ Paiement enregistré mais erreur lors de la conversion. Contactez l\'administrateur.'
]);
}
} else {
// ✅ Avance MER complète
log_message('info', "✅ Avance MER {$avance_id} complétée (pas de conversion)");
return $this->response->setJSON([
'success' => true,
'message' => '✅ Paiement effectué ! L\'avance MER est maintenant complète.',
'converted' => false,
'type' => 'mere'
]);
}
}
// ✅ Paiement partiel
return $this->response->setJSON([
'success' => true,
'message' => '✅ Paiement partiel enregistré',
'amount_due_remaining' => $amount_due,
'amount_due_formatted' => number_format($amount_due, 0, ',', ' ') . ' Ar',
'type' => $avance['type_avance']
]);
}
/**
* ✅ Conversion manuelle (optionnel - pour forcer la conversion)
*/
public function forceConvertToOrder($avance_id)
{
$this->verifyRole('updateAvance');
$avanceModel = new \App\Models\Avance();
$avance = $avanceModel->find($avance_id);
if (!$avance) {
session()->setFlashdata('errors', 'Avance introuvable');
return redirect()->back();
}
if ($avance['type_avance'] !== 'terre') {
session()->setFlashdata('errors', 'Seules les avances TERRE peuvent être converties');
return redirect()->back();
}
if ($avance['amount_due'] > 0) {
session()->setFlashdata('errors', 'L\'avance doit être complètement payée avant conversion');
return redirect()->back();
}
$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');
return redirect()->back();
}
}
/**
* ✅ Vérifier et convertir toutes les avances complètes
* URL: /avances/checkAndConvertCompleted
*/
public function checkAndConvertCompleted()
{
try {
log_message('info', "=== DÉBUT checkAndConvertCompleted (manuel) ===");
$Avance = new \App\Models\Avance();
// ✅ Récupérer toutes les avances TERRE complètes non converties
$completedTerreAvances = $Avance
->where('type_avance', 'terre')
->where('amount_due <=', 0)
->where('is_order', 0)
->where('active', 1)
->findAll();
if (empty($completedTerreAvances)) {
return $this->response->setJSON([
'success' => true,
'message' => 'Aucune avance complète à convertir',
'converted' => 0
]);
}
$convertedCount = 0;
$errorCount = 0;
$details = [];
foreach ($completedTerreAvances as $avance) {
log_message('info', "🔍 Traitement avance {$avance['avance_id']}...");
$order_id = $Avance->convertToOrder($avance['avance_id']);
if ($order_id) {
$convertedCount++;
$details[] = [
'avance_id' => $avance['avance_id'],
'customer' => $avance['customer_name'],
'order_id' => $order_id,
'status' => 'success'
];
log_message('info', "✅ Avance {$avance['avance_id']} → Commande {$order_id}");
} else {
$errorCount++;
$details[] = [
'avance_id' => $avance['avance_id'],
'customer' => $avance['customer_name'],
'status' => 'error',
'reason' => 'Voir logs pour détails'
];
log_message('error', "❌ Échec conversion avance {$avance['avance_id']}");
}
}
log_message('info', "=== FIN checkAndConvertCompleted - Convertis: {$convertedCount}, Erreurs: {$errorCount} ===");
return $this->response->setJSON([
'success' => true,
'message' => 'Vérification terminée',
'converted' => $convertedCount,
'errors' => $errorCount,
'total_checked' => count($completedTerreAvances),
'details' => $details
]);
} catch (\Exception $e) {
log_message('error', "❌ Erreur checkAndConvertCompleted: " . $e->getMessage());
return $this->response->setJSON([
'success' => false,
'messages' => 'Erreur: ' . $e->getMessage()
]);
}
}
}