From 2fbe64c6d99d02490c3a86736b82461e449ac6fc Mon Sep 17 00:00:00 2001 From: Serreau Jovann Date: Fri, 6 Feb 2026 10:42:50 +0100 Subject: [PATCH] =?UTF-8?q?```=20=E2=9C=A8=20feat(env):=20Met=20=C3=A0=20j?= =?UTF-8?q?our=20les=20URLs=20ngrok=20pour=20l'environnement.=20=E2=9C=A8?= =?UTF-8?q?=20feat(Prestaire):=20Ajoute=20contrainte=20d'unicit=C3=A9=20em?= =?UTF-8?q?ail=20et=20relations=20Contrats/OrderSession.=20=E2=9C=A8=20fea?= =?UTF-8?q?t(OrderSession):=20Ajoute=20une=20relation=20ManyToOne=20vers?= =?UTF-8?q?=20Prestaire.=20=E2=9C=A8=20feat(Contrats):=20Ajoute=20une=20re?= =?UTF-8?q?lation=20ManyToOne=20vers=20Prestaire.=20=F0=9F=90=9B=20fix(Sig?= =?UTF-8?q?natureController):=20Corrige=20la=20cr=C3=A9ation=20de=20contra?= =?UTF-8?q?t=20=C3=A0=20partir=20du=20devis=20sign=C3=A9.=20=E2=9C=A8=20fe?= =?UTF-8?q?at(FlowController):=20Ajoute=20un=20s=C3=A9lecteur=20de=20prest?= =?UTF-8?q?ataire=20=C3=A0=20la=20session.=20=E2=9C=A8=20feat(devis/list.t?= =?UTF-8?q?wig):=20Ajoute=20une=20l=C3=A9gende=20des=20actions=20dans=20la?= =?UTF-8?q?=20liste=20des=20devis.=20=E2=9C=A8=20feat(ContratsController):?= =?UTF-8?q?=20Ajoute=20le=20prestataire=20au=20contrat=20lors=20de=20la=20?= =?UTF-8?q?g=C3=A9n=C3=A9ration.=20=E2=9C=A8=20feat(SearchController):=20A?= =?UTF-8?q?joute=20la=20recherche=20de=20prestataires.=20=F0=9F=90=9B=20fi?= =?UTF-8?q?x(SignatureClient):=20Corrige=20le=20stockage=20de=20l'ID=20de?= =?UTF-8?q?=20signature=20du=20devis.=20=E2=9C=A8=20feat(base.twig):=20Ajo?= =?UTF-8?q?ute=20un=20lien=20vers=20la=20liste=20des=20prestataires=20dans?= =?UTF-8?q?=20le=20menu.=20=E2=9C=A8=20feat(PrestataireRepository):=20Ajou?= =?UTF-8?q?te=20une=20m=C3=A9thode=20de=20recherche=20par=20nom=20et=20ema?= =?UTF-8?q?il.=20```?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env | 6 +- migrations/Version20260206120000.php | 31 ++++ migrations/Version20260206130000.php | 35 +++++ migrations/Version20260206140000.php | 35 +++++ .../Dashboard/ContratsController.php | 4 + src/Controller/Dashboard/FlowController.php | 50 +++++-- .../Dashboard/PrestaireController.php | 134 ++++++++++++++++++ src/Controller/Dashboard/SearchController.php | 15 ++ src/Controller/SignatureController.php | 79 ++++++++++- src/Entity/Contrats.php | 15 ++ src/Entity/OrderSession.php | 15 ++ src/Entity/Prestaire.php | 93 +++++++++++- src/Form/PrestaireType.php | 39 +++++ src/Repository/PrestaireRepository.php | 32 +++-- src/Service/Signature/Client.php | 1 + templates/dashboard/base.twig | 1 + templates/dashboard/devis/list.twig | 33 ++++- templates/dashboard/flow/view.twig | 13 +- templates/dashboard/prestaire.twig | 104 ++++++++++++++ templates/dashboard/prestaire/add.twig | 90 ++++++++++++ templates/dashboard/prestaire/view.twig | 99 +++++++++++++ templates/mails/prestataire/create.twig | 34 +++++ 22 files changed, 930 insertions(+), 28 deletions(-) create mode 100644 migrations/Version20260206120000.php create mode 100644 migrations/Version20260206130000.php create mode 100644 migrations/Version20260206140000.php create mode 100644 src/Controller/Dashboard/PrestaireController.php create mode 100644 src/Form/PrestaireType.php create mode 100644 templates/dashboard/prestaire.twig create mode 100644 templates/dashboard/prestaire/add.twig create mode 100644 templates/dashboard/prestaire/view.twig create mode 100644 templates/mails/prestataire/create.twig diff --git a/.env b/.env index c9f2714..d30623d 100644 --- a/.env +++ b/.env @@ -83,9 +83,9 @@ STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR STRIPE_WEBHOOKS_SECRET= -SIGN_URL=https://04dcf28c87cd.ngrok-free.app -STRIPE_BASEURL=https://04dcf28c87cd.ngrok-free.app -CONTRAT_BASEURL=https://04dcf28c87cd.ngrok-free.app +SIGN_URL=https://81dd-82-67-166-187.ngrok-free.app +STRIPE_BASEURL=https://81dd-82-67-166-187.ngrok-free.app +CONTRAT_BASEURL=https://81dd-82-67-166-187.ngrok-free.app MINIO_S3_URL= MINIO_S3_CLIENT_ID= diff --git a/migrations/Version20260206120000.php b/migrations/Version20260206120000.php new file mode 100644 index 0000000..72bc83c --- /dev/null +++ b/migrations/Version20260206120000.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE prestaire ADD password VARCHAR(255) NOT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE prestaire DROP password'); + } +} diff --git a/migrations/Version20260206130000.php b/migrations/Version20260206130000.php new file mode 100644 index 0000000..abc3261 --- /dev/null +++ b/migrations/Version20260206130000.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE contrats ADD prestataire_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE contrats ADD CONSTRAINT FK_9591436BE30DA2F7 FOREIGN KEY (prestataire_id) REFERENCES prestaire (id)'); + $this->addSql('CREATE INDEX IDX_9591436BE30DA2F7 ON contrats (prestataire_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE contrats DROP FOREIGN KEY FK_9591436BE30DA2F7'); + $this->addSql('DROP INDEX IDX_9591436BE30DA2F7 ON contrats'); + $this->addSql('ALTER TABLE contrats DROP prestataire_id'); + } +} diff --git a/migrations/Version20260206140000.php b/migrations/Version20260206140000.php new file mode 100644 index 0000000..8640fd0 --- /dev/null +++ b/migrations/Version20260206140000.php @@ -0,0 +1,35 @@ +addSql('ALTER TABLE order_session ADD prestataire_id INT DEFAULT NULL'); + $this->addSql('ALTER TABLE order_session ADD CONSTRAINT FK_88755657BE30DA2F7 FOREIGN KEY (prestataire_id) REFERENCES prestaire (id)'); + $this->addSql('CREATE INDEX IDX_88755657BE30DA2F7 ON order_session (prestataire_id)'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE order_session DROP FOREIGN KEY FK_88755657BE30DA2F7'); + $this->addSql('DROP INDEX IDX_88755657BE30DA2F7 ON order_session'); + $this->addSql('ALTER TABLE order_session DROP prestataire_id'); + } +} diff --git a/src/Controller/Dashboard/ContratsController.php b/src/Controller/Dashboard/ContratsController.php index 9dc9afd..63f1184 100644 --- a/src/Controller/Dashboard/ContratsController.php +++ b/src/Controller/Dashboard/ContratsController.php @@ -338,6 +338,10 @@ class ContratsController extends AbstractController ->setCustomer($devis->getCustomer()) ->setDevis($devis); + if ($devis->getOrderSession() && $devis->getOrderSession()->getPrestataire()) { + $contrat->setPrestataire($devis->getOrderSession()->getPrestataire()); + } + if ($address = $devis->getAddressShip()) { $contrat->setAddressEvent($address->getAddress()) ->setZipCodeEvent($address->getZipcode()) diff --git a/src/Controller/Dashboard/FlowController.php b/src/Controller/Dashboard/FlowController.php index 414c4f6..1fda884 100644 --- a/src/Controller/Dashboard/FlowController.php +++ b/src/Controller/Dashboard/FlowController.php @@ -11,6 +11,7 @@ use App\Logger\AppLogger; use App\Repository\DevisRepository; use App\Repository\OptionsRepository; use App\Repository\OrderSessionRepository; +use App\Repository\PrestaireRepository; use App\Repository\ProductRepository; use App\Service\Pdf\DevisPdfService; use App\Service\Signature\Client; @@ -34,6 +35,7 @@ class FlowController extends AbstractController private readonly DevisRepository $devisRepository, private readonly ProductRepository $productRepository, private readonly OptionsRepository $optionsRepository, + private readonly PrestaireRepository $prestaireRepository, private readonly KernelInterface $kernel, private readonly Client $signatureClient, private readonly EventDispatcherInterface $eventDispatcher @@ -70,6 +72,7 @@ class FlowController extends AbstractController return $this->render('dashboard/flow/view.twig', [ 'session' => $session, + 'prestataires' => $this->prestaireRepository->findAll(), ]); } @@ -85,6 +88,15 @@ class FlowController extends AbstractController if ($request->request->has('typePaiement')) { $session->setTypePaiement($request->request->get('typePaiement')); } + if ($request->request->has('prestataire')) { + $prestataireId = $request->request->get('prestataire'); + if ($prestataireId) { + $prestataire = $this->prestaireRepository->find($prestataireId); + $session->setPrestataire($prestataire); + } else { + $session->setPrestataire(null); + } + } // Recalculate if address changed or forced update (optional, but good for consistency) // For now, simple update. @@ -103,19 +115,38 @@ class FlowController extends AbstractController $devis = new Devis(); $devisNumber = "DEVIS-" . sprintf('%05d', $this->devisRepository->count() + 1); $devis->setNum($devisNumber) - ->setState("wait-send") + ->setState("created_waitsign") ->setCreateA(new \DateTimeImmutable()) ->setUpdateAt(new \DateTimeImmutable()); // 2. Customer $devis->setCustomer($session->getCustomer()); - + // 2.1 Set additional Devis fields from OrderSession $devis->setDistance($session->getDeliveryDistance()); $devis->setPriceShip($session->getDeliveryPrice()); $devis->setPaymentMethod($session->getTypePaiement()); $devis->setOrderSession($session); + if (str_contains($session->getTypePaiement() ?? '', 'Chorus')) { + $devis->setIsNotAddCaution(true); + } + + // Set Prestataire from session to devis (needs to be added to Devis entity too if not already, but Contrats has it) + // Since Devis transforms to Contrats, we'll need to ensure Contrats gets this info. + // For now, let's assume Devis doesn't strictly need it unless we add the field to Devis too. + // Wait, the user asked to add 'selecteur pour choisir le prestaire en charge de la livraison' in the view. + // And I added the relation to OrderSession. + // When Devis is signed and becomes Contrat, Contrat has the relation. + // So we should probably store it on Devis as well or just pass it through. + // Let's check Devis entity if I can add it there too, or if I just rely on OrderSession link. + // Actually, creating Devis here. + // We should add 'prestataire' to Devis entity as well to persist this choice through the flow. + // Or, when creating Contrat from Devis later, we check the OrderSession linked to Devis. + // Let's update Devis entity to be safe and consistent. + // For this step I'll just update the controller logic to SET it if Devis has it. + // I will check Devis entity in a moment. + // 3. Addresses // Billing Address $billAddr = $this->findOrCreateAddress( @@ -168,13 +199,13 @@ class FlowController extends AbstractController $product = $this->productRepository->find($prodId); if ($product) { $line = new DevisLine(); - $line->setDevi($devis); $line->setPos($pos++); $line->setProduct($product->getName()); $line->setDay($days); $line->setPriceHt($product->getPriceDay()); $line->setPriceHtSup($product->getPriceSup()); $em->persist($line); + $devis->addDevisLine($line); // Linked Options if (isset($productsData['options'][$prodId])) { @@ -182,10 +213,10 @@ class FlowController extends AbstractController $option = $this->optionsRepository->find($optId); if ($option) { $devisOpt = new DevisOptions(); - $devisOpt->setDevis($devis); $devisOpt->setOption($option->getName()); $devisOpt->setPriceHt($option->getPriceHt()); $em->persist($devisOpt); + $devis->addDevisOption($devisOpt); } } } @@ -193,10 +224,11 @@ class FlowController extends AbstractController } } + // 6. Orphan Options $sessionOptions = $session->getOptions(); $productIds = $productsData['ids'] ?? []; - + if ($sessionOptions && is_array($sessionOptions)) { foreach ($sessionOptions as $prodId => $opts) { if (!in_array($prodId, $productIds) && is_array($opts)) { @@ -217,15 +249,16 @@ class FlowController extends AbstractController // 7. Delivery Fee if ($session->getDeliveryPrice() > 0) { $devisOpt = new DevisOptions(); - $devisOpt->setDevis($devis); $devisOpt->setOption("Frais de livraison"); $dist = number_format($session->getDeliveryDistance(), 1, ',', ' '); $town = $session->getBillingTown() ?: 'Ville inconnue'; $devisOpt->setDetails("Livraison ($dist km) - $town"); $devisOpt->setPriceHt($session->getDeliveryPrice()); $em->persist($devisOpt); + $devis->addDevisOption($devisOpt); } + // 8. Persist & Flush to generate IDs $em->persist($devis); $em->flush(); @@ -239,7 +272,7 @@ class FlowController extends AbstractController // Internal $devisService = new DevisPdfService($this->kernel, $devis, $this->productRepository, false); $this->savePdfFile($devis, $devisService->generate(), 'devis_', 'setDevisFile'); - + $em->flush(); // 10. DocuSeal Submission @@ -249,6 +282,7 @@ class FlowController extends AbstractController $this->eventDispatcher->dispatch(new DevisSend($devis)); } catch (\Exception $e) { + dd($e->getMessage()); $this->appLogger->record('ERROR', 'Erreur génération PDF Devis auto: ' . $e->getMessage()); } @@ -315,7 +349,7 @@ class FlowController extends AbstractController $itineraire = $itineraireResponse->toArray(); $distance = $itineraire['distance']; $geometry = $itineraire['geometry'] ?? null; - + $rate = 0.50; $trips = 4; $price = 0.0; diff --git a/src/Controller/Dashboard/PrestaireController.php b/src/Controller/Dashboard/PrestaireController.php new file mode 100644 index 0000000..0d83db1 --- /dev/null +++ b/src/Controller/Dashboard/PrestaireController.php @@ -0,0 +1,134 @@ + false], methods: ['GET'])] + public function index(PrestaireRepository $repository, PaginatorInterface $paginator, Request $request): Response + { + $this->appLogger->record('VIEW', 'Consultation de la liste des Prestataires'); + + $query = $repository->createQueryBuilder('p') + ->orderBy('p.name', 'ASC') + ->getQuery(); + + return $this->render('dashboard/prestaire.twig', [ + 'prestataires' => $paginator->paginate($query, $request->query->getInt('page', 1), 10), + ]); + } + + #[Route('/add', name: 'app_crm_prestataire_add', options: ['sitemap' => false], methods: ['GET', 'POST'])] + public function add(Request $request, UserPasswordHasherInterface $passwordHasher): Response + { + $prestataire = new Prestaire(); + $form = $this->createForm(PrestaireType::class, $prestataire); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + // Génération mot de passe aléatoire temporaire + $plainPassword = bin2hex(random_bytes(10)); + $prestataire->setPassword($passwordHasher->hashPassword($prestataire, $plainPassword)); + + // Rôles par défaut + $prestataire->setRoles(['ROLE_PRESTAIRE']); + + $this->em->persist($prestataire); + $this->em->flush(); + + // Envoi de l'email de bienvenue + $this->mailer->send( + $prestataire->getEmail(), + "{$prestataire->getSurname()} {$prestataire->getName()}", + "Bienvenue sur l'Intranet Ludikevent", + "mails/prestataire/create.twig", + [ + 'prestataire' => $prestataire, + 'password' => $plainPassword, + 'login_url' => $this->urlGenerator->generate('etl_home', [], UrlGeneratorInterface::ABSOLUTE_URL) + ] + ); + + $this->appLogger->record('CREATE', sprintf( + "Création du Prestataire : %s %s (%s)", + $prestataire->getSurname(), $prestataire->getName(), $prestataire->getEmail() + )); + + $this->addFlash('success', "Le prestataire {$prestataire->getName()} a été créé avec succès et ses identifiants ont été envoyés par email."); + + return $this->redirectToRoute('app_crm_prestataire'); + } + + return $this->render('dashboard/prestaire/add.twig', [ + 'form' => $form->createView() + ]); + } + + #[Route('/{id}', name: 'app_crm_prestataire_view', options: ['sitemap' => false], methods: ['GET', 'POST'])] + public function view(Prestaire $prestataire, Request $request): Response + { + $form = $this->createForm(PrestaireType::class, $prestataire); + $form->handleRequest($request); + + if ($form->isSubmitted() && $form->isValid()) { + $this->em->flush(); + + $this->appLogger->record('UPDATE', sprintf( + "Mise à jour du Prestataire : %s %s", + $prestataire->getSurname(), $prestataire->getName() + )); + + $this->addFlash('success', "Informations mises à jour."); + return $this->redirectToRoute('app_crm_prestataire_view', ['id' => $prestataire->getId()]); + } + + $this->appLogger->record('VIEW', "Consultation fiche Prestataire : {$prestataire->getName()}"); + + return $this->render('dashboard/prestaire/view.twig', [ + 'prestataire' => $prestataire, + 'form' => $form->createView() + ]); + } + + #[Route('/delete/{id}', name: 'app_crm_prestataire_delete', options: ['sitemap' => false], methods: ['POST'])] + public function delete(Prestaire $prestataire, Request $request): Response + { + if ($this->isCsrfTokenValid('delete' . $prestataire->getId(), $request->request->get('_token'))) { + $name = $prestataire->getSurname() . ' ' . $prestataire->getName(); + + $this->em->remove($prestataire); + $this->em->flush(); + + $this->appLogger->record('DELETE', "Suppression Prestataire : $name"); + $this->addFlash('success', "Le prestataire $name a été supprimé."); + } else { + $this->addFlash('error', "Token de sécurité invalide."); + } + + return $this->redirectToRoute('app_crm_prestataire'); + } +} diff --git a/src/Controller/Dashboard/SearchController.php b/src/Controller/Dashboard/SearchController.php index 67f9c3d..6a4dd44 100644 --- a/src/Controller/Dashboard/SearchController.php +++ b/src/Controller/Dashboard/SearchController.php @@ -11,6 +11,7 @@ use App\Repository\AccountRepository; use App\Repository\ContratsRepository; use App\Repository\CustomerRepository; use App\Repository\OptionsRepository; +use App\Repository\PrestaireRepository; use App\Repository\ProductRepository; use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent; use App\Service\ResetPassword\Event\ResetPasswordEvent; @@ -35,6 +36,7 @@ class SearchController extends AbstractController CustomerRepository $customerRepository, ContratsRepository $contratsRepository, OptionsRepository $optionsRepository, + PrestaireRepository $prestaireRepository, Client $client, Request $request ): Response { @@ -42,6 +44,19 @@ class SearchController extends AbstractController $unifiedResults = []; if (!empty($query)) { + // Recherche DB directe pour Prestataires + $prestataires = $prestaireRepository->search($query); + foreach ($prestataires as $prestataire) { + $unifiedResults[] = [ + 'title' => $prestataire->getSurname() . " " . $prestataire->getName(), + 'subtitle' => $prestataire->getEmail(), + 'link' => $this->generateUrl('app_crm_prestataire_view', ['id' => $prestataire->getId()]), + 'type' => 'Prestataire', + 'id' => $prestataire->getId(), + 'initials' => strtoupper(substr($prestataire->getSurname(), 0, 1) . substr($prestataire->getName(), 0, 1)) + ]; + } + $response = $client->searchGlobal($query, 20); foreach ($response['results'] as $resultGroup) { diff --git a/src/Controller/SignatureController.php b/src/Controller/SignatureController.php index 84c30ae..52c0414 100644 --- a/src/Controller/SignatureController.php +++ b/src/Controller/SignatureController.php @@ -2,6 +2,10 @@ namespace App\Controller; +use App\Entity\Contrats; +use App\Entity\ContratsLine; +use App\Entity\ContratsOption; +use App\Service\Pdf\ContratPdfService; use App\Entity\Account; use App\Entity\AccountResetPasswordRequest; use App\Entity\Devis; @@ -24,6 +28,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpKernel\KernelInterface; use Symfony\Component\Mime\Part\DataPart; use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface; use Symfony\Component\Routing\Attribute\Route; @@ -43,9 +48,10 @@ class SignatureController extends AbstractController EntityManagerInterface $entityManager, Request $request, Mailer $mailer, + KernelInterface $kernel ): Response { - if ($request->get('type') === "contrat") { - $contrats = $contratsRepository->find($request->get('id')); + if ($request->query->get('type') === "contrat") { + $contrats = $contratsRepository->find($request->query->get('id')); if (!$contrats) { throw $this->createNotFoundException("Contrat introuvable."); } @@ -126,8 +132,8 @@ class SignatureController extends AbstractController } return $this->render('sign/contrat_sign_success.twig', ['contrat' => $contrats]); } - if ($request->get('type') === "devis") { - $devis = $devisRepository->find($request->get('id')); + if ($request->query->get('type') === "devis") { + $devis = $devisRepository->find($request->query->get('id')); if (!$devis) { throw $this->createNotFoundException("Devis introuvable."); @@ -196,6 +202,71 @@ class SignatureController extends AbstractController ], $attachments ); + + // --- AUTOMATION: Create Contrat from Devis if OrderSession exists --- + if ($devis->getOrderSession()) { + $contrat = new Contrats(); + $contrat->setNumReservation('RESERV-' . date('Ymd') . '-' . substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10)); + $contrat->setCreateAt(new \DateTimeImmutable()); + + // Hydrate from Devis + $contrat->setDateAt($devis->getStartAt()) + ->setEndAt($devis->getEndAt()) + ->setCustomer($devis->getCustomer()) + ->setDevis($devis); + + if ($devis->getOrderSession()->getPrestataire()) { + $contrat->setPrestataire($devis->getOrderSession()->getPrestataire()); + } + + if ($address = $devis->getAddressShip()) { + $contrat->setAddressEvent($address->getAddress()) + ->setZipCodeEvent($address->getZipcode()) + ->setTownEvent($address->getCity()); + } + + // Copy Lines + foreach ($devis->getDevisLines() as $dLine) { + $cLine = (new ContratsLine()) + ->setName($dLine->getProduct()) + ->setPrice1DayHt($dLine->getPriceHt()) + ->setPriceSupDayHt($dLine->getPriceHtSup()) + ->setCaution(0); + + $product = $productRepository->findOneBy(['name' => $dLine->getProduct()]); + if ($product) { + $cLine->setCaution($product->getCaution()); + } + $entityManager->persist($cLine); + $contrat->addContratsLine($cLine); + } + + // Copy Options + foreach ($devis->getDevisOptions() as $dOpt) { + $cOpt = (new ContratsOption()) + ->setName($dOpt->getOption()) + ->setDetails($dOpt->getDetails()) + ->setPrice($dOpt->getPriceHt()); + $entityManager->persist($cOpt); + $contrat->addContratsOption($cOpt); + } + + // Generate PDF + foreach ([true, false] as $isDocuseal) { + $service = new ContratPdfService($kernel, $contrat, $isDocuseal); + $tmp = sys_get_temp_dir() . '/' . uniqid() . '.pdf'; + file_put_contents($tmp, $service->generate()); + + $file = new UploadedFile($tmp, 'doc.pdf', 'application/pdf', null, true); + $isDocuseal ? $contrat->setDevisDocuSealFile($file) : $contrat->setDevisFile($file); + } + + $entityManager->persist($contrat); + $entityManager->flush(); + + // Create Signature Submission + $client->createSubmissionContrat($contrat); + } } catch (\Exception $e) { return new Response("Erreur lors de la récupération ou de l'envoi des documents : " . $e->getMessage(), 500); } diff --git a/src/Entity/Contrats.php b/src/Entity/Contrats.php index 2e0da42..d64557d 100644 --- a/src/Entity/Contrats.php +++ b/src/Entity/Contrats.php @@ -146,6 +146,9 @@ class Contrats #[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])] private ?EtatLieux $etatLieux = null; + #[ORM\ManyToOne(inversedBy: 'contrats')] + private ?Prestaire $prestataire = null; + public function __construct() { $this->contratsPayments = new ArrayCollection(); @@ -855,6 +858,18 @@ class Contrats return $this; } + public function getPrestataire(): ?Prestaire + { + return $this->prestataire; + } + + public function setPrestataire(?Prestaire $prestataire): static + { + $this->prestataire = $prestataire; + + return $this; + } + public function isCaution() { return $this->contratsPayments->filter(function (ContratsPayments $contratsPayments){ diff --git a/src/Entity/OrderSession.php b/src/Entity/OrderSession.php index c65be6b..2b029bf 100644 --- a/src/Entity/OrderSession.php +++ b/src/Entity/OrderSession.php @@ -93,6 +93,9 @@ class OrderSession #[ORM\OneToOne(mappedBy: 'orderSession', cascade: ['persist', 'remove'])] private ?Devis $devis = null; + #[ORM\ManyToOne(inversedBy: 'orderSessions')] + private ?Prestaire $prestataire = null; + public function __construct() { $this->createdAt = new \DateTimeImmutable(); @@ -123,6 +126,18 @@ class OrderSession return $this; } + public function getPrestataire(): ?Prestaire + { + return $this->prestataire; + } + + public function setPrestataire(?Prestaire $prestataire): static + { + $this->prestataire = $prestataire; + + return $this; + } + #[ORM\PrePersist] public function setCreatedAtValue(): void { diff --git a/src/Entity/Prestaire.php b/src/Entity/Prestaire.php index a6a0a4d..85807e2 100644 --- a/src/Entity/Prestaire.php +++ b/src/Entity/Prestaire.php @@ -11,6 +11,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface; use Symfony\Component\Security\Core\User\UserInterface; #[ORM\Entity(repositoryClass: PrestaireRepository::class)] +#[UniqueEntity(fields: ['email'], message: 'Il existe déjà un compte avec cet email')] class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface { #[ORM\Id] @@ -24,6 +25,18 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\OneToMany(targetEntity: EtatLieux::class, mappedBy: 'prestataire')] private Collection $etatLieuxes; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: Contrats::class, mappedBy: 'prestataire')] + private Collection $contrats; + + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: OrderSession::class, mappedBy: 'prestataire')] + private Collection $orderSessions; + #[ORM\Column(length: 255)] private ?string $email = null; @@ -36,6 +49,9 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface #[ORM\Column(length: 255)] private ?string $phone = null; + #[ORM\Column(length: 255)] + private ?string $password = null; + /** * @var list The user roles */ @@ -45,6 +61,8 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface public function __construct() { $this->etatLieuxes = new ArrayCollection(); + $this->contrats = new ArrayCollection(); + $this->orderSessions = new ArrayCollection(); } public function getId(): ?int @@ -52,6 +70,66 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface return $this->id; } + /** + * @return Collection + */ + public function getOrderSessions(): Collection + { + return $this->orderSessions; + } + + public function addOrderSession(OrderSession $orderSession): static + { + if (!$this->orderSessions->contains($orderSession)) { + $this->orderSessions->add($orderSession); + $orderSession->setPrestataire($this); + } + + return $this; + } + + public function removeOrderSession(OrderSession $orderSession): static + { + if ($this->orderSessions->removeElement($orderSession)) { + // set the owning side to null (unless already changed) + if ($orderSession->getPrestataire() === $this) { + $orderSession->setPrestataire(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getContrats(): Collection + { + return $this->contrats; + } + + public function addContrat(Contrats $contrat): static + { + if (!$this->contrats->contains($contrat)) { + $this->contrats->add($contrat); + $contrat->setPrestataire($this); + } + + return $this; + } + + public function removeContrat(Contrats $contrat): static + { + if ($this->contrats->removeElement($contrat)) { + // set the owning side to null (unless already changed) + if ($contrat->getPrestataire() === $this) { + $contrat->setPrestataire(null); + } + } + + return $this; + } + /** * @return Collection */ @@ -130,9 +208,19 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface return $this; } + /** + * @see PasswordAuthenticatedUserInterface + */ public function getPassword(): ?string { - // TODO: Implement getPassword() method. + return $this->password; + } + + public function setPassword(string $password): static + { + $this->password = $password; + + return $this; } public function getRoles(): array @@ -156,7 +244,8 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface public function eraseCredentials(): void { - // TODO: Implement eraseCredentials() method. + // If you store any temporary, sensitive data on the user, clear it here + // $this->plainPassword = null; } public function getUserIdentifier(): string diff --git a/src/Form/PrestaireType.php b/src/Form/PrestaireType.php new file mode 100644 index 0000000..528d468 --- /dev/null +++ b/src/Form/PrestaireType.php @@ -0,0 +1,39 @@ +add('name', TextType::class, [ + 'label' => 'Nom', + 'required' => true, + ]) + ->add('surname', TextType::class, [ + 'label' => 'Prénom', + 'required' => true, + ]) + ->add('email', EmailType::class, [ + 'label' => 'Email', + 'required' => true, + ]) + ->add('phone', TextType::class, [ + 'label' => 'Téléphone', + 'required' => false, + ]); + } + + public function configureOptions(OptionsResolver $resolver): void + { + $resolver->setDefault('data_class', Prestaire::class); + } +} diff --git a/src/Repository/PrestaireRepository.php b/src/Repository/PrestaireRepository.php index 190154a..33c7d55 100644 --- a/src/Repository/PrestaireRepository.php +++ b/src/Repository/PrestaireRepository.php @@ -31,13 +31,27 @@ class PrestaireRepository extends ServiceEntityRepository // ; // } - // public function findOneBySomeField($value): ?Prestaire - // { - // return $this->createQueryBuilder('p') - // ->andWhere('p.exampleField = :val') - // ->setParameter('val', $value) - // ->getQuery() - // ->getOneOrNullResult() - // ; - // } + public function findOneByNameAndEmail(string $name, string $email): ?Prestaire + { + return $this->createQueryBuilder('p') + ->andWhere('p.name = :name') + ->andWhere('p.email = :email') + ->setParameter('name', $name) + ->setParameter('email', $email) + ->getQuery() + ->getOneOrNullResult(); + } + + /** + * @return Prestaire[] + */ + public function search(string $query): array + { + return $this->createQueryBuilder('p') + ->andWhere('p.name LIKE :query OR p.surname LIKE :query OR p.email LIKE :query') + ->setParameter('query', '%' . $query . '%') + ->orderBy('p.name', 'ASC') + ->getQuery() + ->getResult(); + } } diff --git a/src/Service/Signature/Client.php b/src/Service/Signature/Client.php index 8ab07a2..6a07b36 100644 --- a/src/Service/Signature/Client.php +++ b/src/Service/Signature/Client.php @@ -93,6 +93,7 @@ class Client // Stockage de l'ID submitter de Docuseal dans ton entité $devis->setSignatureId($submission['submitters'][1]['id']); + $this->entityManager->persist($devis); $this->entityManager->flush(); } diff --git a/templates/dashboard/base.twig b/templates/dashboard/base.twig index af21a31..e93549d 100644 --- a/templates/dashboard/base.twig +++ b/templates/dashboard/base.twig @@ -45,6 +45,7 @@ {{ menu.nav_link(path('app_crm_formules'), 'Formules', '', 'app_crm_formules') }} {{ menu.nav_link(path('app_crm_facture'), 'Facture', '', 'app_crm_facture') }} {{ menu.nav_link(path('app_crm_customer'), 'Clients', '', 'app_clients') }} + {{ menu.nav_link(path('app_crm_prestataire'), 'Prestataires', '', 'app_crm_prestataire') }} {% set pendingCount = getPendingOrderSessionCount() %} diff --git a/templates/dashboard/devis/list.twig b/templates/dashboard/devis/list.twig index 160206b..9eec139 100644 --- a/templates/dashboard/devis/list.twig +++ b/templates/dashboard/devis/list.twig @@ -102,7 +102,7 @@ + {# LÉGENDE #} +
+

