src/Controller/ApiDaController.php line 680

Open in your IDE?
  1. <?php
  2. namespace App\Controller;
  3. use App\Entity\Bailleur;
  4. use App\Entity\Catalogue;
  5. use App\Entity\CentreCout;
  6. use App\Entity\DemAchatTemp;
  7. use App\Entity\NumDA;
  8. use App\Entity\RapportageDA;
  9. use App\Entity\demAchat;
  10. use App\Service\ContratCadreService;
  11. use App\Service\TranslationWriter;
  12. use Doctrine\ORM\EntityManagerInterface;
  13. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  14. use Symfony\Component\HttpFoundation\JsonResponse;
  15. use Symfony\Component\HttpFoundation\Request;
  16. use Symfony\Component\Routing\Annotation\Route;
  17. use Symfony\Component\Security\Core\Security;
  18. use Symfony\Component\HttpFoundation\Response;
  19. use App\Service\DemandeAchatService;
  20. /**
  21. * Contrôleur API JSON pour le mode offline des Demandes d'Achat.
  22. *
  23. * Ces endpoints sont des versions JSON des routes existantes.
  24. * Ils sont appelés par da-offline.js en mode online,
  25. * et par le Service Worker lors de la synchronisation offline.
  26. *
  27. * Routes à ajouter dans config/routes.yaml (ou annotations) :
  28. * api_da_ajouter_temp : POST /api/da/ajouter-temp
  29. * api_da_ajout_design : POST /api/da/ajout-design-temp
  30. * api_da_envoyer : POST /api/da/envoyer
  31. * api_da_import_excel : POST /api/da/import-excel
  32. * api_da_liste_temp : GET /api/da/liste-temp/{num}
  33. */
  34. class ApiDaController extends AbstractController
  35. {
  36. public function __construct(
  37. private EntityManagerInterface $em,
  38. private Security $security,
  39. private ContratCadreService $contratCadreService,
  40. private TranslationWriter $translationWriter,
  41. private DemandeAchatService $demandeAchatService
  42. ) {}
  43. // =========================================================
  44. // POST /api/da/ajouter-temp
  45. // Équivalent JSON de ajouter_achat_temp
  46. // Crée le premier article du brouillon (nouveau numDemandeAchat)
  47. // =========================================================
  48. public function apiAjouterTemp(Request $request): JsonResponse
  49. {
  50. $user = $this->security->getUser();
  51. if (!$user) {
  52. return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
  53. }
  54. $data = $this->decodeRequest($request);
  55. // Champs obligatoires
  56. $idCentreCout = $data['centreCout'] ?? null;
  57. $idBailleur = $data['bailleur'] ?? null;
  58. $quantDemand = $data['quantDemand'] ?? null;
  59. $prixUnit = $data['prixUnit'] ?? null;
  60. $obje = $data['obje'] ?? '';
  61. $delais = $data['delais'] ?? null;
  62. $region = $data['region'] ?? null;
  63. $descrip = $data['descrip'] ?? null;
  64. $idDesignation = $data['iddesignrech'] ?? null;
  65. if (!$idCentreCout || !$idBailleur) {
  66. return new JsonResponse(['success' => false, 'message' => 'Centre de coût et projet obligatoires'], 400);
  67. }
  68. // Résolution des entités liées
  69. $cecout = $this->em->getRepository(CentreCout::class)->find($idCentreCout);
  70. $nomprojet = $this->em->getRepository(Bailleur::class)->find($idBailleur);
  71. if (!$cecout || !$nomprojet) {
  72. return new JsonResponse(['success' => false, 'message' => 'Centre de coût ou projet introuvable'], 404);
  73. }
  74. $designrech = $idDesignation
  75. ? $this->em->getRepository(Catalogue::class)->findOneByDesignation($idDesignation)
  76. : null;
  77. // Calcul du prochain numDemandeAchat
  78. $num = $this->getNextNumDemande();
  79. // Conversion région
  80. $regionLabel = $this->convertRegion($region);
  81. // Création de l'entité brouillon
  82. $demAch = new DemAchatTemp();
  83. $demAch->setCentreCout($cecout->getLibele());
  84. $demAch->setCostcenter($cecout);
  85. $demAch->setNombaille($nomprojet);
  86. $demAch->setNomProjet($nomprojet->getLibele());
  87. $demAch->setQuantDemande((int)$quantDemand);
  88. $demAch->setObjet($obje);
  89. $demAch->setRegion($regionLabel);
  90. $demAch->setDescription($descrip);
  91. $demAch->setDelais($delais);
  92. $demAch->setDateSoumis(new \DateTime());
  93. $demAch->setUser($user);
  94. $demAch->setNumDemandeAchat($num);
  95. if ($designrech) {
  96. $demAch->setDesignation($designrech->getDesignation());
  97. $demAch->setCatalogue($designrech);
  98. $demAch->setPrixU((float)$prixUnit);
  99. } else {
  100. $demAch->setDesignation('Article non trouvé dans le catalogue');
  101. }
  102. $this->em->persist($demAch);
  103. $this->em->flush();
  104. return new JsonResponse([
  105. 'success' => true,
  106. 'message' => 'Article ajouté avec succès',
  107. 'numDemandeAchat' => $num,
  108. 'id' => $demAch->getId(),
  109. ]);
  110. }
  111. // =========================================================
  112. // POST /api/da/ajout-design-temp
  113. // Équivalent JSON de ajout_design_da_temp
  114. // Ajoute un article supplémentaire à un brouillon existant
  115. // =========================================================
  116. public function apiAjoutDesignTemp(Request $request): JsonResponse
  117. {
  118. $user = $this->security->getUser();
  119. if (!$user) {
  120. return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
  121. }
  122. $data = $this->decodeRequest($request);
  123. $idBailleur = $data['bailleur'] ?? null;
  124. $idCentreCout = $data['centreCout'] ?? null;
  125. $quantDemand = $data['quantDemand'] ?? null;
  126. $prixUnit = $data['prixUnit'] ?? null;
  127. $descrip = $data['descrip'] ?? null;
  128. $idDesignation = $data['iddesignrech'] ?? null;
  129. // Récupération du dernier brouillon de cet utilisateur
  130. $demtempuses = $this->em->getRepository(DemAchatTemp::class)->findByUser($user);
  131. $demtemp = end($demtempuses);
  132. if (!$demtemp) {
  133. return new JsonResponse(['success' => false, 'message' => 'Aucun brouillon en cours'], 404);
  134. }
  135. $nomprojet = $this->em->getRepository(Bailleur::class)->find($idBailleur);
  136. $centrecout = $this->em->getRepository(CentreCout::class)->find($idCentreCout);
  137. $designrech = $idDesignation
  138. ? $this->em->getRepository(Catalogue::class)->findOneByDesignation($idDesignation)
  139. : null;
  140. $num = $demtemp->getNumDemandeAchat();
  141. $demAch = new DemAchatTemp();
  142. $demAch->setCentreCout($centrecout ? $centrecout->getLibele() : '');
  143. $demAch->setCostcenter($centrecout);
  144. $demAch->setNomProjet($nomprojet ? $nomprojet->getLibele() : '');
  145. $demAch->setNombaille($nomprojet);
  146. $demAch->setRegion($demtemp->getRegion());
  147. $demAch->setQuantDemande((int)$quantDemand);
  148. $demAch->setObjet($demtemp->getObjet());
  149. $demAch->setDescription($descrip);
  150. $demAch->setDelais($demtemp->getDelais());
  151. $demAch->setDateSoumis(new \DateTime());
  152. $demAch->setUser($user);
  153. $demAch->setNumDemandeAchat($num);
  154. $demAch->setNumOrdre(1);
  155. if ($designrech) {
  156. $demAch->setDesignation($designrech->getDesignation());
  157. $demAch->setCatalogue($designrech);
  158. $demAch->setPrixU((float)$prixUnit);
  159. } else {
  160. $demAch->setDesignation('Article non trouvé dans le catalogue');
  161. }
  162. $this->em->persist($demAch);
  163. $this->em->flush();
  164. return new JsonResponse([
  165. 'success' => true,
  166. 'message' => 'Article ajouté avec succès',
  167. 'numDemandeAchat' => $num,
  168. 'id' => $demAch->getId(),
  169. ]);
  170. }
  171. // =========================================================
  172. // POST /api/da/envoyer
  173. // Équivalent JSON de daenvoyer
  174. // Convertit les DemAchatTemp en demAchat définitifs
  175. // =========================================================
  176. public function apiDaEnvoyer(Request $request): JsonResponse
  177. {
  178. $user = $this->security->getUser();
  179. if (!$user) {
  180. return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
  181. }
  182. $data = $this->decodeRequest($request);
  183. $numero = $data['num'] ?? null;
  184. if (!$numero) {
  185. return new JsonResponse(['success' => false, 'message' => 'Numéro de brouillon manquant'], 400);
  186. }
  187. $datemps = $this->em->getRepository(DemAchatTemp::class)->findDemandeAchatPerso((int)$numero);
  188. if (empty($datemps)) {
  189. return new JsonResponse(['success' => false, 'message' => 'Aucun brouillon trouvé pour ce numéro'], 404);
  190. }
  191. $numda = $this->em->getRepository(NumDA::class)->find(1);
  192. $num = $numda->getNum() + 1;
  193. $dat = date('Y-m-d H:i:s');
  194. $achat = null;
  195. foreach ($datemps as $datemp) {
  196. $achat = new demAchat();
  197. $raport = new RapportageDA();
  198. $achat->setDesignation($datemp->getDesignation());
  199. $achat->setPrixU($datemp->getPrixU());
  200. $achat->setCentreCout($datemp->getCentreCout());
  201. $achat->setNomProjet($datemp->getNomProjet());
  202. $achat->setNombaille($datemp->getNombaille());
  203. $achat->setCostcenter($datemp->getCostcenter());
  204. $achat->setCatalogue($datemp->getCatalogue());
  205. $achat->setObjet($datemp->getObjet());
  206. $achat->setRegion($datemp->getRegion());
  207. $achat->setDescription($datemp->getDescription());
  208. $achat->setUser($datemp->getUser());
  209. $achat->setQuantDemande($datemp->getQuantDemande());
  210. $achat->setResteACommander($datemp->getQuantDemande());
  211. $achat->setDelais($datemp->getDelais());
  212. $achat->setNumOrdre(1);
  213. $achat->setDateSoumis(new \DateTime($dat));
  214. // Gestion contrat-cadre (même logique que daenvoyer existant)
  215. $montantLigne = (float)$datemp->getPrixU() * (float)$datemp->getQuantDemande();
  216. $catalogue = $datemp->getCatalogue();
  217. $categorieCatalogue = $catalogue ? $catalogue->getCategorycatalogue() : null;
  218. $contratCadre = null;
  219. if ($categorieCatalogue) {
  220. foreach ($categorieCatalogue->getCategoriesMarche() as $categorieMarche) {
  221. $contrats = $this->contratCadreService->listerDisponiblesPourDA(
  222. $categorieMarche->getId(),
  223. $montantLigne
  224. );
  225. if (!empty($contrats)) {
  226. $contratCadre = $contrats[0];
  227. break;
  228. }
  229. }
  230. }
  231. if ($contratCadre) {
  232. $achat->setContratCadre($contratCadre);
  233. $achat->setStatutContratCadree(demAchat::CC_SUGGERE);
  234. $contratCadre->setMontantReserve(
  235. (string)((float)$contratCadre->getMontantReserve() + $montantLigne)
  236. );
  237. $this->em->persist($contratCadre);
  238. }
  239. $numda->setNum($num);
  240. $achat->setNumDemandeAchat($num);
  241. $this->em->persist($achat);
  242. $this->em->remove($datemp);
  243. $raport->setDateSoumis(new \DateTime($dat));
  244. $raport->setDemAchat($achat);
  245. $this->em->persist($raport);
  246. $this->em->flush();
  247. // Traductions
  248. if (!empty($datemp->getDescription()) && trim($datemp->getDescription()) !== '') {
  249. $this->translationWriter->create('demAchat', $achat->getId(), 'description', $achat->getDescription());
  250. }
  251. }
  252. if ($achat) {
  253. $this->em->refresh($achat);
  254. $this->translationWriter->create('demAchat', $achat->getId(), 'objet', $achat->getObjet());
  255. $this->em->flush();
  256. }
  257. return new JsonResponse([
  258. 'success' => true,
  259. 'message' => 'DA créée avec succès',
  260. 'numDemandeAchat' => $num,
  261. ]);
  262. }
  263. // =========================================================
  264. // POST /api/da/import-excel
  265. // Traitement du fichier Excel en mode offline
  266. // Le fichier est envoyé en base64 depuis IndexedDB
  267. // =========================================================
  268. public function apiImportExcel(Request $request): JsonResponse
  269. {
  270. $user = $this->security->getUser();
  271. if (!$user) {
  272. return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
  273. }
  274. // Cas 1 : envoi normal (formulaire online)
  275. $uploadedFile = $request->files->get('fiche');
  276. // Cas 2 : envoi offline (base64 depuis IndexedDB via Service Worker)
  277. if (!$uploadedFile) {
  278. $data = json_decode($request->getContent(), true);
  279. $base64File = $data['fileBase64'] ?? null;
  280. $fileName = $data['fileName'] ?? 'import.xlsx';
  281. if (!$base64File) {
  282. return new JsonResponse(['success' => false, 'message' => 'Aucun fichier reçu'], 400);
  283. }
  284. // Reconstruire un fichier temporaire depuis le base64
  285. $fileContent = base64_decode($base64File);
  286. $tmpPath = sys_get_temp_dir() . '/' . uniqid('da_excel_') . '_' . $fileName;
  287. file_put_contents($tmpPath, $fileContent);
  288. // Créer un SplFileInfo pour le passer au service
  289. $uploadedFile = new \SplFileInfo($tmpPath);
  290. }
  291. try {
  292. // Réutilise le service existant ExcelImportService
  293. // Injectez-le dans le constructeur si nécessaire
  294. // $this->excelImportService->importDemandesAchat($uploadedFile);
  295. // Nettoyage fichier temporaire si besoin
  296. if (isset($tmpPath) && file_exists($tmpPath)) {
  297. unlink($tmpPath);
  298. }
  299. return new JsonResponse([
  300. 'success' => true,
  301. 'message' => 'Import Excel traité avec succès',
  302. ]);
  303. } catch (\Exception $e) {
  304. return new JsonResponse([
  305. 'success' => false,
  306. 'message' => 'Erreur lors de l\'import : ' . $e->getMessage(),
  307. ], 500);
  308. }
  309. }
  310. // =========================================================
  311. // GET /api/da/liste-temp/{num}
  312. // Retourne les lignes DemAchatTemp d'un brouillon (pour UI offline)
  313. // =========================================================
  314. public function apiListeTemp(int $num): JsonResponse
  315. {
  316. $user = $this->security->getUser();
  317. if (!$user) {
  318. return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
  319. }
  320. $datemps = $this->em->getRepository(DemAchatTemp::class)->findDemandeAchatPerso($num);
  321. $lignes = [];
  322. foreach ($datemps as $d) {
  323. $lignes[] = [
  324. 'id' => $d->getId(),
  325. 'designation' => $d->getDesignation(),
  326. 'quantDemande' => $d->getQuantDemande(),
  327. 'prixU' => $d->getPrixU(),
  328. 'description' => $d->getDescription(),
  329. 'centreCout' => $d->getCentreCout(),
  330. 'nomProjet' => $d->getNomProjet(),
  331. 'objet' => $d->getObjet(),
  332. 'region' => $d->getRegion(),
  333. 'delais' => $d->getDelais(),
  334. 'numDemandeAchat' => $d->getNumDemandeAchat(),
  335. ];
  336. }
  337. return new JsonResponse([
  338. 'success' => true,
  339. 'lignes' => $lignes,
  340. 'total' => count($lignes),
  341. ]);
  342. }
  343. // =========================================================
  344. // Helpers privés
  345. // =========================================================
  346. private function decodeRequest(Request $request): array
  347. {
  348. // Supporte JSON body et form-data
  349. $contentType = $request->headers->get('Content-Type', '');
  350. if (str_contains($contentType, 'application/json')) {
  351. return json_decode($request->getContent(), true) ?? [];
  352. }
  353. return $request->request->all();
  354. }
  355. private function getNextNumDemande(): int
  356. {
  357. $demachas = $this->em->getRepository(DemAchatTemp::class)->findAll();
  358. $num = 1;
  359. foreach ($demachas as $demacha) {
  360. if ($demacha->getNumDemandeAchat() > $num) {
  361. $num = $demacha->getNumDemandeAchat();
  362. }
  363. }
  364. return $num + 1;
  365. }
  366. private function convertRegion(?string $region): string
  367. {
  368. $map = [
  369. '1' => 'Bamako',
  370. '2' => 'Segou',
  371. '3' => 'Mopti',
  372. '4' => 'Sikasso',
  373. '5' => 'Koulikoro',
  374. ];
  375. return $map[$region] ?? ($region ?? '');
  376. }
  377. public function checkSession(): JsonResponse
  378. {
  379. $user = $this->security->getUser();
  380. if (!$user) {
  381. return new JsonResponse([
  382. 'authenticated' => false,
  383. 'message' => 'Session expirée ou non authentifié',
  384. ], 401);
  385. }
  386. return new JsonResponse([
  387. 'authenticated' => true,
  388. 'username' => $user->getNom() . ' ' . $user->getPrenom(),
  389. 'userId' => $user->getId(),
  390. ]);
  391. }
  392. public function soumettrebrouillonComplet(Request $request): JsonResponse
  393. {
  394. $user = $this->security->getUser();
  395. if (!$user) {
  396. return new JsonResponse(['success' => false, 'message' => 'Session expirée'], 401);
  397. }
  398. $data = json_decode($request->getContent(), true);
  399. if (!$data) {
  400. return new JsonResponse(['success' => false, 'message' => 'Données invalides'], 400);
  401. }
  402. $objet = $data['objet'] ?? '';
  403. $region = $data['region'] ?? '';
  404. $centreCoutId = $data['centreCoutId'] ?? null;
  405. $bailleurId = $data['bailleurId'] ?? null;
  406. $delais = $data['delais'] ?? null;
  407. $articles = $data['articles'] ?? [];
  408. if (empty($articles)) {
  409. return new JsonResponse(['success' => false, 'message' => 'Aucun article'], 400);
  410. }
  411. $cecout = $centreCoutId ? $this->em->getRepository(CentreCout::class)->find($centreCoutId) : null;
  412. $nomprojet = $bailleurId ? $this->em->getRepository(Bailleur::class)->find($bailleurId) : null;
  413. if (!$cecout || !$nomprojet) {
  414. return new JsonResponse(['success' => false, 'message' => 'Centre de coût ou projet introuvable'], 404);
  415. }
  416. // ── Calcul du prochain numéro de brouillon ───────────────────────────
  417. $demachas = $this->em->getRepository(DemAchatTemp::class)->findAll();
  418. $num = 1;
  419. foreach ($demachas as $d) {
  420. if ($d->getNumDemandeAchat() > $num) $num = $d->getNumDemandeAchat();
  421. }
  422. $num++;
  423. // ── Création des DemAchatTemp pour chaque article ───────────────────
  424. foreach ($articles as $article) {
  425. $designation = $article['designation'] ?? '';
  426. $quantite = (int)($article['quantite'] ?? 1);
  427. $prixU = (float)($article['prixU'] ?? 0);
  428. $description = $article['description'] ?? '';
  429. $designrech = $this->em->getRepository(Catalogue::class)
  430. ->findOneByDesignation($designation);
  431. $demAch = new DemAchatTemp();
  432. $demAch->setCentreCout($cecout->getLibele());
  433. $demAch->setCostcenter($cecout);
  434. $demAch->setNombaille($nomprojet);
  435. $demAch->setNomProjet($nomprojet->getLibele());
  436. $demAch->setRegion($region);
  437. $demAch->setObjet($objet);
  438. $demAch->setQuantDemande($quantite);
  439. $demAch->setDescription($description);
  440. $demAch->setDelais($delais);
  441. $demAch->setDateSoumis(new \DateTime());
  442. $demAch->setUser($user);
  443. $demAch->setNumDemandeAchat($num);
  444. $demAch->setNumOrdre(1);
  445. if ($designrech) {
  446. $demAch->setDesignation($designrech->getDesignation());
  447. $demAch->setCatalogue($designrech);
  448. $demAch->setPrixU($prixU);
  449. } else {
  450. $demAch->setDesignation($designation . ' (catalogue à vérifier)');
  451. $demAch->setPrixU($prixU);
  452. }
  453. $this->em->persist($demAch);
  454. }
  455. $this->em->flush();
  456. // ── Conversion immédiate en DA définitive (daenvoyer) ────────────────
  457. $datemps = $this->em->getRepository(DemAchatTemp::class)->findDemandeAchatPerso($num);
  458. $numda = $this->em->getRepository(NumDA::class)->find(1);
  459. $numDA = $numda->getNum() + 1;
  460. $dat = date('Y-m-d H:i:s');
  461. $achat = null;
  462. foreach ($datemps as $datemp) {
  463. $achat = new demAchat();
  464. $raport = new RapportageDA();
  465. $achat->setDesignation($datemp->getDesignation());
  466. $achat->setPrixU($datemp->getPrixU());
  467. $achat->setCentreCout($datemp->getCentreCout());
  468. $achat->setNomProjet($datemp->getNomProjet());
  469. $achat->setNombaille($datemp->getNombaille());
  470. $achat->setCostcenter($datemp->getCostcenter());
  471. $achat->setCatalogue($datemp->getCatalogue());
  472. $achat->setObjet($datemp->getObjet());
  473. $achat->setRegion($datemp->getRegion());
  474. $achat->setDescription($datemp->getDescription());
  475. $achat->setUser($datemp->getUser());
  476. $achat->setQuantDemande($datemp->getQuantDemande());
  477. $achat->setResteACommander($datemp->getQuantDemande());
  478. $achat->setDelais($datemp->getDelais());
  479. $achat->setNumOrdre(1);
  480. $achat->setDateSoumis(new \DateTime($dat));
  481. // Gestion contrat-cadre
  482. $montantLigne = (float)$datemp->getPrixU() * (float)$datemp->getQuantDemande();
  483. $catalogue = $datemp->getCatalogue();
  484. $categorieCatalogue = $catalogue ? $catalogue->getCategorycatalogue() : null;
  485. $contratCadre = null;
  486. if ($categorieCatalogue) {
  487. foreach ($categorieCatalogue->getCategoriesMarche() as $categorieMarche) {
  488. $contrats = $this->contratCadreService->listerDisponiblesPourDA(
  489. $categorieMarche->getId(), $montantLigne
  490. );
  491. if (!empty($contrats)) { $contratCadre = $contrats[0]; break; }
  492. }
  493. }
  494. if ($contratCadre) {
  495. $achat->setContratCadre($contratCadre);
  496. $achat->setStatutContratCadree(demAchat::CC_SUGGERE);
  497. $contratCadre->setMontantReserve(
  498. (string)((float)$contratCadre->getMontantReserve() + $montantLigne)
  499. );
  500. $this->em->persist($contratCadre);
  501. }
  502. $numda->setNum($numDA);
  503. $achat->setNumDemandeAchat($numDA);
  504. $this->em->persist($achat);
  505. $this->em->remove($datemp);
  506. $raport->setDateSoumis(new \DateTime($dat));
  507. $raport->setDemAchat($achat);
  508. $this->em->persist($raport);
  509. $this->em->flush();
  510. if (!empty($datemp->getDescription()) && trim($datemp->getDescription()) !== '') {
  511. $this->translationWriter->create(
  512. 'demAchat', $achat->getId(), 'description', $achat->getDescription()
  513. );
  514. }
  515. }
  516. if ($achat) {
  517. $this->em->refresh($achat);
  518. $this->translationWriter->create('demAchat', $achat->getId(), 'objet', $achat->getObjet());
  519. $this->em->flush();
  520. }
  521. return new JsonResponse([
  522. 'success' => true,
  523. 'message' => 'DA créée avec succès',
  524. 'numDemandeAchat' => $numDA,
  525. ]);
  526. }
  527. // =========================================================
  528. // GET /api/da/references
  529. // Retourne catalogue + centres de coût + bailleurs en JSON
  530. // Appelé par da-offline.js lors de la visite online pour
  531. // mettre en cache ces données dans IndexedDB (da-shell.html)
  532. // =========================================================
  533. public function getReferences(): JsonResponse
  534. {
  535. $user = $this->security->getUser();
  536. if (!$user) {
  537. return new JsonResponse(['success' => false], 401);
  538. }
  539. $catalogues = $this->em->getRepository(Catalogue::class)
  540. ->createQueryBuilder('c')
  541. ->select('c.id, c.designation') // ← prixUnitaire retiré
  542. ->getQuery()
  543. ->getArrayResult();
  544. $centres = $this->em->getRepository(CentreCout::class)
  545. ->createQueryBuilder('c')
  546. ->select('c.id, c.libele')
  547. ->getQuery()
  548. ->getArrayResult();
  549. $bailleurs = $this->em->getRepository(Bailleur::class)
  550. ->createQueryBuilder('b')
  551. ->select('b.id, b.libele')
  552. ->getQuery()
  553. ->getArrayResult();
  554. return new JsonResponse([
  555. 'success' => true,
  556. 'catalogue' => $catalogues,
  557. 'centres' => $centres,
  558. 'bailleurs' => $bailleurs,
  559. 'cachedAt' => (new \DateTime())->format('Y-m-d H:i:s'),
  560. ]);
  561. }
  562. // =========================================================
  563. // GET /da-offline
  564. // Sert da-shell.html depuis Symfony (avec headers corrects)
  565. // Permet d'être mis en cache par le SW même si le fichier
  566. // est dans /public/da-shell.html
  567. // =========================================================
  568. public function daShell(): Response
  569. {
  570. return new Response(
  571. file_get_contents($this->getParameter('kernel.project_dir') . '/public/da-shell.html'),
  572. 200,
  573. ['Content-Type' => 'text/html; charset=utf-8']
  574. );
  575. }
  576. // =========================================================
  577. // GET /api/da/mes-das
  578. // Retourne toutes les DAs visibles par l'utilisateur connecté
  579. // Utilisé par cacheReferences() pour la consultation offline
  580. // =========================================================
  581. public function getMesDas(): JsonResponse
  582. {
  583. $user = $this->security->getUser();
  584. if (!$user) {
  585. return new JsonResponse(['success' => false, 'message' => 'Non authentifié'], 401);
  586. }
  587. // Réutiliser getValidationData() du service existant
  588. $data = $this->demandeAchatService->getValidationData();
  589. // Récupérer toutes les DAs visibles selon le rôle
  590. // list_demAcha = DAs à valider au niveau de l'utilisateur
  591. // list_demAchauser = Mes DAs
  592. // list_demAchaall = Toutes les DAs (si ROLE_VU_ALL)
  593. $dasIds = [];
  594. // Fusionner toutes les listes de DAs visibles
  595. $listes = [
  596. $data['list_demAcha'] ?? [], // DAs à mon niveau
  597. $data['list_demAcha2'] ?? [], // DAs à valider
  598. $data['list_demAchauser'] ?? [], // Mes DAs
  599. $data['list_demAchavalide']?? [], // DAs que j'ai validées
  600. $data['list_demAcharefuse']?? [], // DAs que j'ai rejetées
  601. ];
  602. // Si VU_ALL, ajouter toutes les DAs
  603. if ($this->security->isGranted('ROLE_VU_ALL')) {
  604. $listes[] = $data['list_demAchaall'] ?? [];
  605. }
  606. // Collecter tous les IDs uniques
  607. foreach ($listes as $liste) {
  608. foreach ($liste as $numDA => $id) {
  609. $dasIds[$id] = $id;
  610. }
  611. }
  612. if (empty($dasIds)) {
  613. return new JsonResponse(['success' => true, 'das' => [], 'total' => 0]);
  614. }
  615. // Récupérer les DAs (une ligne représentative par numDemandeAchat)
  616. $das = [];
  617. foreach ($dasIds as $id) {
  618. $da = $this->em->getRepository(demAchat::class)->find($id);
  619. if (!$da) continue;
  620. // Calculer le total de la DA (toutes les lignes)
  621. $lignes = $this->em->getRepository(demAchat::class)->findBy([
  622. 'numDemandeAchat' => $da->getNumDemandeAchat(),
  623. 'drapoRejetchef' => 0,
  624. ]);
  625. $total = 0;
  626. $nbLignes = count($lignes);
  627. foreach ($lignes as $ligne) {
  628. $total += (float)$ligne->getPrixU() * (float)$ligne->getQuantDemande();
  629. }
  630. if ($da->getTva()) {
  631. $total += $total * ((float)$da->getTva() / 100);
  632. }
  633. // Statut de validation
  634. $statut = $this->getStatutDA($da);
  635. $das[] = [
  636. 'id' => $da->getId(),
  637. 'numDemandeAchat' => $da->getNumDemandeAchat(),
  638. 'objet' => $da->getObjet(),
  639. 'region' => $da->getRegion(),
  640. 'centreCout' => $da->getCentreCout(),
  641. 'nomProjet' => $da->getNomProjet(),
  642. 'dateSoumis' => $da->getDateSoumis()?->format('d/m/Y'),
  643. 'demandeur' => $da->getUser()?->getNom() . ' ' . $da->getUser()?->getPrenom(),
  644. 'total' => $total,
  645. 'nbLignes' => $nbLignes,
  646. 'statut' => $statut,
  647. 'delais' => $da->getDelais(),
  648. 'drapoImp' => $da->getDrapoImp(),
  649. 'drapoValide' => $da->getDrapoValide(),
  650. 'lignes' => array_map(function($l) {
  651. return [
  652. 'id' => $l->getId(),
  653. 'designation' => $l->getDesignation(),
  654. 'quantite' => $l->getQuantDemande(),
  655. 'prixU' => $l->getPrixU(),
  656. 'centreCout' => $l->getCentreCout(),
  657. 'nomProjet' => $l->getNomProjet(),
  658. 'description' => $l->getDescription(),
  659. ];
  660. }, $lignes),
  661. ];
  662. }
  663. // Trier par numDemandeAchat décroissant
  664. usort($das, fn($a, $b) => $b['numDemandeAchat'] <=> $a['numDemandeAchat']);
  665. return new JsonResponse([
  666. 'success' => true,
  667. 'das' => $das,
  668. 'total' => count($das),
  669. ]);
  670. }
  671. // ─── Helper : statut lisible d'une DA ────────────────────────────────────
  672. private function getStatutDA(demAchat $da): string
  673. {
  674. if ($da->getDrapoRejetchef()) return 'Ligne rejetée';
  675. if ($da->getDrapoRejetLead()) return 'Rejeté (Lead)';
  676. if ($da->getDrapoRRrefu()) return 'Rejeté (RR)';
  677. if ($da->getDrapoDSCSCrefu()) return 'Rejeté (Finance)';
  678. if ($da->getDrapoDPrefu()) return 'Rejeté (DP)';
  679. if ($da->getDrapoSuprefu()) return 'Rejeté (Sup)';
  680. if ($da->getDrapoChefprefu()) return 'Rejeté (Chef projet)';
  681. if ($da->getDrapoLeadrefu()) return 'Rejeté (Lead)';
  682. if ($da->getDrapoImp()) return 'Approuvé – prêt à imprimer';
  683. if ($da->getDrapoRR()) return 'Validé (RR)';
  684. if ($da->getDrapoDSCSC()) return 'Validé (Finance)';
  685. if ($da->getDrapoDP()) return 'Validé (DP)';
  686. if ($da->getDrapoLead()) return 'Validé (Lead)';
  687. if ($da->getDrapoChefp()) return 'Validé (Chef projet)';
  688. if ($da->getDrapoSup()) return 'Validé (Sup. hiérar.)';
  689. if ($da->getDrapoRespAcha()) return 'Validé (Resp. achat)';
  690. if ($da->getDrapoRespReg()) return 'Validé (Resp. régional)';
  691. return 'En attente de validation';
  692. }
  693. }