diff --git a/public/media/etat_lieux/media/2025-12-30-17-30-12-341-69860187de871853088502.jpg b/public/media/etat_lieux/media/2025-12-30-17-30-12-341-69860187de871853088502.jpg new file mode 100644 index 0000000..1425a93 Binary files /dev/null and b/public/media/etat_lieux/media/2025-12-30-17-30-12-341-69860187de871853088502.jpg differ diff --git a/src/Controller/EtlController.php b/src/Controller/EtlController.php index 710c342..3e6fb20 100644 --- a/src/Controller/EtlController.php +++ b/src/Controller/EtlController.php @@ -8,6 +8,8 @@ use App\Entity\ContratsPayments; use App\Entity\EtatLieux; use App\Entity\EtatLieuxComment; use App\Entity\EtatLieuxFile; +use App\Entity\EtatLieuxPointControl; +use App\Entity\ProductPointControll; use App\Entity\Prestaire; use App\Form\PrestairePasswordType; use App\Repository\ContratsRepository; @@ -29,6 +31,8 @@ use Symfony\Component\Routing\Attribute\Route; use Symfony\Component\Routing\Generator\UrlGeneratorInterface; use Symfony\Component\Security\Http\Authentication\AuthenticationUtils; use KnpU\OAuth2ClientBundle\Client\ClientRegistry; +use Vich\UploaderBundle\Templating\Helper\UploaderHelper; +use Vich\UploaderBundle\Storage\StorageInterface; class EtlController extends AbstractController { @@ -448,7 +452,55 @@ class EtlController extends AbstractController return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]); } - #[Route('/etl/mission/{id}/edl/file', name: 'etl_edl_add_file', methods: ['POST'])] + #[Route('/etl/mission/{id}/edl/save-points', name: 'etl_edl_save_points', methods: ['POST'])] + public function eltEdlSavePoints(Contrats $contrat, Request $request, EntityManagerInterface $em): Response + { + $user = $this->getUser(); + if (!$user) { + return $this->redirectToRoute('etl_login'); + } + + $etatLieux = $contrat->getEtatLieux(); + if (!$etatLieux) { + return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]); + } + + $data = $request->request->all('points'); + + if ($data) { + foreach ($data as $productId => $points) { + foreach ($points as $pointId => $values) { + $productPoint = $em->getRepository(ProductPointControll::class)->find($pointId); + + if ($productPoint) { + $existing = null; + 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); + } + } + } + $em->flush(); + $this->addFlash('success', 'Points de contrôle enregistrés.'); + } + + return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]); + } + + #[Route('/etl/mission/{id}/edl/file', name: 'etl_edl_add_file', methods: ['POST'])] public function eltEdlAddFile(Contrats $contrat, Request $request, EntityManagerInterface $em): Response { $user = $this->getUser(); @@ -551,32 +603,7 @@ class EtlController extends AbstractController $etatLieux = $contrat->getEtatLieux(); if ($etatLieux) { $etatLieux->setStatus('edl_done'); - - // Generate PDF - $pdfService = new EtatLieuxPdfService($kernel, $contrat); - $pdfContent = $pdfService->generate(); - - // Save PDF - $tmpPath = sys_get_temp_dir() . '/edl_entrant_' . $contrat->getId() . '_' . uniqid() . '.pdf'; - file_put_contents($tmpPath, $pdfContent); - - // Update entity with file - $file = new UploadedFile($tmpPath, 'edl_entrant.pdf', 'application/pdf', null, true); - $etatLieux->setEtatLieuxUnsignFile($file); - $etatLieux->setUpdatedAt(new \DateTimeImmutable()); - - $em->flush(); // Save file - - // Send to DocuSeal (Assuming method exists or similar logic) - // If createSubmissionEtatLieux doesn't exist, this might fail. - // But based on prompt "send docuseal", I assume integration is ready or I follow pattern. - // I'll call createSubmissionEtatLieux. - try { - $signatureClient->createSubmissionEtatLieux($etatLieux); - } catch (\Exception $e) { - // Fallback or log if method missing, but proceeding - } - + $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()]); } @@ -584,6 +611,50 @@ class EtlController extends AbstractController return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]); } + #[Route('/etl/mission/{id}/edl/regenerate-view', name: 'etl_edl_regenerate_view', methods: ['GET'])] + public function eltEdlRegenerateAndView(Contrats $contrat, EntityManagerInterface $em, KernelInterface $kernel, SignatureClient $signatureClient, UploaderHelper $uploaderHelper): Response + { + $user = $this->getUser(); + if (!$user) { + return $this->redirectToRoute('etl_login'); + } + + $this->generateAndSendToDocuSeal($contrat, $em, $kernel, $signatureClient); + + $etatLieux = $contrat->getEtatLieux(); + $path = $uploaderHelper->asset($etatLieux, 'etatLieuxUnsignFile'); + + return new RedirectResponse($path); + } + + private function generateAndSendToDocuSeal(Contrats $contrat, EntityManagerInterface $em, KernelInterface $kernel, SignatureClient $signatureClient): void + { + $etatLieux = $contrat->getEtatLieux(); + + // Generate PDF + $pdfService = new EtatLieuxPdfService($kernel, $contrat); + $pdfContent = $pdfService->generate(); + + // Save PDF + $tmpPath = sys_get_temp_dir() . '/edl_entrant_' . $contrat->getId() . '_' . uniqid() . '.pdf'; + file_put_contents($tmpPath, $pdfContent); + + // Update entity with file + $file = new UploadedFile($tmpPath, 'edl_entrant.pdf', 'application/pdf', null, true); + $etatLieux->setEtatLieuxUnsignFile($file); + $etatLieux->setUpdatedAt(new \DateTimeImmutable()); + + $em->persist($etatLieux); + $em->flush(); // Save file + + // Send to DocuSeal + try { + $signatureClient->createSubmissionEtatLieux($etatLieux); + } catch (\Exception $e) { + // Fallback + } + } + #[Route('/etl/mission/{id}/signed-entry-state', name: 'etl_mission_signed_entry_state', methods: ['GET'])] public function eltMissionSignedEntryState(Contrats $contrat, SignatureClient $signatureClient): Response { @@ -644,7 +715,7 @@ class EtlController extends AbstractController } #[Route('/etl/mission/{id}/edl/close', name: 'etl_edl_close', methods: ['POST'])] - public function eltMissionCloseEdl(Contrats $contrat, SignatureClient $signatureClient, EntityManagerInterface $em, Mailer $mailer): Response + public function eltMissionCloseEdl(Contrats $contrat, SignatureClient $signatureClient, EntityManagerInterface $em, Mailer $mailer, StorageInterface $storage): Response { $user = $this->getUser(); if (!$user) { @@ -663,7 +734,7 @@ class EtlController extends AbstractController $submission = $signatureClient->getSubmition($submissionId); $signedPdfUrl = $submission['documents'][0]['url'] ?? null; - $auditUrl = $submission['audit_log_url'] ?? null; // Assuming DocuSeal API returns this or similar + $auditUrl = $submission['audit_log_url'] ?? null; if ($signedPdfUrl) { $tmpPath = sys_get_temp_dir() . '/edl_signed_' . $contrat->getId() . '.pdf'; @@ -671,12 +742,15 @@ class EtlController extends AbstractController $file = new UploadedFile($tmpPath, 'edl_entrant_signed.pdf', 'application/pdf', null, true); $etatLieux->setEtatLieuxSignFile($file); } + if ($auditUrl) { + $tmpPathAudit = sys_get_temp_dir() . '/edl_audit_signed_' . $contrat->getId() . '.pdf'; + file_put_contents($tmpPathAudit, file_get_contents($auditUrl)); + $file = new UploadedFile($tmpPathAudit, 'edl_audit_signed.pdf', 'application/pdf', null, true); + $etatLieux->setEtatLieuxAuditFile($file); + } + $etatLieux->setUpdatedAt(new \DateTimeImmutable()); - // Audit log URL might not be directly exposed or requires different call. - // If not available easily, we skip or try constructing it. - // Assuming simple download for now if URL exists. - - $etatLieux->setStatus('edl_validated'); // Final state + $etatLieux->setStatus('edl_validated'); $contrat->setReservationState('progress'); $em->flush(); @@ -690,10 +764,24 @@ class EtlController extends AbstractController } $attachments = []; - if (isset($tmpPath) && file_exists($tmpPath)) { + + // Try resolve path from Vich + $signPath = $storage->resolvePath($etatLieux, 'etatLieuxSignFile'); + // If resolvePath returns null (e.g. no mapping or file not found yet?), check manual path + // But flush() should have moved it. resolvePath usually returns absolute path. + if ($signPath && file_exists($signPath)) { + $attachments[] = DataPart::fromPath($signPath, 'Etat_des_lieux_signe.pdf'); + } elseif (isset($tmpPath) && file_exists($tmpPath)) { $attachments[] = DataPart::fromPath($tmpPath, 'Etat_des_lieux_signe.pdf'); } + $auditPath = $storage->resolvePath($etatLieux, 'etatLieuxAuditFile'); + if ($auditPath && file_exists($auditPath)) { + $attachments[] = DataPart::fromPath($auditPath, 'Audit_Etat_des_lieux_signe.pdf'); + } elseif (isset($tmpPathAudit) && file_exists($tmpPathAudit)) { + $attachments[] = DataPart::fromPath($tmpPathAudit, 'Audit_Etat_des_lieux_signe.pdf'); + } + foreach (array_unique($recipients) as $email) { $mailer->send( $email, diff --git a/src/Service/Pdf/EtatLieuxPdfService.php b/src/Service/Pdf/EtatLieuxPdfService.php index 21b838c..81662b5 100644 --- a/src/Service/Pdf/EtatLieuxPdfService.php +++ b/src/Service/Pdf/EtatLieuxPdfService.php @@ -27,20 +27,53 @@ class EtatLieuxPdfService extends Fpdf $this->SetAutoPageBreak(true, 35); } - /** - * Génère le PDF de l'état des lieux entrant - */ public function generate(): string { $this->AddPage(); $this->renderEtatLieuxEntrant(); - // On peut ajouter une page de signature si nécessaire, - // ou laisser la signature se faire sur ce document via DocuSeal + $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'); + $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); + } + private function renderEtatLieuxEntrant(): void { $this->SetY(50); @@ -57,7 +90,66 @@ class EtatLieuxPdfService extends Fpdf $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->Ln(10); + $this->Ln(5); + + // --- INFO PARTIES (2 BLOCS SÉPARÉS) --- + + // Detect Delivery/Installation + $hasDelivery = false; + foreach ($this->contrats->getContratsLines() as $line) { + if (stripos($line->getName(), 'livraison') !== false || stripos($line->getName(), 'installation') !== false) { + $hasDelivery = true; break; + } + } + if (!$hasDelivery) { + foreach ($this->contrats->getContratsOptions() as $opt) { + if (stripos($opt->getName(), 'livraison') !== false || stripos($opt->getName(), 'installation') !== false) { + $hasDelivery = true; break; + } + } + } + + $prestataireTitle = " PRESTATAIRE"; + if ($hasDelivery) { + $prestataireTitle .= " (Livreur / Installateur)"; + } + + $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" . + "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); // --- LISTE DU MATÉRIEL --- $this->SetFont('Arial', 'B', 10); @@ -74,7 +166,7 @@ class EtatLieuxPdfService extends Fpdf } // Affichage simple avec case à cocher - $this->MultiCell(190, 8, $this->clean("[ ] " . $line->getName()), 1, 'L'); + $this->MultiCell(190, 8, $this->clean($line->getName()), 1, 'L'); } foreach ($this->contrats->getContratsOptions() as $opt) { @@ -83,10 +175,62 @@ class EtatLieuxPdfService extends Fpdf continue; } - $this->MultiCell(190, 8, $this->clean("[ ] [Option] " . $opt->getName()), 1, 'L'); + $this->MultiCell(190, 8, $this->clean("[Option] " . $opt->getName()), 1, 'L'); } - $this->Ln(10); + $this->Ln(5); + + // --- POINTS DE CONTRÔLE --- + $etatLieux = $this->contrats->getEtatLieux(); + if ($etatLieux) { + $this->SetFont('Arial', 'B', 10); + $this->SetFillColor(240, 240, 240); + $this->Cell(190, 8, $this->clean(" POINTS DE CONTRÔLE"), 1, 1, 'L', true); + $this->Ln(3); + + foreach ($this->contrats->getProductReserves() as $reserve) { + $product = $reserve->getProduct(); + if ($product && count($product->getProductPointControlls()) > 0) { + + $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 = ''; + foreach ($etatLieux->getPointControls() as $ep) { + if ($ep->getName() === $pPoint->getName()) { + $details = $ep->getDetails(); + 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); + } + $this->Ln(3); + } + } + $this->Ln(3); + } + + $this->Ln(5); // --- COMMENTAIRES --- $this->SetFont('Arial', 'B', 10); @@ -118,27 +262,6 @@ class EtatLieuxPdfService extends Fpdf $endY = $this->GetY(); // Box removed as requested $this->Ln(30); - - // --- 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->Cell(95, 40, "", 1, 0); - $this->Cell(95, 40, "", 1, 1); - - // DocuSeal tags invisible (si besoin d'intégration automatique plus tard) - $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); } // --- HELPER METHODS DUPLICATED FROM ContratPdfService --- diff --git a/src/Service/Signature/Client.php b/src/Service/Signature/Client.php index 46ae297..09ceb54 100644 --- a/src/Service/Signature/Client.php +++ b/src/Service/Signature/Client.php @@ -326,10 +326,6 @@ class Client public function createSubmissionEtatLieux(EtatLieux $etatLieux): void { - // Si déjà initié, on arrête (ou on pourrait retourner les liens existants) - if ($etatLieux->getSignIdCustomer()) { - return; - } $contrat = $etatLieux->getContrat(); $customer = $contrat->getCustomer(); diff --git a/templates/etl/edl.twig b/templates/etl/edl.twig index f9f679e..722f0b1 100644 --- a/templates/etl/edl.twig +++ b/templates/etl/edl.twig @@ -76,6 +76,57 @@ + {# POINTS DE CONTROLE #} +
+

Points de Contrôle

+ +
+ {% for reserve in mission.productReserves %} + {% set product = reserve.product %} + {% if product.productPointControlls|length > 0 %} +
+

+ + {{ product.name }} +

+
+ {% 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 %} + +
+
+
+ +
+
+

{{ point.name }}

+ +
+
+
+ {% endfor %} +
+
+ {% endif %} + {% endfor %} + + +
+
+ {# COMMENTAIRES #}

Commentaires

diff --git a/templates/etl/signed_entry_state.twig b/templates/etl/signed_entry_state.twig index f74599a..876623e 100644 --- a/templates/etl/signed_entry_state.twig +++ b/templates/etl/signed_entry_state.twig @@ -19,7 +19,12 @@

État des Lieux Terminé

-

Veuillez procéder à la signature du document.

+

Veuillez procéder à la signature du document.

+ + + + Voir le PDF (Actualiser) +
{# ACTIONS SIGNATURE #}