diff --git a/.env b/.env index 85aa473..a86f994 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://c55e-82-67-166-187.ngrok-free.app -STRIPE_BASEURL=https://c55e-82-67-166-187.ngrok-free.app -CONTRAT_BASEURL=https://c55e-82-67-166-187.ngrok-free.app +SIGN_URL=https://eb44-82-67-166-187.ngrok-free.app +STRIPE_BASEURL=https://eb44-82-67-166-187.ngrok-free.app +CONTRAT_BASEURL=https://eb44-82-67-166-187.ngrok-free.app MINIO_S3_URL= MINIO_S3_CLIENT_ID= diff --git a/migrations/Version20260211170445.php b/migrations/Version20260211170445.php new file mode 100644 index 0000000..de0deb2 --- /dev/null +++ b/migrations/Version20260211170445.php @@ -0,0 +1,34 @@ +addSql('CREATE TABLE etat_lieux_return_comment (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, content TEXT NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, etat_lieux_id INT NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_5503D02A3F1DAE3C ON etat_lieux_return_comment (etat_lieux_id)'); + $this->addSql('ALTER TABLE etat_lieux_return_comment ADD CONSTRAINT FK_5503D02A3F1DAE3C FOREIGN KEY (etat_lieux_id) REFERENCES etat_lieux (id) NOT DEFERRABLE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE etat_lieux_return_comment DROP CONSTRAINT FK_5503D02A3F1DAE3C'); + $this->addSql('DROP TABLE etat_lieux_return_comment'); + } +} diff --git a/migrations/Version20260212074558.php b/migrations/Version20260212074558.php new file mode 100644 index 0000000..6bf2ef6 --- /dev/null +++ b/migrations/Version20260212074558.php @@ -0,0 +1,39 @@ +addSql('CREATE TABLE etat_lieux_return_point_control (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, status BOOLEAN DEFAULT false NOT NULL, details TEXT DEFAULT NULL, etat_lieux_id INT NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_2EADBB563F1DAE3C ON etat_lieux_return_point_control (etat_lieux_id)'); + $this->addSql('CREATE TABLE etat_return_lieux_file (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, file_name VARCHAR(255) DEFAULT NULL, file_size INT DEFAULT NULL, mime_type VARCHAR(255) DEFAULT NULL, type VARCHAR(50) NOT NULL, created_at TIMESTAMP(0) WITHOUT TIME ZONE NOT NULL, etat_lieux_id INT NOT NULL, PRIMARY KEY (id))'); + $this->addSql('CREATE INDEX IDX_2E89E9D43F1DAE3C ON etat_return_lieux_file (etat_lieux_id)'); + $this->addSql('ALTER TABLE etat_lieux_return_point_control ADD CONSTRAINT FK_2EADBB563F1DAE3C FOREIGN KEY (etat_lieux_id) REFERENCES etat_lieux (id) NOT DEFERRABLE'); + $this->addSql('ALTER TABLE etat_return_lieux_file ADD CONSTRAINT FK_2E89E9D43F1DAE3C FOREIGN KEY (etat_lieux_id) REFERENCES etat_lieux (id) NOT DEFERRABLE'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE etat_lieux_return_point_control DROP CONSTRAINT FK_2EADBB563F1DAE3C'); + $this->addSql('ALTER TABLE etat_return_lieux_file DROP CONSTRAINT FK_2E89E9D43F1DAE3C'); + $this->addSql('DROP TABLE etat_lieux_return_point_control'); + $this->addSql('DROP TABLE etat_return_lieux_file'); + } +} diff --git a/migrations/Version20260212090115.php b/migrations/Version20260212090115.php new file mode 100644 index 0000000..a17a9e2 --- /dev/null +++ b/migrations/Version20260212090115.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE etat_lieux ADD raison_refused TEXT DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE etat_lieux DROP raison_refused'); + } +} diff --git a/src/Controller/EtlController.php b/src/Controller/EtlController.php index cbb9ef5..83b6e01 100644 --- a/src/Controller/EtlController.php +++ b/src/Controller/EtlController.php @@ -9,6 +9,9 @@ use App\Entity\EtatLieux; use App\Entity\EtatLieuxComment; use App\Entity\EtatLieuxFile; use App\Entity\EtatLieuxPointControl; +use App\Entity\EtatLieuxReturnComment; +use App\Entity\EtatLieuxReturnPointControl; +use App\Entity\EtatReturnLieuxFile; use App\Entity\ProductPointControll; use App\Entity\Prestaire; use App\Form\PrestairePasswordType; @@ -461,7 +464,12 @@ class EtlController extends AbstractController $content = $request->request->get('content'); if ($content) { $etatLieux = $contrat->getEtatLieux(); - $comment = new EtatLieuxComment(); + if($etatLieux->getStatus() == "return_edl_progress") { + $comment = new EtatLieuxReturnComment(); + } else { + $comment = new EtatLieuxComment(); + } + $comment->setContent($content); $comment->setEtatLieux($etatLieux); $em->persist($comment); @@ -494,22 +502,42 @@ class EtlController extends AbstractController if ($productPoint) { $existing = null; - foreach ($etatLieux->getPointControls() as $ep) { - if ($ep->getName() === $productPoint->getName()) { - $existing = $ep; - break; + if($etatLieux->getStatus() == "return_edl_progress") { + foreach ($etatLieux->getPointControlsReturn() as $ep) { + if ($ep->getName() === $productPoint->getName()) { + $existing = $ep; + break; + } } + + if (!$existing) { + $existing = new EtatLieuxReturnPointControl(); + $existing->setName($productPoint->getName()); + $existing->setEtatLieux($etatLieux); + $em->persist($existing); + } + + $existing->setStatus(isset($values['status'])); + $existing->setDetails($values['details'] ?? null); + } else { + foreach ($etatLieux->getPointControls() as $ep) { + if ($ep->getName() === $productPoint->getName()) { + $existing = $ep; + break; + } + } + + if (!$existing) { + $existing = new EtatLieuxPointControl(); + $existing->setName($productPoint->getName()); + $existing->setEtatLieux($etatLieux); + $em->persist($existing); + } + + $existing->setStatus(isset($values['status'])); + $existing->setDetails($values['details'] ?? null); } - if (!$existing) { - $existing = new EtatLieuxPointControl(); - $existing->setName($productPoint->getName()); - $existing->setEtatLieux($etatLieux); - $em->persist($existing); - } - - $existing->setStatus(isset($values['status'])); - $existing->setDetails($values['details'] ?? null); } } } @@ -533,17 +561,29 @@ class EtlController extends AbstractController $etatLieux = $contrat->getEtatLieux(); $hasFiles = false; + if ($photos) { if (!is_array($photos)) $photos = [$photos]; foreach ($photos as $uploadedFile) { if ($uploadedFile instanceof UploadedFile) { $this->compressImage($uploadedFile); - $file = new EtatLieuxFile(); - $file->setFile($uploadedFile); - $file->setType('photo'); - $file->setEtatLieux($etatLieux); - $em->persist($file); - $hasFiles = true; + + if($etatLieux->getStatus() == "return_edl_progress") { + $file = new EtatReturnLieuxFile(); + $file->setFile($uploadedFile); + $file->setType('photo'); + $file->setEtatLieux($etatLieux); + $em->persist($file); + $hasFiles = true; + } else { + $file = new EtatLieuxFile(); + $file->setFile($uploadedFile); + $file->setType('photo'); + $file->setEtatLieux($etatLieux); + $em->persist($file); + $hasFiles = true; + } + } } } @@ -552,12 +592,22 @@ class EtlController extends AbstractController foreach ($videos as $uploadedFile) { if ($uploadedFile instanceof UploadedFile) { $this->compressVideo($uploadedFile); - $file = new EtatLieuxFile(); - $file->setFile($uploadedFile); - $file->setType('video'); - $file->setEtatLieux($etatLieux); - $em->persist($file); - $hasFiles = true; + if($etatLieux->getStatus() == "return_edl_progress") { + $file = new EtatReturnLieuxFile(); + $file->setFile($uploadedFile); + $file->setType('video'); + $file->setEtatLieux($etatLieux); + $em->persist($file); + $hasFiles = true; + } else { + $file = new EtatLieuxFile(); + $file->setFile($uploadedFile); + $file->setType('video'); + $file->setEtatLieux($etatLieux); + $em->persist($file); + $hasFiles = true; + } + } } } @@ -587,8 +637,11 @@ class EtlController extends AbstractController $etatLieux = $contrat->getEtatLieux(); + if($etatLieux->getStatus() == "return_edl_progress") { + $file = $em->getRepository(EtatReturnLieuxFile::class)->find($fileId); + } else { $file = $em->getRepository(EtatLieuxFile::class)->find($fileId); - +} if ($file && $file->getEtatLieux() === $etatLieux) { @@ -615,11 +668,19 @@ class EtlController extends AbstractController } $etatLieux = $contrat->getEtatLieux(); - if ($etatLieux) { - $etatLieux->setStatus('edl_done'); - $this->generateAndSendToDocuSeal($contrat, $em, $kernel, $signatureClient); + if($etatLieux->getStatus() == "return_edl_progress") { + $etatLieux->setStatus('edl_return_done'); + $this->generateAndSendToDocuSealReturn($contrat, $em, $kernel, $signatureClient); $this->addFlash('success', 'État des lieux terminé et PDF généré.'); return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $contrat->getId()]); + } else { + if ($etatLieux) { + $etatLieux->setStatus('edl_done'); + $this->generateAndSendToDocuSeal($contrat, $em, $kernel, $signatureClient); + + $this->addFlash('success', 'État des lieux terminé et PDF généré.'); + return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $contrat->getId()]); + } } return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]); @@ -655,10 +716,16 @@ class EtlController extends AbstractController return $this->redirectToRoute('etl_login'); } - $this->generateAndSendToDocuSeal($contrat, $em, $kernel, $signatureClient); + if($contrat->getEtatLieux()->getStatus() == "edl_return_done") { + $this->generateAndSendToDocuSealReturn($contrat, $em, $kernel, $signatureClient); + $etatLieux = $contrat->getEtatLieux(); + $path = $uploaderHelper->asset($etatLieux, 'etatLieuxUnsignReturnFile'); + } else { + $this->generateAndSendToDocuSeal($contrat, $em, $kernel, $signatureClient); + $etatLieux = $contrat->getEtatLieux(); + $path = $uploaderHelper->asset($etatLieux, 'etatLieuxUnsignFile'); + } - $etatLieux = $contrat->getEtatLieux(); - $path = $uploaderHelper->asset($etatLieux, 'etatLieuxUnsignFile'); return new RedirectResponse($path); } @@ -703,23 +770,40 @@ class EtlController extends AbstractController $providerSigned = false; $customerSigned = false; + if($contrat->getEtatLieux()->getStatus() == "edl_return_done") { + if ($etatLieux->getSignIdReturn()) { + try { + $sub = $signatureClient->getSubmiter($etatLieux->getSignIdReturn()); + if ($sub && ($sub['status'] ?? '') === 'completed') $providerSigned = true; + } catch (\Exception $e) { + } + } - if ($etatLieux->getSignIdDelivery()) { - try { - $sub = $signatureClient->getSubmiter($etatLieux->getSignIdDelivery()); - if ($sub && ($sub['status'] ?? '') === 'completed') $providerSigned = true; - } catch (\Exception $e) { + if ($etatLieux->getSignIdCustomerReturn()) { + try { + $sub = $signatureClient->getSubmiter($etatLieux->getSignIdCustomerReturn()); + if ($sub && ($sub['status'] ?? '') === 'completed') $customerSigned = true; + } catch (\Exception $e) { + } + } + } else { + + if ($etatLieux->getSignIdDelivery()) { + try { + $sub = $signatureClient->getSubmiter($etatLieux->getSignIdDelivery()); + if ($sub && ($sub['status'] ?? '') === 'completed') $providerSigned = true; + } catch (\Exception $e) { + } + } + + if ($etatLieux->getSignIdCustomer()) { + try { + $sub = $signatureClient->getSubmiter($etatLieux->getSignIdCustomer()); + if ($sub && ($sub['status'] ?? '') === 'completed') $customerSigned = true; + } catch (\Exception $e) { + } } } - - if ($etatLieux->getSignIdCustomer()) { - try { - $sub = $signatureClient->getSubmiter($etatLieux->getSignIdCustomer()); - if ($sub && ($sub['status'] ?? '') === 'completed') $customerSigned = true; - } catch (\Exception $e) { - } - } - return $this->render('etl/signed_entry_state.twig', [ 'mission' => $contrat, 'etatLieux' => $etatLieux, @@ -974,4 +1058,84 @@ class EtlController extends AbstractController return $file; } + + private function generateAndSendToDocuSealReturn(Contrats $contrat, EntityManagerInterface $em, KernelInterface $kernel, SignatureClient $signatureClient) + { + $etatLieux = $contrat->getEtatLieux(); + + // Generate PDF + $pdfService = new EtatLieuxPdfService($kernel, $contrat); + $pdfContent = $pdfService->generate(); + + + // Save PDF + $tmpPath = sys_get_temp_dir() . '/edl_sortant_' . $contrat->getId() . '_' . uniqid() . '.pdf'; + file_put_contents($tmpPath, $pdfContent); + + // Update entity with file + $file = new UploadedFile($tmpPath, 'edl_sortant_.pdf', 'application/pdf', null, true); + $etatLieux->setEtatLieuxUnsignReturnFile($file); + $etatLieux->setUpdatedAt(new \DateTimeImmutable()); + + $em->persist($etatLieux); + $em->flush(); // Save file + + // Send to DocuSeal + try { + $signatureClient->createSubmissionEtatLieuxSortant($etatLieux); + } catch (\Exception $e) { + // Fallback + } + } + + #[Route('/etl/mission/{id}/edl/refused', name: 'etl_edl_customer_refused', methods: ['POST'])] + public function eltEdlCustomerRefused( + Contrats $contrat, + Request $request, + EntityManagerInterface $em, + Mailer $mailer + ): Response { + $user = $this->getUser(); + if (!$user) { + return $this->redirectToRoute('etl_login'); + } + + $etatLieux = $contrat->getEtatLieux(); + + // On vérifie que l'on est bien en phase de retour + if (!$etatLieux || $etatLieux->getStatus() !== 'edl_return_done') { + $this->addFlash('error', 'Action impossible dans l\'état actuel.'); + return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $contrat->getId()]); + } + + // Récupération de la raison saisie + $reason = $request->request->get('refusal_reason'); + + // Mise à jour de l'entité + // Note : Assurez-vous d'avoir le champ raisonRefus dans votre entité EtatLieux + $etatLieux->setRaisonRefused($reason); + $etatLieux->setStatus('edl_return_refused'); // Nouveau status pour différencier + + // On clôture tout de même la mission côté réservation + $contrat->setReservationState('finished'); + + $em->flush(); + + // Notification par email (Optionnel mais recommandé) + $mailer->send( + 'contact@ludikevent.fr', + 'Admin Ludikevent', + "Signature refusée par le client - #" . $contrat->getNumReservation(), + "mails/etl/edl_refused_alert.twig", + [ + 'contrat' => $contrat, + 'reason' => $reason, + 'prestataire' => $user + ] + ); + + $this->addFlash('warning', 'Le refus a été enregistré. La mission est clôturée avec mention de litige.'); + + return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]); + } } diff --git a/src/Entity/EtatLieux.php b/src/Entity/EtatLieux.php index e22ee2b..2ccb2fd 100644 --- a/src/Entity/EtatLieux.php +++ b/src/Entity/EtatLieux.php @@ -5,6 +5,7 @@ namespace App\Entity; use App\Repository\EtatLieuxRepository; use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; +use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\HttpFoundation\File\File; use Vich\UploaderBundle\Mapping\Attribute\Uploadable; @@ -37,17 +38,31 @@ class EtatLieux #[ORM\OneToMany(targetEntity: EtatLieuxFile::class, mappedBy: 'etatLieux', cascade: ['persist', 'remove'])] private Collection $files; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: EtatReturnLieuxFile::class, mappedBy: 'etatLieux', cascade: ['persist', 'remove'])] + private Collection $filesReturn; + /** * @var Collection */ #[ORM\OneToMany(targetEntity: EtatLieuxComment::class, mappedBy: 'etatLieux', cascade: ['persist', 'remove'])] private Collection $comments; + #[ORM\OneToMany(targetEntity: EtatLieuxReturnComment::class, mappedBy: 'etatLieux', cascade: ['persist', 'remove'])] + private Collection $commentsReturns; + /** * @var Collection */ #[ORM\OneToMany(targetEntity: EtatLieuxPointControl::class, mappedBy: 'etatLieux', cascade: ['persist', 'remove'])] private Collection $pointControls; + /** + * @var Collection + */ + #[ORM\OneToMany(targetEntity: EtatLieuxReturnPointControl::class, mappedBy: 'etatLieux', cascade: ['persist', 'remove'])] + private Collection $pointControlsReturn; #[ORM\Column(length: 255, nullable: true)] private ?string $signIdDelivery = null; @@ -122,11 +137,17 @@ class EtatLieux #[ORM\Column(nullable: true)] private ?\DateTimeImmutable $updatedAt = null; + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $raisonRefused = null; + public function __construct() { $this->files = new ArrayCollection(); + $this->filesReturn = new ArrayCollection(); $this->comments = new ArrayCollection(); + $this->commentsReturns = new ArrayCollection(); $this->pointControls = new ArrayCollection(); + $this->pointControlsReturn = new ArrayCollection(); } public function getId(): ?int @@ -164,6 +185,37 @@ class EtatLieux return $this; } + + /** + * @return Collection + */ + public function getFileReturn(): Collection + { + return $this->filesReturn; + } + + public function addFileReturn(EtatReturnLieuxFile $file): static + { + if (!$this->filesReturn->contains($file)) { + $this->filesReturn->add($file); + $file->setEtatLieux($this); + } + + return $this; + } + + public function removeFileReturn(EtatReturnLieuxFile $file): static + { + if ($this->filesReturn->removeElement($file)) { + // set the owning side to null (unless already changed) + if ($file->getEtatLieux() === $this) { + $file->setEtatLieux(null); + } + } + + return $this; + } + public function getStatus(): string { return $this->status; @@ -241,6 +293,93 @@ class EtatLieux return $this; } + /* + * @return Collection + */ + public function getEtatReturnLieuxFile(): Collection + { + return $this->filesReturn; + } + + public function addEtatReturnLieuxFile(EtatReturnLieuxFile $comment): static + { + if (!$this->filesReturn->contains($comment)) { + $this->filesReturn->add($comment); + $comment->setEtatLieux($this); + } + + return $this; + } + + public function removeEtatReturnLieuxFile(EtatReturnLieuxFile $comment): static + { + if ($this->filesReturn->removeElement($comment)) { + if ($comment->getEtatLieux() === $this) { + $comment->setEtatLieux(null); + } + } + + return $this; + } + + /* + * @return Collection + */ + public function getPointControlsReturn(): Collection + { + return $this->pointControlsReturn; + } + + public function addPointControlsReturn(EtatLieuxReturnPointControl $comment): static + { + if (!$this->pointControlsReturn->contains($comment)) { + $this->pointControlsReturn->add($comment); + $comment->setEtatLieux($this); + } + + return $this; + } + + public function removeEtatLieuxReturnPointControl(EtatLieuxReturnPointControl $comment): static + { + if ($this->pointControlsReturn->removeElement($comment)) { + if ($comment->getEtatLieux() === $this) { + $comment->setEtatLieux(null); + } + } + + return $this; + } + + /** + * @return Collection + */ + public function getCommentsReturn(): Collection + { + return $this->commentsReturns; + } + + public function addCommentReturn(EtatLieuxReturnComment $comment): static + { + if (!$this->commentsReturns->contains($comment)) { + $this->commentsReturns->add($comment); + $comment->setEtatLieux($this); + } + + return $this; + } + + public function removeCommentReturn(EtatLieuxReturnComment $comment): static + { + if ($this->commentsReturns->removeElement($comment)) { + if ($comment->getEtatLieux() === $this) { + $comment->setEtatLieux(null); + } + } + + return $this; + } + public function getSignIdDelivery(): ?string { return $this->signIdDelivery; @@ -527,4 +666,16 @@ class EtatLieux return $this; } + + public function getRaisonRefused(): ?string + { + return $this->raisonRefused; + } + + public function setRaisonRefused(?string $raisonRefused): static + { + $this->raisonRefused = $raisonRefused; + + return $this; + } } diff --git a/src/Entity/EtatLieuxReturnComment.php b/src/Entity/EtatLieuxReturnComment.php new file mode 100644 index 0000000..97623d2 --- /dev/null +++ b/src/Entity/EtatLieuxReturnComment.php @@ -0,0 +1,72 @@ +createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getContent(): ?string + { + return $this->content; + } + + public function setContent(string $content): static + { + $this->content = $content; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getEtatLieux(): ?EtatLieux + { + return $this->etatLieux; + } + + public function setEtatLieux(?EtatLieux $etatLieux): static + { + $this->etatLieux = $etatLieux; + + return $this; + } +} diff --git a/src/Entity/EtatLieuxReturnPointControl.php b/src/Entity/EtatLieuxReturnPointControl.php new file mode 100644 index 0000000..2798dc3 --- /dev/null +++ b/src/Entity/EtatLieuxReturnPointControl.php @@ -0,0 +1,82 @@ + false])] + private ?bool $status = false; + + #[ORM\Column(type: Types::TEXT, nullable: true)] + private ?string $details = null; + + #[ORM\ManyToOne(inversedBy: 'pointControlsReturn')] + #[ORM\JoinColumn(nullable: false)] + private ?EtatLieux $etatLieux = null; + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): static + { + $this->name = $name; + + return $this; + } + + public function isStatus(): ?bool + { + return $this->status; + } + + public function setStatus(bool $status): static + { + $this->status = $status; + + return $this; + } + + public function getDetails(): ?string + { + return $this->details; + } + + public function setDetails(?string $details): static + { + $this->details = $details; + + return $this; + } + + public function getEtatLieux(): ?EtatLieux + { + return $this->etatLieux; + } + + public function setEtatLieux(?EtatLieux $etatLieux): static + { + $this->etatLieux = $etatLieux; + + return $this; + } +} diff --git a/src/Entity/EtatReturnLieuxFile.php b/src/Entity/EtatReturnLieuxFile.php new file mode 100644 index 0000000..44038a4 --- /dev/null +++ b/src/Entity/EtatReturnLieuxFile.php @@ -0,0 +1,130 @@ +createdAt = new \DateTimeImmutable(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getEtatLieux(): ?EtatLieux + { + return $this->etatLieux; + } + + public function setEtatLieux(?EtatLieux $etatLieux): static + { + $this->etatLieux = $etatLieux; + + return $this; + } + + public function getFile(): ?File + { + return $this->file; + } + + public function setFile(?File $file): void + { + $this->file = $file; + if (null !== $file) { + $this->createdAt = new \DateTimeImmutable(); + } + } + + public function getFileName(): ?string + { + return $this->fileName; + } + + public function setFileName(?string $fileName): void + { + $this->fileName = $fileName; + } + + public function getFileSize(): ?int + { + return $this->fileSize; + } + + public function setFileSize(?int $fileSize): void + { + $this->fileSize = $fileSize; + } + + public function getMimeType(): ?string + { + return $this->mimeType; + } + + public function setMimeType(?string $mimeType): void + { + $this->mimeType = $mimeType; + } + + public function getType(): ?string + { + return $this->type; + } + + public function setType(string $type): static + { + $this->type = $type; + + return $this; + } + + public function getCreatedAt(): ?\DateTimeImmutable + { + return $this->createdAt; + } + + public function setCreatedAt(\DateTimeImmutable $createdAt): static + { + $this->createdAt = $createdAt; + + return $this; + } +} diff --git a/src/Repository/EtatLieuxReturnCommentRepository.php b/src/Repository/EtatLieuxReturnCommentRepository.php new file mode 100644 index 0000000..8975020 --- /dev/null +++ b/src/Repository/EtatLieuxReturnCommentRepository.php @@ -0,0 +1,24 @@ + + * + * @method EtatLieuxComment|null find($id, $lockMode = null, $lockVersion = null) + * @method EtatLieuxComment|null findOneBy(array $criteria, array $orderBy = null) + * @method EtatLieuxComment[] findAll() + * @method EtatLieuxComment[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class EtatLieuxReturnCommentRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EtatLieuxReturnComment::class); + } +} diff --git a/src/Repository/EtatLieuxReturnFileRepository.php b/src/Repository/EtatLieuxReturnFileRepository.php new file mode 100644 index 0000000..fdf82f9 --- /dev/null +++ b/src/Repository/EtatLieuxReturnFileRepository.php @@ -0,0 +1,23 @@ + + * + * @method EtatLieuxReturnFile|null find($id, $lockMode = null, $lockVersion = null) + * @method EtatLieuxReturnFile|null findOneBy(array $criteria, array $orderBy = null) + * @method EtatLieuxReturnFile[] findAll() + * @method EtatLieuxReturnFile[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class EtatLieuxReturnFileRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EtatLieuxReturnFile::class); + } +} diff --git a/src/Repository/EtatLieuxReturnPointControlRepository.php b/src/Repository/EtatLieuxReturnPointControlRepository.php new file mode 100644 index 0000000..0774cf2 --- /dev/null +++ b/src/Repository/EtatLieuxReturnPointControlRepository.php @@ -0,0 +1,23 @@ + + * + * @method EtatLieuxReturnPointControl|null find($id, $lockMode = null, $lockVersion = null) + * @method EtatLieuxReturnPointControl|null findOneBy(array $criteria, array $orderBy = null) + * @method EtatLieuxReturnPointControl[] findAll() + * @method EtatLieuxReturnPointControl[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null) + */ +class EtatLieuxReturnPointControlRepository extends ServiceEntityRepository +{ + public function __construct(ManagerRegistry $registry) + { + parent::__construct($registry, EtatLieuxReturnPointControl::class); + } +} diff --git a/src/Service/Pdf/EtatLieuxPdfService.php b/src/Service/Pdf/EtatLieuxPdfService.php index 81662b5..2c50e13 100644 --- a/src/Service/Pdf/EtatLieuxPdfService.php +++ b/src/Service/Pdf/EtatLieuxPdfService.php @@ -31,46 +31,50 @@ class EtatLieuxPdfService extends Fpdf { $this->AddPage(); $this->renderEtatLieuxEntrant(); - + $this->renderSignaturePage(); - + return $this->Output('S'); } private function renderSignaturePage(): void { $this->AddPage(); - + $this->SetFont('Arial', 'B', 14); $this->SetTextColor(37, 99, 235); $this->Cell(0, 10, $this->clean("SIGNATURES"), 0, 1, 'C'); $this->Ln(10); - + $this->SetFont('Arial', '', 10); $this->SetTextColor(0, 0, 0); - $this->MultiCell(0, 5, $this->clean("En signant ce document, les parties valident l'état des lieux d'installation ci-dessus."), 0, 'C'); + if($this->contrats->getEtatLieux()->getStatus() == "edl_return_done"){ + $this->MultiCell(0, 5, $this->clean("En signant ce document, les parties valident l'état de retour ci-dessus."), 0, 'C'); + } else { + $this->MultiCell(0, 5, $this->clean("En signant ce document, les parties valident l'état des lieux d'installation ci-dessus."), 0, 'C'); + } $this->Ln(20); // --- SIGNATURES --- $ySign = $this->GetY(); - + $this->SetFont('Arial', 'B', 10); $this->Cell(95, 8, $this->clean("Le Prestataire"), 0, 0, 'C'); $this->Cell(95, 8, $this->clean("Le Client (Bon pour accord)"), 0, 1, 'C'); - + $this->Ln(8); $this->Cell(95, 40, "", 1, 0); $this->Cell(95, 40, "", 1, 1); - + // DocuSeal tags $this->SetXY(20, $ySign + 35); $this->SetFont('Arial', '', 8); $this->SetTextColor(255, 255, 255); $this->Cell(50, 5, '{{Sign;type=signature;role=Ludikevent}}', 0, 0); - + $this->SetXY(115, $ySign + 35); $this->Cell(50, 5, '{{Sign;type=signature;role=Client}}', 0, 0); - + $this->SetTextColor(0, 0, 0); } @@ -81,19 +85,24 @@ class EtatLieuxPdfService extends Fpdf // Titre $this->SetFont('Arial', 'B', 14); $this->SetTextColor(37, 99, 235); - $this->Cell(0, 10, $this->clean("ÉTAT DES LIEUX D'INSTALLATION (ENTRANT)"), 0, 1, 'C'); - + if($this->contrats->getEtatLieux()->getStatus() == "edl_return_done"){ + $this->Cell(0, 10, $this->clean("ÉTAT DES RETOUR"), 0, 1, 'C'); + } else { + $this->Cell(0, 10, $this->clean("ÉTAT DES LIEUX D'INSTALLATION (ENTRANT)"), 0, 1, 'C'); + } + + $this->SetFont('Arial', 'B', 10); $this->SetTextColor(0, 0, 0); $this->Cell(0, 7, $this->clean("Contrat N° " . $this->contrats->getNumReservation()), 0, 1, 'C'); $this->Ln(5); $this->SetFont('Arial', '', 10); - $this->MultiCell(0, 5, $this->clean("Le locataire reconnaît avoir reçu le matériel ci-dessous en bon état de fonctionnement, propre et conforme à la commande."), 0, 'C'); + $this->MultiCell(0, 5, $this->clean("Le locataire reconnaît avoir lue et accepter l'état des retours."), 0, 'C'); $this->Ln(5); // --- INFO PARTIES (2 BLOCS SÉPARÉS) --- - + // Detect Delivery/Installation $hasDelivery = false; foreach ($this->contrats->getContratsLines() as $line) { @@ -117,37 +126,37 @@ class EtatLieuxPdfService extends Fpdf $colWidth = 90; $gap = 10; $xStart = 10; - + $this->SetFillColor(245, 245, 245); $this->SetFont('Arial', 'B', 9); - + // Header Block 1 $this->Cell($colWidth, 6, $this->clean($prestataireTitle), 1, 0, 'L', true); - + // Header Block 2 $this->SetX($xStart + $colWidth + $gap); $this->Cell($colWidth, 6, $this->clean(" CLIENT / LIEU"), 1, 1, 'L', true); - + $this->SetFont('Arial', '', 8); $yContent = $this->GetY(); - + // Content Block 1 (Prestataire) $this->SetXY($xStart, $yContent); $prestataire = $this->contrats->getPrestataire(); $prestataireTxt = $prestataire ? ($prestataire->getName() . "\n" . $prestataire->getEmail()) : "Ludikevent (Admin)\ncontact@ludikevent.fr"; $this->MultiCell($colWidth, 5, $this->clean($prestataireTxt), 'LRB', 'L'); $h1 = $this->GetY() - $yContent; - + // Content Block 2 (Client) $this->SetXY($xStart + $colWidth + $gap, $yContent); $customer = $this->contrats->getCustomer(); - $clientTxt = $customer->getName() . " " . $customer->getSurname() . "\n" . - $customer->getEmail() . "\n" . - $customer->getPhone() . "\n" . + $clientTxt = $customer->getName() . " " . $customer->getSurname() . "\n" . + $customer->getEmail() . "\n" . + $customer->getPhone() . "\n" . "Lieu: " . $this->contrats->getAddressEvent() . " " . $this->contrats->getZipCodeEvent() . " " . $this->contrats->getTownEvent(); $this->MultiCell($colWidth, 5, $this->clean($clientTxt), 'LRB', 'L'); $h2 = $this->GetY() - $yContent; - + // Reset Y to max height $this->SetY($yContent + max($h1, $h2) + 5); @@ -158,7 +167,7 @@ class EtatLieuxPdfService extends Fpdf $this->Cell(190, 8, $this->clean(" DÉSIGNATION DU MATÉRIEL"), 1, 1, 'L', true); $this->SetFont('Arial', '', 9); - + foreach ($this->contrats->getContratsLines() as $line) { // Skip livraison if (stripos($line->getName(), 'livraison') !== false) { @@ -174,7 +183,7 @@ class EtatLieuxPdfService extends Fpdf if (stripos($opt->getName(), 'livraison') !== false) { continue; } - + $this->MultiCell(190, 8, $this->clean("[Option] " . $opt->getName()), 1, 'L'); } @@ -191,16 +200,16 @@ class EtatLieuxPdfService extends Fpdf foreach ($this->contrats->getProductReserves() as $reserve) { $product = $reserve->getProduct(); if ($product && count($product->getProductPointControlls()) > 0) { - - $this->SetFillColor(230, 240, 255); + + $this->SetFillColor(230, 240, 255); $this->SetFont('Arial', 'B', 9); $this->Cell(190, 6, $this->clean(" " . $product->getName()), 1, 1, 'L', true); - + $this->SetFillColor(250, 250, 250); $this->SetFont('Arial', 'B', 8); $this->Cell(95, 5, $this->clean(" Nom du contrôle"), 1, 0, 'L', true); $this->Cell(95, 5, $this->clean(" Commentaire / État"), 1, 1, 'L', true); - + $this->SetFont('Arial', '', 8); foreach ($product->getProductPointControlls() as $pPoint) { $details = ''; @@ -210,17 +219,17 @@ class EtatLieuxPdfService extends Fpdf break; } } - + $y = $this->GetY(); $x = $this->GetX(); - + $this->MultiCell(95, 5, $this->clean($pPoint->getName()), 1, 'L'); $h1 = $this->GetY() - $y; - + $this->SetXY($x + 95, $y); $this->MultiCell(95, 5, $this->clean($details), 1, 'L'); $h2 = $this->GetY() - $y; - + $this->SetY($y + max($h1, $h2)); $this->SetX(10); } @@ -235,10 +244,10 @@ class EtatLieuxPdfService extends Fpdf // --- COMMENTAIRES --- $this->SetFont('Arial', 'B', 10); $this->Cell(0, 8, $this->clean("COMMENTAIRES & MÉDIAS :"), 0, 1, 'L'); - + $currentY = $this->GetY(); $etatLieux = $this->contrats->getEtatLieux(); - + if ($etatLieux) { // Comments $comments = $etatLieux->getComments(); @@ -250,7 +259,7 @@ class EtatLieuxPdfService extends Fpdf } $this->Ln(2); } - + // Files $filesCount = $etatLieux->getFiles()->count(); if ($filesCount > 0) { @@ -258,7 +267,7 @@ class EtatLieuxPdfService extends Fpdf $this->Cell(0, 6, $this->clean(">> Nombre de photos/vidéos jointes au dossier numérique : " . $filesCount), 0, 1, 'L'); } } - + $endY = $this->GetY(); // Box removed as requested $this->Ln(30); diff --git a/src/Service/Signature/Client.php b/src/Service/Signature/Client.php index f8fa2e3..d610e71 100644 --- a/src/Service/Signature/Client.php +++ b/src/Service/Signature/Client.php @@ -381,11 +381,19 @@ class Client public function getSigningUrl(EtatLieux $etatLieux, string $role): ?string { - $submitterId = match ($role) { - 'Ludikevent', 'Prestataire' => $etatLieux->getSignIdDelivery(), - 'Client' => $etatLieux->getSignIdCustomer(), - default => null - }; + if($etatLieux->getStatus() == "edl_return_done"){ + $submitterId = match ($role) { + 'Ludikevent', 'Prestataire' => $etatLieux->getSignIdReturn(), + 'Client' => $etatLieux->getSignIdCustomerReturn(), + default => null + }; + } else { + $submitterId = match ($role) { + 'Ludikevent', 'Prestataire' => $etatLieux->getSignIdDelivery(), + 'Client' => $etatLieux->getSignIdCustomer(), + default => null + }; + } if (!$submitterId) { return null; @@ -393,4 +401,59 @@ class Client return $this->getLinkSign($submitterId); } + + public function createSubmissionEtatLieuxSortant(?EtatLieux $etatLieux) + { + $contrat = $etatLieux->getContrat(); + $customer = $contrat->getCustomer(); + // Prestataire or Admin + $prestataireEmail = 'contact@ludikevent.fr'; + if ($etatLieux->getPrestataire()) { + $prestataireEmail = $etatLieux->getPrestataire()->getEmail(); + } elseif ($etatLieux->getAccount()) { + $prestataireEmail = $etatLieux->getAccount()->getEmail(); + } + + // URL où on redirige après signature + $completedRedirectUrl = $this->baseUrl . $this->urlGenerator->generate('etl_mission_signed_entry_state', ['id' => $contrat->getId()]); + + // Récupération du fichier PDF EDL (Non signé) + $relativeFileUrl = $this->storage->resolveUri($etatLieux, 'etatLieuxUnsignReturnFile'); + $fileUrl = $this->baseUrl . $relativeFileUrl; + + $submission = $this->docuseal->createSubmissionFromPdf([ + 'name' => 'Etat des Lieux - Contrat #' . $contrat->getNumReservation(), + 'completed_redirect_url' => $completedRedirectUrl, + 'send_email' => true, // Envoi email aux deux parties + 'documents' => [ + [ + 'name' => 'edl_' . $contrat->getNumReservation() . '.pdf', + 'file' => $fileUrl, + ], + ], + 'submitters' => [ + [ + 'role' => 'Ludikevent', // Prestataire + 'email' => $prestataireEmail, + ], + [ + 'role' => 'Client', + 'email' => $customer->getEmail(), + 'name' => $customer->getSurname() . ' ' . $customer->getName(), + ], + ], + ]); + + // Mapping des IDs + foreach ($submission['submitters'] as $submitter) { + if ($submitter['role'] === 'Ludikevent') { + $etatLieux->setSignIdReturn($submitter['id']); + } elseif ($submitter['role'] === 'Client') { + $etatLieux->setSignIdCustomerReturn($submitter['id']); + } + } + + $this->entityManager->persist($etatLieux); + $this->entityManager->flush(); + } } diff --git a/templates/etl/edl.twig b/templates/etl/edl.twig index 722f0b1..c112798 100644 --- a/templates/etl/edl.twig +++ b/templates/etl/edl.twig @@ -21,6 +21,26 @@

