<?php
namespace App\Controller;
use App\Entity\Bailleur;
use App\Entity\Catalogue;
use App\Entity\CentreCout;
use App\Entity\DemAchatTemp;
use App\Entity\NumDA;
use App\Entity\RapportageDA;
use App\Entity\demAchat;
use App\Service\ContratCadreService;
use App\Service\TranslationWriter;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
use Symfony\Component\Security\Core\Security;
use Symfony\Component\HttpFoundation\Response;
/**
* Contrôleur API JSON pour le mode offline des Demandes d'Achat.
*
* Ces endpoints sont des versions JSON des routes existantes.
* Ils sont appelés par da-offline.js en mode online,
* et par le Service Worker lors de la synchronisation offline.
*
* Routes à ajouter dans config/routes.yaml (ou annotations) :
* api_da_ajouter_temp : POST /api/da/ajouter-temp
* api_da_ajout_design : POST /api/da/ajout-design-temp
* api_da_envoyer : POST /api/da/envoyer
* api_da_import_excel : POST /api/da/import-excel
* api_da_liste_temp : GET /api/da/liste-temp/{num}
*/
class ApiDaController extends AbstractController
{
public function __construct(
private EntityManagerInterface $em,
private Security $security,
private ContratCadreService $contratCadreService,
private TranslationWriter $translationWriter
) {}
// =========================================================
// POST /api/da/ajouter-temp
// Équivalent JSON de ajouter_achat_temp
// Crée le premier article du brouillon (nouveau numDemandeAchat)
// =========================================================
public function apiAjouterTemp(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
$data = $this->decodeRequest($request);
// Champs obligatoires
$idCentreCout = $data['centreCout'] ?? null;
$idBailleur = $data['bailleur'] ?? null;
$quantDemand = $data['quantDemand'] ?? null;
$prixUnit = $data['prixUnit'] ?? null;
$obje = $data['obje'] ?? '';
$delais = $data['delais'] ?? null;
$region = $data['region'] ?? null;
$descrip = $data['descrip'] ?? null;
$idDesignation = $data['iddesignrech'] ?? null;
if (!$idCentreCout || !$idBailleur) {
return new JsonResponse(['success' => false, 'message' => 'Centre de coût et projet obligatoires'], 400);
}
// Résolution des entités liées
$cecout = $this->em->getRepository(CentreCout::class)->find($idCentreCout);
$nomprojet = $this->em->getRepository(Bailleur::class)->find($idBailleur);
if (!$cecout || !$nomprojet) {
return new JsonResponse(['success' => false, 'message' => 'Centre de coût ou projet introuvable'], 404);
}
$designrech = $idDesignation
? $this->em->getRepository(Catalogue::class)->findOneByDesignation($idDesignation)
: null;
// Calcul du prochain numDemandeAchat
$num = $this->getNextNumDemande();
// Conversion région
$regionLabel = $this->convertRegion($region);
// Création de l'entité brouillon
$demAch = new DemAchatTemp();
$demAch->setCentreCout($cecout->getLibele());
$demAch->setCostcenter($cecout);
$demAch->setNombaille($nomprojet);
$demAch->setNomProjet($nomprojet->getLibele());
$demAch->setQuantDemande((int)$quantDemand);
$demAch->setObjet($obje);
$demAch->setRegion($regionLabel);
$demAch->setDescription($descrip);
$demAch->setDelais($delais);
$demAch->setDateSoumis(new \DateTime());
$demAch->setUser($user);
$demAch->setNumDemandeAchat($num);
if ($designrech) {
$demAch->setDesignation($designrech->getDesignation());
$demAch->setCatalogue($designrech);
$demAch->setPrixU((float)$prixUnit);
} else {
$demAch->setDesignation('Article non trouvé dans le catalogue');
}
$this->em->persist($demAch);
$this->em->flush();
return new JsonResponse([
'success' => true,
'message' => 'Article ajouté avec succès',
'numDemandeAchat' => $num,
'id' => $demAch->getId(),
]);
}
// =========================================================
// POST /api/da/ajout-design-temp
// Équivalent JSON de ajout_design_da_temp
// Ajoute un article supplémentaire à un brouillon existant
// =========================================================
public function apiAjoutDesignTemp(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
$data = $this->decodeRequest($request);
$idBailleur = $data['bailleur'] ?? null;
$idCentreCout = $data['centreCout'] ?? null;
$quantDemand = $data['quantDemand'] ?? null;
$prixUnit = $data['prixUnit'] ?? null;
$descrip = $data['descrip'] ?? null;
$idDesignation = $data['iddesignrech'] ?? null;
// Récupération du dernier brouillon de cet utilisateur
$demtempuses = $this->em->getRepository(DemAchatTemp::class)->findByUser($user);
$demtemp = end($demtempuses);
if (!$demtemp) {
return new JsonResponse(['success' => false, 'message' => 'Aucun brouillon en cours'], 404);
}
$nomprojet = $this->em->getRepository(Bailleur::class)->find($idBailleur);
$centrecout = $this->em->getRepository(CentreCout::class)->find($idCentreCout);
$designrech = $idDesignation
? $this->em->getRepository(Catalogue::class)->findOneByDesignation($idDesignation)
: null;
$num = $demtemp->getNumDemandeAchat();
$demAch = new DemAchatTemp();
$demAch->setCentreCout($centrecout ? $centrecout->getLibele() : '');
$demAch->setCostcenter($centrecout);
$demAch->setNomProjet($nomprojet ? $nomprojet->getLibele() : '');
$demAch->setNombaille($nomprojet);
$demAch->setRegion($demtemp->getRegion());
$demAch->setQuantDemande((int)$quantDemand);
$demAch->setObjet($demtemp->getObjet());
$demAch->setDescription($descrip);
$demAch->setDelais($demtemp->getDelais());
$demAch->setDateSoumis(new \DateTime());
$demAch->setUser($user);
$demAch->setNumDemandeAchat($num);
$demAch->setNumOrdre(1);
if ($designrech) {
$demAch->setDesignation($designrech->getDesignation());
$demAch->setCatalogue($designrech);
$demAch->setPrixU((float)$prixUnit);
} else {
$demAch->setDesignation('Article non trouvé dans le catalogue');
}
$this->em->persist($demAch);
$this->em->flush();
return new JsonResponse([
'success' => true,
'message' => 'Article ajouté avec succès',
'numDemandeAchat' => $num,
'id' => $demAch->getId(),
]);
}
// =========================================================
// POST /api/da/envoyer
// Équivalent JSON de daenvoyer
// Convertit les DemAchatTemp en demAchat définitifs
// =========================================================
public function apiDaEnvoyer(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
$data = $this->decodeRequest($request);
$numero = $data['num'] ?? null;
if (!$numero) {
return new JsonResponse(['success' => false, 'message' => 'Numéro de brouillon manquant'], 400);
}
$datemps = $this->em->getRepository(DemAchatTemp::class)->findDemandeAchatPerso((int)$numero);
if (empty($datemps)) {
return new JsonResponse(['success' => false, 'message' => 'Aucun brouillon trouvé pour ce numéro'], 404);
}
$numda = $this->em->getRepository(NumDA::class)->find(1);
$num = $numda->getNum() + 1;
$dat = date('Y-m-d H:i:s');
$achat = null;
foreach ($datemps as $datemp) {
$achat = new demAchat();
$raport = new RapportageDA();
$achat->setDesignation($datemp->getDesignation());
$achat->setPrixU($datemp->getPrixU());
$achat->setCentreCout($datemp->getCentreCout());
$achat->setNomProjet($datemp->getNomProjet());
$achat->setNombaille($datemp->getNombaille());
$achat->setCostcenter($datemp->getCostcenter());
$achat->setCatalogue($datemp->getCatalogue());
$achat->setObjet($datemp->getObjet());
$achat->setRegion($datemp->getRegion());
$achat->setDescription($datemp->getDescription());
$achat->setUser($datemp->getUser());
$achat->setQuantDemande($datemp->getQuantDemande());
$achat->setResteACommander($datemp->getQuantDemande());
$achat->setDelais($datemp->getDelais());
$achat->setNumOrdre(1);
$achat->setDateSoumis(new \DateTime($dat));
// Gestion contrat-cadre (même logique que daenvoyer existant)
$montantLigne = (float)$datemp->getPrixU() * (float)$datemp->getQuantDemande();
$catalogue = $datemp->getCatalogue();
$categorieCatalogue = $catalogue ? $catalogue->getCategorycatalogue() : null;
$contratCadre = null;
if ($categorieCatalogue) {
foreach ($categorieCatalogue->getCategoriesMarche() as $categorieMarche) {
$contrats = $this->contratCadreService->listerDisponiblesPourDA(
$categorieMarche->getId(),
$montantLigne
);
if (!empty($contrats)) {
$contratCadre = $contrats[0];
break;
}
}
}
if ($contratCadre) {
$achat->setContratCadre($contratCadre);
$achat->setStatutContratCadree(demAchat::CC_SUGGERE);
$contratCadre->setMontantReserve(
(string)((float)$contratCadre->getMontantReserve() + $montantLigne)
);
$this->em->persist($contratCadre);
}
$numda->setNum($num);
$achat->setNumDemandeAchat($num);
$this->em->persist($achat);
$this->em->remove($datemp);
$raport->setDateSoumis(new \DateTime($dat));
$raport->setDemAchat($achat);
$this->em->persist($raport);
$this->em->flush();
// Traductions
if (!empty($datemp->getDescription()) && trim($datemp->getDescription()) !== '') {
$this->translationWriter->create('demAchat', $achat->getId(), 'description', $achat->getDescription());
}
}
if ($achat) {
$this->em->refresh($achat);
$this->translationWriter->create('demAchat', $achat->getId(), 'objet', $achat->getObjet());
$this->em->flush();
}
return new JsonResponse([
'success' => true,
'message' => 'DA créée avec succès',
'numDemandeAchat' => $num,
]);
}
// =========================================================
// POST /api/da/import-excel
// Traitement du fichier Excel en mode offline
// Le fichier est envoyé en base64 depuis IndexedDB
// =========================================================
public function apiImportExcel(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
// Cas 1 : envoi normal (formulaire online)
$uploadedFile = $request->files->get('fiche');
// Cas 2 : envoi offline (base64 depuis IndexedDB via Service Worker)
if (!$uploadedFile) {
$data = json_decode($request->getContent(), true);
$base64File = $data['fileBase64'] ?? null;
$fileName = $data['fileName'] ?? 'import.xlsx';
if (!$base64File) {
return new JsonResponse(['success' => false, 'message' => 'Aucun fichier reçu'], 400);
}
// Reconstruire un fichier temporaire depuis le base64
$fileContent = base64_decode($base64File);
$tmpPath = sys_get_temp_dir() . '/' . uniqid('da_excel_') . '_' . $fileName;
file_put_contents($tmpPath, $fileContent);
// Créer un SplFileInfo pour le passer au service
$uploadedFile = new \SplFileInfo($tmpPath);
}
try {
// Réutilise le service existant ExcelImportService
// Injectez-le dans le constructeur si nécessaire
// $this->excelImportService->importDemandesAchat($uploadedFile);
// Nettoyage fichier temporaire si besoin
if (isset($tmpPath) && file_exists($tmpPath)) {
unlink($tmpPath);
}
return new JsonResponse([
'success' => true,
'message' => 'Import Excel traité avec succès',
]);
} catch (\Exception $e) {
return new JsonResponse([
'success' => false,
'message' => 'Erreur lors de l\'import : ' . $e->getMessage(),
], 500);
}
}
// =========================================================
// GET /api/da/liste-temp/{num}
// Retourne les lignes DemAchatTemp d'un brouillon (pour UI offline)
// =========================================================
public function apiListeTemp(int $num): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
}
$datemps = $this->em->getRepository(DemAchatTemp::class)->findDemandeAchatPerso($num);
$lignes = [];
foreach ($datemps as $d) {
$lignes[] = [
'id' => $d->getId(),
'designation' => $d->getDesignation(),
'quantDemande' => $d->getQuantDemande(),
'prixU' => $d->getPrixU(),
'description' => $d->getDescription(),
'centreCout' => $d->getCentreCout(),
'nomProjet' => $d->getNomProjet(),
'objet' => $d->getObjet(),
'region' => $d->getRegion(),
'delais' => $d->getDelais(),
'numDemandeAchat' => $d->getNumDemandeAchat(),
];
}
return new JsonResponse([
'success' => true,
'lignes' => $lignes,
'total' => count($lignes),
]);
}
// =========================================================
// Helpers privés
// =========================================================
private function decodeRequest(Request $request): array
{
// Supporte JSON body et form-data
$contentType = $request->headers->get('Content-Type', '');
if (str_contains($contentType, 'application/json')) {
return json_decode($request->getContent(), true) ?? [];
}
return $request->request->all();
}
private function getNextNumDemande(): int
{
$demachas = $this->em->getRepository(DemAchatTemp::class)->findAll();
$num = 1;
foreach ($demachas as $demacha) {
if ($demacha->getNumDemandeAchat() > $num) {
$num = $demacha->getNumDemandeAchat();
}
}
return $num + 1;
}
private function convertRegion(?string $region): string
{
$map = [
'1' => 'Bamako',
'2' => 'Segou',
'3' => 'Mopti',
'4' => 'Sikasso',
'5' => 'Koulikoro',
];
return $map[$region] ?? ($region ?? '');
}
public function checkSession(): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse([
'authenticated' => false,
'message' => 'Session expirée ou non authentifié',
], 401);
}
return new JsonResponse([
'authenticated' => true,
'username' => $user->getNom() . ' ' . $user->getPrenom(),
'userId' => $user->getId(),
]);
}
public function soumettrebrouillonComplet(Request $request): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false, 'message' => 'Session expirée'], 401);
}
$data = json_decode($request->getContent(), true);
if (!$data) {
return new JsonResponse(['success' => false, 'message' => 'Données invalides'], 400);
}
$objet = $data['objet'] ?? '';
$region = $data['region'] ?? '';
$centreCoutId = $data['centreCoutId'] ?? null;
$bailleurId = $data['bailleurId'] ?? null;
$delais = $data['delais'] ?? null;
$articles = $data['articles'] ?? [];
if (empty($articles)) {
return new JsonResponse(['success' => false, 'message' => 'Aucun article'], 400);
}
$cecout = $centreCoutId ? $this->em->getRepository(CentreCout::class)->find($centreCoutId) : null;
$nomprojet = $bailleurId ? $this->em->getRepository(Bailleur::class)->find($bailleurId) : null;
if (!$cecout || !$nomprojet) {
return new JsonResponse(['success' => false, 'message' => 'Centre de coût ou projet introuvable'], 404);
}
// ── Calcul du prochain numéro de brouillon ───────────────────────────
$demachas = $this->em->getRepository(DemAchatTemp::class)->findAll();
$num = 1;
foreach ($demachas as $d) {
if ($d->getNumDemandeAchat() > $num) $num = $d->getNumDemandeAchat();
}
$num++;
// ── Création des DemAchatTemp pour chaque article ───────────────────
foreach ($articles as $article) {
$designation = $article['designation'] ?? '';
$quantite = (int)($article['quantite'] ?? 1);
$prixU = (float)($article['prixU'] ?? 0);
$description = $article['description'] ?? '';
$designrech = $this->em->getRepository(Catalogue::class)
->findOneByDesignation($designation);
$demAch = new DemAchatTemp();
$demAch->setCentreCout($cecout->getLibele());
$demAch->setCostcenter($cecout);
$demAch->setNombaille($nomprojet);
$demAch->setNomProjet($nomprojet->getLibele());
$demAch->setRegion($region);
$demAch->setObjet($objet);
$demAch->setQuantDemande($quantite);
$demAch->setDescription($description);
$demAch->setDelais($delais);
$demAch->setDateSoumis(new \DateTime());
$demAch->setUser($user);
$demAch->setNumDemandeAchat($num);
$demAch->setNumOrdre(1);
if ($designrech) {
$demAch->setDesignation($designrech->getDesignation());
$demAch->setCatalogue($designrech);
$demAch->setPrixU($prixU);
} else {
$demAch->setDesignation($designation . ' (catalogue à vérifier)');
$demAch->setPrixU($prixU);
}
$this->em->persist($demAch);
}
$this->em->flush();
// ── Conversion immédiate en DA définitive (daenvoyer) ────────────────
$datemps = $this->em->getRepository(DemAchatTemp::class)->findDemandeAchatPerso($num);
$numda = $this->em->getRepository(NumDA::class)->find(1);
$numDA = $numda->getNum() + 1;
$dat = date('Y-m-d H:i:s');
$achat = null;
foreach ($datemps as $datemp) {
$achat = new demAchat();
$raport = new RapportageDA();
$achat->setDesignation($datemp->getDesignation());
$achat->setPrixU($datemp->getPrixU());
$achat->setCentreCout($datemp->getCentreCout());
$achat->setNomProjet($datemp->getNomProjet());
$achat->setNombaille($datemp->getNombaille());
$achat->setCostcenter($datemp->getCostcenter());
$achat->setCatalogue($datemp->getCatalogue());
$achat->setObjet($datemp->getObjet());
$achat->setRegion($datemp->getRegion());
$achat->setDescription($datemp->getDescription());
$achat->setUser($datemp->getUser());
$achat->setQuantDemande($datemp->getQuantDemande());
$achat->setResteACommander($datemp->getQuantDemande());
$achat->setDelais($datemp->getDelais());
$achat->setNumOrdre(1);
$achat->setDateSoumis(new \DateTime($dat));
// Gestion contrat-cadre
$montantLigne = (float)$datemp->getPrixU() * (float)$datemp->getQuantDemande();
$catalogue = $datemp->getCatalogue();
$categorieCatalogue = $catalogue ? $catalogue->getCategorycatalogue() : null;
$contratCadre = null;
if ($categorieCatalogue) {
foreach ($categorieCatalogue->getCategoriesMarche() as $categorieMarche) {
$contrats = $this->contratCadreService->listerDisponiblesPourDA(
$categorieMarche->getId(), $montantLigne
);
if (!empty($contrats)) { $contratCadre = $contrats[0]; break; }
}
}
if ($contratCadre) {
$achat->setContratCadre($contratCadre);
$achat->setStatutContratCadree(demAchat::CC_SUGGERE);
$contratCadre->setMontantReserve(
(string)((float)$contratCadre->getMontantReserve() + $montantLigne)
);
$this->em->persist($contratCadre);
}
$numda->setNum($numDA);
$achat->setNumDemandeAchat($numDA);
$this->em->persist($achat);
$this->em->remove($datemp);
$raport->setDateSoumis(new \DateTime($dat));
$raport->setDemAchat($achat);
$this->em->persist($raport);
$this->em->flush();
if (!empty($datemp->getDescription()) && trim($datemp->getDescription()) !== '') {
$this->translationWriter->create(
'demAchat', $achat->getId(), 'description', $achat->getDescription()
);
}
}
if ($achat) {
$this->em->refresh($achat);
$this->translationWriter->create('demAchat', $achat->getId(), 'objet', $achat->getObjet());
$this->em->flush();
}
return new JsonResponse([
'success' => true,
'message' => 'DA créée avec succès',
'numDemandeAchat' => $numDA,
]);
}
// =========================================================
// GET /api/da/references
// Retourne catalogue + centres de coût + bailleurs en JSON
// Appelé par da-offline.js lors de la visite online pour
// mettre en cache ces données dans IndexedDB (da-shell.html)
// =========================================================
public function getReferences(): JsonResponse
{
$user = $this->security->getUser();
if (!$user) {
return new JsonResponse(['success' => false], 401);
}
$catalogues = $this->em->getRepository(Catalogue::class)
->createQueryBuilder('c')
->select('c.id, c.designation') // ← prixUnitaire retiré
->getQuery()
->getArrayResult();
$centres = $this->em->getRepository(CentreCout::class)
->createQueryBuilder('c')
->select('c.id, c.libele')
->getQuery()
->getArrayResult();
$bailleurs = $this->em->getRepository(Bailleur::class)
->createQueryBuilder('b')
->select('b.id, b.libele')
->getQuery()
->getArrayResult();
return new JsonResponse([
'success' => true,
'catalogue' => $catalogues,
'centres' => $centres,
'bailleurs' => $bailleurs,
'cachedAt' => (new \DateTime())->format('Y-m-d H:i:s'),
]);
}
// =========================================================
// GET /da-offline
// Sert da-shell.html depuis Symfony (avec headers corrects)
// Permet d'être mis en cache par le SW même si le fichier
// est dans /public/da-shell.html
// =========================================================
public function daShell(): Response
{
return new Response(
file_get_contents($this->getParameter('kernel.project_dir') . '/public/da-shell.html'),
200,
['Content-Type' => 'text/html; charset=utf-8']
);
}
}