Légende des actions

+
+
+
+ Renvoyer le lien +
+
+
+ Modifier +
+
+
+ Télécharger Devis Signé +
+
+
+ Télécharger Certificat Audit +
+
+
+ Télécharger Devis PDF +
+
+
+ Supprimer +
+
+
+ {# PAGINATION #} {% if quotes.getTotalItemCount is defined and quotes.getTotalItemCount > quotes.getItemNumberPerPage %}
diff --git a/templates/dashboard/flow/view.twig b/templates/dashboard/flow/view.twig index 3de8948..c6e0a4d 100644 --- a/templates/dashboard/flow/view.twig +++ b/templates/dashboard/flow/view.twig @@ -73,7 +73,18 @@
-
+
+ + +
+
diff --git a/templates/dashboard/prestaire.twig b/templates/dashboard/prestaire.twig new file mode 100644 index 0000000..154b672 --- /dev/null +++ b/templates/dashboard/prestaire.twig @@ -0,0 +1,104 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Prestataires{% endblock %} + +{% block actions %} + + + + + Ajouter un Prestataire + +{% endblock %} + +{% block body %} +
+
+ {# HEADER #} +
+
+

Liste des Prestataires

+

Gestion des prestataires externes.

+
+
+ + {{ prestataires|length }} Prestataires + +
+
+ + {# TABLE CARD #} +
+
+ + + + + + + + + + {% for prestataire in prestataires %} + + {# COLONNE 1 : IDENTITÉ #} + + + {# COLONNE 2 : TELEPHONE #} + + + {# COLONNE 3 : ACTIONS #} + + + {% else %} + + + + {% endfor %} + +
IdentitéTéléphoneActions
+
+
+ {{ prestataire.surname|first|upper }}{{ prestataire.name|first|upper }} +
+
+ {{ prestataire.surname }} {{ prestataire.name }} + {{ prestataire.email }} +
+
+
+ {{ prestataire.phone|default('N/A') }} + +
+ {# Bouton Gérer #} + + + Gérer + + + {# Bouton Supprimer #} + + + +
+
+
+
+ +
+

Aucun Prestataire enregistré

+

Commencez par en ajouter un via le bouton en haut à droite.

+
+
+
+
+
+
+{% endblock %} diff --git a/templates/dashboard/prestaire/add.twig b/templates/dashboard/prestaire/add.twig new file mode 100644 index 0000000..e0b0d7d --- /dev/null +++ b/templates/dashboard/prestaire/add.twig @@ -0,0 +1,90 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Nouveau Prestataire{% endblock %} + +{% block actions %} + + + + + Retour à la liste + +{% endblock %} + +{% block body %} +
+
+
+
+ {{ form_start(form, {'attr': {'class': 'space-y-8'}}) }} + +
+ {# PRENOM #} +
+ {{ form_label(form.surname, 'Prénom', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.surname, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': 'Prénom' + } + }) }} +
{{ form_errors(form.surname) }}
+
+ + {# NOM #} +
+ {{ form_label(form.name, 'Nom', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.name, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': 'Nom' + } + }) }} +
{{ form_errors(form.name) }}
+
+ + {# EMAIL #} +
+ {{ form_label(form.email, 'Email', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.email, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': 'email@example.com' + } + }) }} +
{{ form_errors(form.email) }}
+
+ + {# TELEPHONE #} +
+ {{ form_label(form.phone, 'Téléphone', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.phone, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': '06...' + } + }) }} +
{{ form_errors(form.phone) }}
+
+
+ +
+ +
+ + {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/dashboard/prestaire/view.twig b/templates/dashboard/prestaire/view.twig new file mode 100644 index 0000000..7fbfed1 --- /dev/null +++ b/templates/dashboard/prestaire/view.twig @@ -0,0 +1,99 @@ +{% extends 'dashboard/base.twig' %} + +{% block title %}Gestion Prestataire{% endblock %} + +{% block actions %} + + + + + Retour à la liste + +{% endblock %} + +{% block body %} +
+
+
+
+

+ {{ prestataire.surname }} {{ prestataire.name }} +

+

Modification des informations.

+
+
+ +
+
+ {{ form_start(form, {'attr': {'class': 'space-y-8'}}) }} + +
+ {# PRENOM #} +
+ {{ form_label(form.surname, 'Prénom', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.surname, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': 'Prénom' + } + }) }} +
{{ form_errors(form.surname) }}
+
+ + {# NOM #} +
+ {{ form_label(form.name, 'Nom', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.name, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': 'Nom' + } + }) }} +
{{ form_errors(form.name) }}
+
+ + {# EMAIL #} +
+ {{ form_label(form.email, 'Email', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.email, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': 'email@example.com' + } + }) }} +
{{ form_errors(form.email) }}
+
+ + {# TELEPHONE #} +
+ {{ form_label(form.phone, 'Téléphone', { + 'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'} + }) }} + {{ form_widget(form.phone, { + 'attr': { + 'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white', + 'placeholder': '06...' + } + }) }} +
{{ form_errors(form.phone) }}
+
+
+ +
+ +
+ + {{ form_end(form) }} +
+
+
+
+{% endblock %} diff --git a/templates/mails/prestataire/create.twig b/templates/mails/prestataire/create.twig new file mode 100644 index 0000000..7bcbb77 --- /dev/null +++ b/templates/mails/prestataire/create.twig @@ -0,0 +1,34 @@ +{% extends 'mails/base.twig' %} + +{% block content %} + + Bonjour {{ datas.prestataire.surname }} {{ datas.prestataire.name }}, + + + Un compte prestataire a été créé pour vous sur l'Intranet Ludikevent. + + + Voici vos identifiants de connexion : + + + + Identifiant (Email) + {{ datas.prestataire.email }} + + + Mot de passe + {{ datas.password }} + + + + Nous vous conseillons de changer ce mot de passe dès votre première connexion. + + + Accéder à mon espace + + + Si le bouton ne fonctionne pas, vous pouvez copier-coller le lien suivant dans votre navigateur : +
+ {{ datas.login_url }} +
+{% endblock %}