Photos & Vidéos

+ {% for file in etatLieux.fileReturn %} +
+ {% if file.type == 'photo' %} + Photo + {% else %} + + {% endif %} + +
+ +
+
+ {% endfor %} {% for file in etatLieux.files %}
{% if file.type == 'photo' %} @@ -33,16 +53,14 @@
{% endif %} - +
- {% else %} -
Aucun média ajouté.
- {% endfor %} + {% endfor %} {# LIGHTBOX MODAL #} @@ -69,7 +87,7 @@ - + @@ -79,7 +97,7 @@ {# POINTS DE CONTROLE #}

Points de Contrôle

- +
{% for reserve in mission.productReserves %} {% set product = reserve.product %} @@ -93,24 +111,31 @@ {% for point in product.productPointControlls %} {# Try to find existing status/comment in etatLieux.pointControls #} {% set existingPoint = null %} - {% for ep in etatLieux.pointControls %} - {% if ep.name == point.name %} - {% set existingPoint = ep %} - {% endif %} - {% endfor %} - + {% if etatLieux.status == "return_edl_progress" %} + {% for ep in etatLieux.pointControlsReturn %} + {% if ep.name == point.name %} + {% set existingPoint = ep %} + {% endif %} + {% endfor %} + {% else %} + {% for ep in etatLieux.pointControls %} + {% if ep.name == point.name %} + {% set existingPoint = ep %} + {% endif %} + {% endfor %} + {% endif %}
-

{{ point.name }}

-
@@ -120,7 +145,7 @@
{% endif %} {% endfor %} - + @@ -132,14 +157,22 @@

Commentaires

- {% for comment in etatLieux.comments %} -
-

{{ comment.createdAt|date('d/m H:i') }}

-

{{ comment.content }}

-
+ {% if etatLieux.status == "return_edl_progress" %} + {% for comment in etatLieux.commentsReturn %} +
+

{{ comment.createdAt|date('d/m H:i') }}

+

{{ comment.content }}

+
+ {% endfor %} {% else %} -

Aucun commentaire.

- {% endfor %} + {% for comment in etatLieux.comments %} +
+

{{ comment.createdAt|date('d/m H:i') }}

+

{{ comment.content }}

+
+ {% endfor %} + {% endif %} +
diff --git a/templates/etl/signed_entry_state.twig b/templates/etl/signed_entry_state.twig index 11035ac..4e17329 100644 --- a/templates/etl/signed_entry_state.twig +++ b/templates/etl/signed_entry_state.twig @@ -4,7 +4,7 @@ {% block body %}
- + {# HEADER #}
@@ -20,7 +20,7 @@

État des Lieux Terminé

Veuillez procéder à la signature du document.

- +
Voir le PDF (Actualiser) @@ -31,7 +31,7 @@

Signatures Requises

- +
{% if providerSigned %}
@@ -55,6 +55,32 @@ Faire Signer (Client) + {# ... (sous le bouton "Faire Signer (Client)") ... #} + + {% if etatLieux.status == "edl_return_done" %} +
+ + + + + + + +
+ {% endif %} {% endif %}
diff --git a/templates/etl/view.twig b/templates/etl/view.twig index 9a59e8b..3372060 100644 --- a/templates/etl/view.twig +++ b/templates/etl/view.twig @@ -117,10 +117,12 @@ Reprendre l'état des lieux {% elseif mission.etatLieux.status == 'edl_validated' %} - - - Commenter l'état des lieux de retour - +
+ +
{% endif %} {# DATES #} diff --git a/templates/mails/etl/edl_refused_alert.twig b/templates/mails/etl/edl_refused_alert.twig new file mode 100644 index 0000000..f38a382 --- /dev/null +++ b/templates/mails/etl/edl_refused_alert.twig @@ -0,0 +1,54 @@ +{% extends 'mails/base.twig' %} + +{% block content %} + + + + ⚠️ ALERTE : Signature Refusée (Retour) + + + + + + Bonjour l'équipe Ludikevent, + + + Lors de l'état des lieux de retour pour la réservation #{{ datas.contrat.numReservation }}, le client a refusé de procéder à la signature. + + + + RAISON DU REFUS INDIQUÉE : + + + {{ datas.reason|default('Aucune raison spécifique n\'a été saisie par le prestataire.') }} + + + + Détails de la mission : + + + Client : {{ datas.contrat.customer.surname }} {{ datas.contrat.customer.name }}
+ Intervenant : {{ datas.prestataire.surname|default('') }} {{ datas.prestataire.name|default('Non assigné') }}
+ Lieu : {{ datas.contrat.addressEvent }} {{ datas.contrat.zipCodeEvent }} {{ datas.contrat.townEvent }} +
+ + {# Observations de retour (on utilise commentsReturn si c'est le champ lié aux remarques de sortie) #} + {% if datas.contrat.etatLieux.commentsReturn|length > 0 %} + + Observations saisies lors du retour : + + {% for comment in datas.contrat.etatLieux.commentsReturn %} + + - {{ comment.content }} + + {% endfor %} + {% endif %} + + + Consulter le dossier complet + + + + L'état des lieux a été marqué avec le statut "Refusé". Veuillez vérifier les fichiers médias joints pour constater d'éventuels dommages. + +{% endblock %}