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
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()
|
|
]);
|
|
}
|
|
}
|
|
|
|
|
|
}
|