src/Controller/PointVenteController.php line 540

Open in your IDE?
  1. <?php
  2. // src/Controller/PointVenteController.php
  3. namespace App\Controller;
  4. use App\Entity\Categorie;
  5. use App\Entity\Client;
  6. use App\Entity\CommandeCaisse;
  7. use App\Entity\LigneCommandeCaisse;
  8. use App\Entity\Produit;
  9. use App\Entity\Salarie;
  10. use App\Entity\TableRestaurant;
  11. use App\Form\CategorieType;
  12. use App\Form\ClientType;
  13. use App\Form\ProduitCaisseType;
  14. use App\Repository\CategorieRepository;
  15. use App\Repository\ClientRepository;
  16. use App\Repository\ClotureCaisseRepository;
  17. use App\Repository\CommandeCaisseRepository;
  18. use App\Repository\ProduitRepository;
  19. use App\Service\CaisseService;
  20. use App\Service\EntrepriseResolver;
  21. use App\Service\FileUploader;
  22. use Doctrine\ORM\EntityManagerInterface;
  23. use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
  24. use Symfony\Component\HttpFoundation\JsonResponse;
  25. use Symfony\Component\HttpFoundation\Request;
  26. use Symfony\Component\HttpFoundation\Response;
  27. use Symfony\Component\Routing\Annotation\Route;
  28. /**
  29.  * Écran de caisse. Toute la logique métier (panier, calculs, attente,
  30.  * encaissement) est dans CaisseService ; ce contrôleur ne fait que
  31.  * l'interface. Les routes "point_vente_*" sont ouvertes aux salariés
  32.  * (SalarieAccessSubscriber), les droits fins sont vérifiés ici.
  33.  *
  34.  * @Route("/caisse")
  35.  */
  36. class PointVenteController extends AbstractController
  37. {
  38.     private $entityManager;
  39.     private $entrepriseResolver;
  40.     private $caisse;
  41.     public function __construct(EntityManagerInterface $entityManager, EntrepriseResolver $entrepriseResolver, CaisseService $caisse)
  42.     {
  43.         $this->entityManager = $entityManager;
  44.         $this->entrepriseResolver = $entrepriseResolver;
  45.         $this->caisse = $caisse;
  46.     }
  47.     /**
  48.      * @Route("/", name="point_vente_index")
  49.      */
  50.     public function index(CategorieRepository $categorieRepository, ProduitRepository $produitRepository, Request $request, ClientRepository $clientRepository, ClotureCaisseRepository $clotureCaisseRepository, CommandeCaisseRepository $commandeRepository): Response
  51.     {
  52.         if (!$this->getUser()) {
  53.             return $this->redirectToRoute('app_login');
  54.         }
  55.         $principal = $this->getUser();
  56.         $user = $this->entrepriseResolver->getOwnerUser();
  57.         $entreprise = $this->entrepriseResolver->getCurrentEntreprise();
  58.         $client = new Client();
  59.         $form = $this->createForm(ClientType::class, $client);
  60.         $commande = $this->caisse->commandeCourante();
  61.         if ($commande) {
  62.             $this->caisse->synchroniserPrix($commande);
  63.         }
  64.         $calcul = $commande ? $this->caisse->calculer($commande) : $this->caisse->calculer(new CommandeCaisse());
  65.         $searchTerm = $request->query->get('search');
  66.         $categorieId = $request->query->get('categorie');
  67.         return $this->render('caisse/point_vente/index.html.twig', [
  68.             'categories' => $categorieRepository->findWithProduitsByUser($user),
  69.             'produits' => $produitRepository->search($searchTerm, $categorieId, $user),
  70.             'topProduits' => $produitRepository->findBy(['topVente' => true, 'user' => $user], null, 5),
  71.             'commande' => $commande,
  72.             'panier' => $calcul['lignes'],
  73.             'totalPanier' => $calcul['totalHT'],
  74.             'totalTva' => $calcul['totalTva'],
  75.             'totalTTC' => $calcul['totalTTC'],
  76.             'detailsTva' => $calcul['detailsTva'],
  77.             'timbre' => $calcul['timbre'],
  78.             'searchTerm' => $searchTerm,
  79.             'selectedCategory' => $categorieId,
  80.             'clients' => $user ? $clientRepository->findByUser($user) : [],
  81.             'form' => $form->createView(),
  82.             'isSalarie' => $principal instanceof Salarie,
  83.             'sessionCaisse' => $entreprise ? $clotureCaisseRepository->findOuverte($entreprise) : null,
  84.             'canCloture' => $this->peutGerer(),
  85.             'canVoirRapports' => !($principal instanceof Salarie) || $principal->isAccesRapports(),
  86.             'canReparations' => $principal instanceof Salarie && $principal->isAccesReparations(),
  87.             'canEncaisser' => $this->peutEncaisser(),
  88.             'principalNom' => $principal instanceof Salarie
  89.                 ? $principal->getNomComplet()
  90.                 : trim($principal->getFirstname() . ' ' . $principal->getLastname()),
  91.             'entreprise' => $entreprise,
  92.             'nbEnAttente' => $user ? $commandeRepository->countEnAttente($user) : 0,
  93.             'typesCommande' => CommandeCaisse::TYPES_RESTAURANT,
  94.             'tablesTransfert' => $commande && $commande->getTable() && $entreprise && $entreprise->isCaisseGestionTables()
  95.                 ? $this->entityManager->getRepository(TableRestaurant::class)->findBy(['user' => $user, 'active' => true], ['salle' => 'ASC', 'position' => 'ASC'])
  96.                 : [],
  97.         ]);
  98.     }
  99.     // ------------------------------------------------------------------
  100.     // Panier
  101.     // ------------------------------------------------------------------
  102.     /**
  103.      * @Route("/add-to-cart/{id}", name="point_vente_add_to_cart", methods={"POST"})
  104.      */
  105.     public function addToCart(Produit $produit, Request $request): Response
  106.     {
  107.         $commande = $this->caisse->commandeCourante(true);
  108.         $ligne = $this->caisse->ajouterProduit($commande, $produit, 1.0, $request->request->get('note'));
  109.         $remise = $request->request->get('remise');
  110.         if ($remise !== null && $remise !== '') {
  111.             $ligne->setRemiseManuelle((float) $remise);
  112.             $this->entityManager->flush();
  113.         }
  114.         return $this->redirectToRoute('point_vente_index', $this->filtresCatalogue($request));
  115.     }
  116.     /**
  117.      * @Route("/scan-barcode", name="point_vente_scan_barcode", methods={"POST"})
  118.      */
  119.     public function scanBarcode(Request $request, ProduitRepository $produitRepository): Response
  120.     {
  121.         if (!$this->isCsrfTokenValid('scan_barcode', $request->request->get('_token'))) {
  122.             return $this->redirectToRoute('point_vente_index');
  123.         }
  124.         $codeBarre = trim((string) $request->request->get('code_barre'));
  125.         $user = $this->entrepriseResolver->getOwnerUser();
  126.         if ($codeBarre === '' || !$user) {
  127.             return $this->redirectToRoute('point_vente_index');
  128.         }
  129.         $produit = $produitRepository->findOneByCodeBarreForUser($codeBarre, $user);
  130.         if (!$produit) {
  131.             // Pas de correspondance exacte : on retombe sur la recherche classique,
  132.             // qui filtre déjà aussi par code-barres (résultat partiel possible).
  133.             $this->addFlash('warning', 'Aucun produit avec ce code-barres : ' . $codeBarre);
  134.             return $this->redirectToRoute('point_vente_index', ['search' => $codeBarre]);
  135.         }
  136.         $this->caisse->ajouterProduit($this->caisse->commandeCourante(true), $produit);
  137.         $this->addFlash('success', 'Produit ajouté au panier : ' . $produit->getDescription());
  138.         return $this->redirectToRoute('point_vente_index');
  139.     }
  140.     /**
  141.      * @Route("/ligne/{id}/quantite", name="point_vente_update_cart", methods={"POST"})
  142.      */
  143.     public function updateCart(LigneCommandeCaisse $ligne, Request $request): Response
  144.     {
  145.         if ($refus = $this->refusSiNonModifiable($ligne)) {
  146.             return $refus;
  147.         }
  148.         $quantite = (float) str_replace(',', '.', (string) $request->request->get('quantite', 1));
  149.         if ($quantite < $ligne->getQuantiteEnvoyeeCuisine() && !$this->peutGerer()) {
  150.             $this->addFlash('error', 'Cet article a déjà été envoyé en cuisine : seul un responsable peut le retirer.');
  151.             return $this->redirectToRoute('point_vente_index');
  152.         }
  153.         $this->caisse->modifierQuantite($ligne, $quantite);
  154.         return $this->redirectToRoute('point_vente_index');
  155.     }
  156.     /**
  157.      * @Route("/ligne/{id}/remise", name="point_vente_apply_discount", methods={"POST"})
  158.      */
  159.     public function applyDiscount(LigneCommandeCaisse $ligne, Request $request): Response
  160.     {
  161.         if ($refus = $this->refusSiNonModifiable($ligne)) {
  162.             return $refus;
  163.         }
  164.         $remise = $request->request->get('remise');
  165.         $ligne->setRemiseManuelle($remise === null || $remise === '' ? null : (float) str_replace(',', '.', (string) $remise));
  166.         $this->entityManager->flush();
  167.         if ($request->isXmlHttpRequest()) {
  168.             return $this->json(['success' => true]);
  169.         }
  170.         return $this->redirectToRoute('point_vente_index');
  171.     }
  172.     /**
  173.      * @Route("/ligne/{id}/note", name="point_vente_ligne_note", methods={"POST"})
  174.      */
  175.     public function noteLigne(LigneCommandeCaisse $ligne, Request $request): Response
  176.     {
  177.         if ($refus = $this->refusSiNonModifiable($ligne)) {
  178.             return $refus;
  179.         }
  180.         $ligne->setNote($request->request->get('note'));
  181.         $this->entityManager->flush();
  182.         return $this->redirectToRoute('point_vente_index');
  183.     }
  184.     /**
  185.      * @Route("/ligne/{id}/supprimer", name="point_vente_remove_from_cart", methods={"POST"})
  186.      */
  187.     public function removeFromCart(LigneCommandeCaisse $ligne): Response
  188.     {
  189.         if ($refus = $this->refusSiNonModifiable($ligne)) {
  190.             return $refus;
  191.         }
  192.         if ($ligne->isEnvoyeeCuisine() && !$this->peutGerer()) {
  193.             $this->addFlash('error', 'Cet article a déjà été envoyé en cuisine : seul un responsable peut le retirer.');
  194.             return $this->redirectToRoute('point_vente_index');
  195.         }
  196.         $this->caisse->supprimerLigne($ligne);
  197.         return $this->redirectToRoute('point_vente_index');
  198.     }
  199.     /**
  200.      * @Route("/vider-panier", name="point_vente_vider_panier", methods={"POST"})
  201.      */
  202.     public function viderPanier(Request $request): Response
  203.     {
  204.         $commande = $this->caisse->commandeCourante();
  205.         if ($commande) {
  206.             foreach ($commande->getLignes() as $ligne) {
  207.                 if ($ligne->isEnvoyeeCuisine() && !$this->peutGerer()) {
  208.                     $this->addFlash('error', 'Des articles ont déjà été envoyés en cuisine : seul un responsable peut vider cette commande.');
  209.                     return $this->redirectToRoute('point_vente_index');
  210.                 }
  211.             }
  212.             if ($commande->getTable()) {
  213.                 // Une table vidée est libérée.
  214.                 $this->caisse->annuler($commande);
  215.             } else {
  216.                 $this->caisse->vider($commande);
  217.             }
  218.         }
  219.         if ($request->isXmlHttpRequest()) {
  220.             return $this->json(['success' => true, 'message' => 'Panier vidé avec succès']);
  221.         }
  222.         $this->addFlash('success', 'Le panier a été vidé');
  223.         return $this->redirectToRoute('point_vente_index');
  224.     }
  225.     /**
  226.      * Type de commande (sur place / à emporter / livraison), couverts, nom, note.
  227.      *
  228.      * @Route("/commande/infos", name="point_vente_commande_infos", methods={"POST"})
  229.      */
  230.     public function infosCommande(Request $request): Response
  231.     {
  232.         $commande = $this->caisse->commandeCourante(true);
  233.         $type = $request->request->get('type_commande');
  234.         if ($type !== null && isset(CommandeCaisse::TYPES_RESTAURANT[$type]) && !$commande->getTable()) {
  235.             $commande->setTypeCommande($type);
  236.         }
  237.         if ($request->request->has('nb_couverts')) {
  238.             $commande->setNbCouverts((int) $request->request->get('nb_couverts'));
  239.         }
  240.         if ($request->request->has('libelle')) {
  241.             $commande->setLibelle($request->request->get('libelle'));
  242.         }
  243.         if ($request->request->has('note')) {
  244.             $commande->setNote(trim((string) $request->request->get('note')) ?: null);
  245.         }
  246.         $this->entityManager->flush();
  247.         return $this->redirectToRoute('point_vente_index');
  248.     }
  249.     // ------------------------------------------------------------------
  250.     // Commandes en attente
  251.     // ------------------------------------------------------------------
  252.     /**
  253.      * @Route("/commande/attente", name="point_vente_mettre_en_attente", methods={"POST"})
  254.      */
  255.     public function mettreEnAttente(Request $request): Response
  256.     {
  257.         $commande = $this->caisse->commandeCourante();
  258.         if (!$commande || $commande->isVide()) {
  259.             $this->addFlash('warning', 'Le panier est vide : rien à mettre en attente.');
  260.             return $this->redirectToRoute('point_vente_index');
  261.         }
  262.         $this->caisse->mettreEnAttente($commande, $request->request->get('libelle'));
  263.         $this->addFlash('success', $commande->getTable()
  264.             ? sprintf('Commande de la %s enregistrée.', $commande->getTable()->getLibelle())
  265.             : sprintf('« %s » mise en attente.', $commande->getNomAffiche()));
  266.         return $this->redirectToRoute($commande->getTable() ? 'point_vente_tables' : 'point_vente_index');
  267.     }
  268.     /**
  269.      * @Route("/commandes-en-attente", name="point_vente_en_attente", methods={"GET"})
  270.      */
  271.     public function listeEnAttente(CommandeCaisseRepository $commandeRepository): Response
  272.     {
  273.         $owner = $this->entrepriseResolver->getOwnerUser();
  274.         $commandes = $owner ? $commandeRepository->findEnAttente($owner) : [];
  275.         $totaux = [];
  276.         foreach ($commandes as $c) {
  277.             $totaux[$c->getId()] = $this->caisse->calculer($c)['totalAPayer'];
  278.         }
  279.         return $this->render('caisse/point_vente/en_attente.html.twig', [
  280.             'commandes' => $commandes,
  281.             'totaux' => $totaux,
  282.             'commandeCourante' => $this->caisse->commandeCourante(),
  283.             'canGerer' => $this->peutGerer(),
  284.         ]);
  285.     }
  286.     /**
  287.      * @Route("/commande/{id}/reprendre", name="point_vente_reprendre", methods={"POST"})
  288.      */
  289.     public function reprendre(CommandeCaisse $commande, Request $request): Response
  290.     {
  291.         if (!$this->isCsrfTokenValid('commande' . $commande->getId(), $request->request->get('_token')) || !$commande->isModifiable()) {
  292.             $this->addFlash('error', 'Cette commande ne peut plus être reprise.');
  293.             return $this->redirectToRoute('point_vente_en_attente');
  294.         }
  295.         $courante = $this->caisse->commandeCourante();
  296.         if ($courante && $courante->getId() !== $commande->getId() && !$courante->isVide()) {
  297.             // Le panier du poste n'est jamais perdu : il part en attente.
  298.             $this->caisse->mettreEnAttente($courante);
  299.             $this->addFlash('info', sprintf('Le panier en cours a été mis en attente (« %s »).', $courante->getNomAffiche()));
  300.         }
  301.         $this->caisse->definirCourante($commande);
  302.         return $this->redirectToRoute('point_vente_index');
  303.     }
  304.     /**
  305.      * @Route("/commande/{id}/annuler", name="point_vente_annuler_commande", methods={"POST"})
  306.      */
  307.     public function annulerCommande(CommandeCaisse $commande, Request $request): Response
  308.     {
  309.         if (!$this->isCsrfTokenValid('commande' . $commande->getId(), $request->request->get('_token')) || !$commande->isModifiable()) {
  310.             $this->addFlash('error', 'Cette commande ne peut pas être annulée.');
  311.             return $this->redirectToRoute('point_vente_en_attente');
  312.         }
  313.         if ($commande->hasArticlesEnvoyesCuisine() && !$this->peutGerer()) {
  314.             $this->addFlash('error', 'Commande déjà envoyée en cuisine : seul un responsable peut l\'annuler.');
  315.             return $this->redirectToRoute('point_vente_en_attente');
  316.         }
  317.         $this->caisse->annuler($commande);
  318.         $this->addFlash('success', sprintf('« %s » annulée.', $commande->getNomAffiche()));
  319.         return $this->redirectToRoute($commande->getTable() ? 'point_vente_tables' : 'point_vente_en_attente');
  320.     }
  321.     // ------------------------------------------------------------------
  322.     // Encaissement
  323.     // ------------------------------------------------------------------
  324.     /**
  325.      * Encaissement par formulaire (espèces, carte, chèque, non payé) : retour à la caisse.
  326.      *
  327.      * @Route("/valider-commande", name="point_vente_valider_commande", methods={"POST"})
  328.      */
  329.     public function validerCommande(Request $request): Response
  330.     {
  331.         try {
  332.             $encaissement = $this->encaisserDepuisRequete($request);
  333.         } catch (\RuntimeException $e) {
  334.             $this->addFlash('error', $e->getMessage());
  335.             return $this->redirectToRoute('point_vente_index');
  336.         }
  337.         $this->flashStock($encaissement['stock']);
  338.         $this->addFlash('success', 'Vente enregistrée (Réf : ' . $encaissement['devis']->getReference() . ')');
  339.         return $this->redirectToRoute('point_vente_index');
  340.     }
  341.     /**
  342.      * Encaissement avec impression du ticket (appel AJAX) : renvoie les données du ticket.
  343.      *
  344.      * @Route("/valider-commande-ticket", name="point_vente_valider_commande_ticket", methods={"POST"})
  345.      */
  346.     public function validerCommandeAvecTicket(Request $request): JsonResponse
  347.     {
  348.         try {
  349.             $encaissement = $this->encaisserDepuisRequete($request);
  350.         } catch (\RuntimeException $e) {
  351.             return $this->json(['success' => false, 'message' => $e->getMessage()], 400);
  352.         }
  353.         $this->flashStock($encaissement['stock']);
  354.         return $this->json([
  355.             'success' => true,
  356.             'message' => 'Vente enregistrée (Réf : ' . $encaissement['devis']->getReference() . ')',
  357.             'ticketData' => $this->caisse->donneesTicket($encaissement, $this->entrepriseResolver->getCurrentEntreprise()),
  358.         ]);
  359.     }
  360.     private function encaisserDepuisRequete(Request $request): array
  361.     {
  362.         if (!$this->peutEncaisser()) {
  363.             throw new \RuntimeException('Vous n\'avez pas le droit d\'encaisser : la commande doit être encaissée par un responsable.');
  364.         }
  365.         if (!$this->isCsrfTokenValid('validate_order', $request->request->get('_token'))) {
  366.             throw new \RuntimeException('Session expirée, rechargez la caisse.');
  367.         }
  368.         $entreprise = $this->entrepriseResolver->getCurrentEntreprise();
  369.         $sessionCaisse = $entreprise ? $this->entityManager->getRepository(\App\Entity\ClotureCaisse::class)->findOuverte($entreprise) : null;
  370.         if (!$sessionCaisse) {
  371.             throw new \RuntimeException('La caisse est fermée : ouvrez la caisse avant d\'encaisser.');
  372.         }
  373.         $commande = $this->caisse->commandeCourante();
  374.         if (!$commande || $commande->isVide()) {
  375.             throw new \RuntimeException('Le panier est vide.');
  376.         }
  377.         $client = null;
  378.         $clientId = $request->request->get('selectedClientId')
  379.             ?: $request->request->get('selectedespeceClientId')
  380.             ?: $request->request->get('selectedClientIdTicket');
  381.         if ($clientId) {
  382.             // Filtre multi-entreprise : un client d'une autre entreprise est introuvable.
  383.             $client = $this->entityManager->getRepository(Client::class)->find($clientId);
  384.             if (!$client) {
  385.                 throw new \RuntimeException('Client sélectionné invalide.');
  386.             }
  387.         }
  388.         return $this->caisse->encaisser(
  389.             $commande,
  390.             (string) ($request->request->get('payment_method') ?: 'espece'),
  391.             (float) str_replace(',', '.', (string) $request->request->get('montant_recu', 0)),
  392.             (float) str_replace(',', '.', (string) $request->request->get('global_discount', 0)),
  393.             $client
  394.         );
  395.     }
  396.     private function flashStock(array $stock): void
  397.     {
  398.         if (!empty($stock['negatifs'])) {
  399.             $this->addFlash('warning', 'Stock négatif pour : ' . implode(', ', $stock['negatifs']) . '. Pensez à régulariser le stock.');
  400.         }
  401.     }
  402.     // ------------------------------------------------------------------
  403.     // Droits
  404.     // ------------------------------------------------------------------
  405.     /** Patron, ou salarié ayant le droit de clôture (responsable). */
  406.     private function peutGerer(): bool
  407.     {
  408.         $principal = $this->getUser();
  409.         return !($principal instanceof Salarie) || $principal->isAccesCloture();
  410.     }
  411.     private function peutEncaisser(): bool
  412.     {
  413.         $principal = $this->getUser();
  414.         return !($principal instanceof Salarie) || $principal->isAccesEncaissement();
  415.     }
  416.     private function refusSiNonModifiable(LigneCommandeCaisse $ligne): ?Response
  417.     {
  418.         if (!$ligne->getCommande()->isModifiable()) {
  419.             $this->addFlash('error', 'Cette commande est déjà clôturée.');
  420.             return $this->redirectToRoute('point_vente_index');
  421.         }
  422.         return null;
  423.     }
  424.     private function filtresCatalogue(Request $request): array
  425.     {
  426.         return array_filter([
  427.             'categorie' => $request->request->get('categorie'),
  428.             'search' => $request->request->get('search'),
  429.         ]);
  430.     }
  431.     // ------------------------------------------------------------------
  432.     // Créations rapides depuis la caisse
  433.     // ------------------------------------------------------------------
  434.     /**
  435.      * @Route("/client/new/modal", name="client_new_modal", methods={"GET","POST"})
  436.      */
  437.     public function newModal(Request $request): Response
  438.     {
  439.         $client = new Client();
  440.         $form = $this->createForm(ClientType::class, $client);
  441.         $form->handleRequest($request);
  442.         if ($form->isSubmitted()) {
  443.             if ($form->isValid()) {
  444.                 $client->setUser($this->entrepriseResolver->getOwnerUser());
  445.                 $this->entityManager->persist($client);
  446.                 $this->entityManager->flush();
  447.                 $this->addFlash('success', 'Client ajouté avec succès');
  448.                 return $this->redirectToRoute('point_vente_index');
  449.             }
  450.             $this->addFlash('error', 'Veuillez corriger les erreurs dans le formulaire');
  451.         }
  452.         return $this->redirectToRoute('point_vente_index');
  453.     }
  454.     /**
  455.      * @Route("/new-from-caisse", name="app_produit_new_from_caisse", methods={"GET", "POST"})
  456.      */
  457.     public function newFromCaisse(Request $request, FileUploader $fileUploader): Response
  458.     {
  459.         $produit = new Produit();
  460.         $form = $this->createForm(ProduitCaisseType::class, $produit);
  461.         $form->handleRequest($request);
  462.         if ($form->isSubmitted() && $form->isValid()) {
  463.             $imageFile = $form->get('imageUrl')->getData();
  464.             if ($imageFile) {
  465.                 try {
  466.                     $produit->setImageUrl($fileUploader->upload($imageFile));
  467.                 } catch (\Exception $e) {
  468.                     $this->addFlash('error', 'Erreur lors de l\'upload de l\'image: ' . $e->getMessage());
  469.                     return $this->redirectToRoute('point_vente_index');
  470.                 }
  471.             }
  472.             $produit->setUser($this->entrepriseResolver->getOwnerUser());
  473.             $produit->setDateCommande(new \DateTime());
  474.             try {
  475.                 $this->entityManager->persist($produit);
  476.                 $this->entityManager->flush();
  477.                 $this->addFlash('success', 'Produit ajouté avec succès');
  478.                 return $this->redirectToRoute('point_vente_index');
  479.             } catch (\Exception $e) {
  480.                 $this->addFlash('error', 'Erreur lors de l\'enregistrement: ' . $e->getMessage());
  481.             }
  482.         }
  483.         return $this->render('caisse/produit/_caisse_form.html.twig', [
  484.             'form' => $form->createView(),
  485.         ]);
  486.     }
  487.     /**
  488.      * @Route("/categorie/new-from-caisse", name="app_categorie_new_from_caisse", methods={"GET", "POST"})
  489.      */
  490.     public function newCategorieFromCaisse(Request $request): Response
  491.     {
  492.         $categorie = new Categorie();
  493.         $form = $this->createForm(CategorieType::class, $categorie);
  494.         $form->handleRequest($request);
  495.         if ($form->isSubmitted() && $form->isValid()) {
  496.             // Associer l'utilisateur propriétaire (patron), même si créé par un salarié
  497.             $categorie->setUser($this->entrepriseResolver->getOwnerUser());
  498.             try {
  499.                 $this->entityManager->persist($categorie);
  500.                 $this->entityManager->flush();
  501.                 $this->addFlash('success', 'Catégorie ajoutée avec succès');
  502.                 return $this->redirectToRoute('point_vente_index');
  503.             } catch (\Exception $e) {
  504.                 $this->addFlash('error', 'Erreur lors de l\'enregistrement: ' . $e->getMessage());
  505.             }
  506.         }
  507.         return $this->render('caisse/categorie/_caisse_form.html.twig', [
  508.             'form' => $form->createView(),
  509.         ]);
  510.     }
  511. }