Files
ludikevent_crm/src/Controller/EtlController.php
Serreau Jovann 2c43d8f0ce fix: forcer session save et retry automatique pour SSO invalid state
Sauvegarde explicite de la session avant la redirection OAuth pour
garantir la persistance du state parameter. Retry automatique du
flow SSO en cas d'InvalidStateAuthenticationException.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 09:03:23 +01:00

1246 lines
49 KiB
PHP

<?php
namespace App\Controller;
use App\Entity\Account;
use App\Entity\Contrats;
use App\Entity\ContratsPayments;
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;
use App\Repository\ContratsRepository;
use App\Service\Mailer\Mailer;
use App\Service\Pdf\EtatLieuxPdfService;
use App\Service\Signature\Client as SignatureClient;
use App\Service\Stripe\Client as StripeClient;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\HttpFoundation\RedirectResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Mime\Part\DataPart;
use Symfony\Component\Mime\Part\File as MimeFile;
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
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
{
#[Route('/etl', name: 'etl_home')]
public function eltHome(EntityManagerInterface $entityManager,ContratsRepository $contratsRepository): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$missions = [];
$states = ['ready', 'pending', 'progress'];
$qb = $contratsRepository->createQueryBuilder('c');
$qb->select('count(c.id)');
if ($user instanceof Prestaire) {
$qb->andWhere('c.prestataire = :user')->setParameter('user', $user);
}
$totalMissions = $qb->getQuery()->getSingleScalarResult();
$qb = $contratsRepository->createQueryBuilder('c');
$qb->select('count(c.id)');
$qb->andWhere('c.dateAt >= :now')->setParameter('now', new \DateTime());
$qb->andWhere('c.reservationState IN (:states)')->setParameter('states', $states);
if ($user instanceof Prestaire) {
$qb->andWhere('c.prestataire = :user')->setParameter('user', $user);
}
$upcomingMissions = $qb->getQuery()->getSingleScalarResult();
if ($user instanceof Account) {
$missions = $contratsRepository->findBy(['reservationState' => $states], ['dateAt' => 'ASC'], 5);
} elseif ($user instanceof Prestaire) {
$missions = $contratsRepository->findBy(['reservationState' => $states, 'prestataire' => $user], ['dateAt' => 'ASC'], 5);
}
$list = [];
foreach ($missions as $mission) {
if($mission->isSigned()) {
$pl = $entityManager->getRepository(ContratsPayments::class)->findOneBy(['type'=>'accompte','contrat'=>$mission]);
if($pl instanceof ContratsPayments &&$pl->getState() == "complete") {
$etatleiux = $mission->getEtatLieux();
if(!is_null($etatleiux)) {
if($etatleiux->getStatus() != "edl_return_done" && $etatleiux->getStatus() != "edl_return_refused"){
$list[] = $mission;
}
} else {
$list[] = $mission;
}
}
}
}
return $this->render('etl/home.twig', [
'missions' => $list,
'totalMissions' => $totalMissions,
'upcomingMissions' => $upcomingMissions
]);
}
#[Route('/etl/contrats', name: 'etl_contrats')]
public function eltContrats(ContratsRepository $contratsRepository): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$missions = [];
$states = ['ready', 'pending'];
if ($user instanceof Account) {
$missions = $contratsRepository->findBy(['reservationState' => $states], ['dateAt' => 'ASC']);
} elseif ($user instanceof Prestaire) {
$missions = $contratsRepository->findBy(['reservationState' => $states, 'prestataire' => $user], ['dateAt' => 'ASC']);
}
return $this->render('etl/contrats.twig', [
'missions' => $missions
]);
}
#[Route('/etl/mission/{id}', name: 'etl_contrat_view', methods: ['GET'])]
public function eltContratView(Contrats $contrat): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
// Security check for Prestaire
if ($user instanceof Prestaire && $contrat->getPrestataire() !== $user) {
throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette mission.');
}
// Calculate Totals
$totalHt = 0;
$totalCaution = 0;
$days = ($contrat->getDateAt() && $contrat->getEndAt()) ? ($contrat->getDateAt()->diff($contrat->getEndAt())->days + 1) : 1;
foreach ($contrat->getContratsLines() as $line) {
$totalHt += $line->getPrice1DayHt() + ($line->getPriceSupDayHt() * max(0, $days - 1));
$totalCaution += $line->getCaution();
}
foreach ($contrat->getContratsOptions() as $option) {
$totalHt += $option->getPrice();
}
$dejaPaye = 0;
foreach ($contrat->getContratsPayments() as $p) {
if ($p->getState() === 'complete' && $p->getType() !== 'caution') {
$dejaPaye += $p->getAmount();
}
}
$solde = $totalHt - $dejaPaye;
// Check if Caution is already paid
$cautionPaid = false;
foreach ($contrat->getContratsPayments() as $p) {
if ($p->getType() === 'caution' && $p->getState() === 'complete') {
$cautionPaid = true;
break;
}
}
return $this->render('etl/view.twig', [
'mission' => $contrat,
'totalCaution' => $totalCaution,
'solde' => $solde,
'cautionPaid' => $cautionPaid
]);
}
#[Route('/etl/mission/{id}/start', name: 'etl_mission_start', methods: ['POST'])]
public function eltMissionStart(Contrats $contrat, EntityManagerInterface $em, Mailer $mailer): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
if ($user instanceof Prestaire && $contrat->getPrestataire() !== $user) {
throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette mission.');
}
$etatLieux = $contrat->getEtatLieux();
if (!$etatLieux) {
$etatLieux = new EtatLieux();
$etatLieux->setContrat($contrat);
if ($user instanceof Prestaire) {
$etatLieux->setPrestataire($user);
} elseif ($user instanceof Account) {
$etatLieux->setAccount($user);
}
$em->persist($etatLieux);
}
$etatLieux->setStatus('delivery_progress');
$em->flush();
// Notification client
if ($contrat->getCustomer()) {
$mailer->send(
$contrat->getCustomer()->getEmail(),
$contrat->getCustomer()->getName(),
"Votre commande est en route ! - #" . $contrat->getNumReservation(),
"mails/customer/delivery_start.twig",
['contrat' => $contrat]
);
}
$this->addFlash('success', 'Livraison démarrée, le client a été notifié.');
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/finish', name: 'etl_mission_finish', methods: ['POST'])]
public function eltMissionFinish(Contrats $contrat, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
if ($user instanceof Prestaire && $contrat->getPrestataire() !== $user) {
throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette mission.');
}
$etatLieux = $contrat->getEtatLieux();
if ($etatLieux) {
$etatLieux->setStatus('delivery_done');
$em->flush();
$this->addFlash('success', 'Livraison terminée et confirmée.');
}
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/caution', name: 'etl_mission_caution', methods: ['POST'])]
public function eltMissionCaution(Contrats $contrat, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
// Check already paid
foreach ($contrat->getContratsPayments() as $p) {
if ($p->getType() === 'caution' && $p->getState() === 'complete') {
$this->addFlash('warning', 'Caution déjà validée.');
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
}
// Calculate Caution Amount
$totalCaution = 0;
$days = ($contrat->getDateAt() && $contrat->getEndAt()) ? ($contrat->getDateAt()->diff($contrat->getEndAt())->days + 1) : 1;
foreach ($contrat->getContratsLines() as $line) {
$totalCaution += $line->getCaution();
}
$payment = new ContratsPayments();
$payment->setContrat($contrat)
->setType('caution')
->setAmount($totalCaution)
->setState('complete')
->setPaymentAt(new \DateTimeImmutable())
->setValidateAt(new \DateTimeImmutable())
->setCard(['type' => 'manuel', 'method' => 'Prestataire'])
->setPaymentId("CAUTION-" . uniqid());
$em->persist($payment);
$em->flush();
$this->addFlash('success', 'Caution validée.');
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/confirme', name: 'elt_mission_confirme', methods: ['POST'])]
public function eltMissionConfirme(Contrats $contrat, StripeClient $stripeClient, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
// Calculate Solde
$totalHt = 0;
$days = ($contrat->getDateAt() && $contrat->getEndAt()) ? ($contrat->getDateAt()->diff($contrat->getEndAt())->days + 1) : 1;
foreach ($contrat->getContratsLines() as $line) {
$totalHt += $line->getPrice1DayHt() + ($line->getPriceSupDayHt() * max(0, $days - 1));
}
foreach ($contrat->getContratsOptions() as $option) {
$totalHt += $option->getPrice();
}
$dejaPaye = 0;
foreach ($contrat->getContratsPayments() as $p) {
if ($p->getState() === 'complete' && $p->getType() !== 'caution') {
$dejaPaye += $p->getAmount();
}
}
$solde = $totalHt - $dejaPaye;
if ($solde <= 0) {
$this->addFlash('info', 'Le solde est déjà réglé.');
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
// Create Payment Intent/Session using StripeClient (Using createPaymentSolde logic but manual or new method)
// Since createPaymentSolde redirects to specific success URL, I might need to adjust or create a generic one.
// Assuming createPaymentSolde can be reused but we might want custom success URL?
// Actually, createPaymentSolde sets success_url to /contrat/payment/success/... which is the customer success page.
// That might be fine, or we want redirection back to ETL?
// If we want redirection back to ETL, we need a new method in StripeClient or pass options.
// For now, I will use getNativeClient to build session manually to control success_url.
try {
$stripe = $stripeClient->getNativeClient();
$customer = $contrat->getCustomer();
// Ensure Stripe Customer exists (Controller helper or reuse existing logic if possible)
if (!$customer->getCustomerId()) {
$stripeClient->createCustomer($customer);
$em->flush();
}
$session = $stripe->checkout->sessions->create([
'customer' => $customer->getCustomerId(),
'line_items' => [[
'price_data' => [
'currency' => 'eur',
'product_data' => [
'name' => 'Solde Réservation #' . $contrat->getNumReservation(),
'description' => 'Règlement du solde via interface prestataire',
],
'unit_amount' => (int)round($solde * 100),
],
'quantity' => 1,
]],
'mode' => 'payment',
'success_url' => $this->generateUrl('etl_contrat_view', ['id' => $contrat->getId()], UrlGeneratorInterface::ABSOLUTE_URL), // Back to ETL view
'cancel_url' => $this->generateUrl('etl_contrat_view', ['id' => $contrat->getId()], UrlGeneratorInterface::ABSOLUTE_URL),
'metadata' => [
'contrat_id' => $contrat->getId(),
'type' => 'etl_payment' // Special type to trigger specific webhook logic
]
]);
// Persist pending payment
$payment = new ContratsPayments();
$payment->setContrat($contrat)
->setType('etl_payment') // Or 'solde' if we want it standard
->setAmount($solde)
->setState('created')
->setPaymentAt(new \DateTimeImmutable())
->setPaymentId($session->id);
$em->persist($payment);
$em->flush();
return new RedirectResponse($session->url);
} catch (\Exception $e) {
$this->addFlash('error', 'Erreur Stripe: ' . $e->getMessage());
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
}
#[Route('/etl/mission/{id}/manual-pay', name: 'etl_mission_manual_pay', methods: ['POST'])]
public function eltMissionManualPayment(Contrats $contrat, Request $request, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$method = $request->request->get('method');
$amount = (float)$request->request->get('amount'); // Optional, or calculate solde
if ($amount <= 0) {
// Calculate Solde if not provided
$totalHt = 0;
$days = ($contrat->getDateAt() && $contrat->getEndAt()) ? ($contrat->getDateAt()->diff($contrat->getEndAt())->days + 1) : 1;
foreach ($contrat->getContratsLines() as $line) {
$totalHt += $line->getPrice1DayHt() + ($line->getPriceSupDayHt() * max(0, $days - 1));
}
foreach ($contrat->getContratsOptions() as $option) {
$totalHt += $option->getPrice();
}
$dejaPaye = 0;
foreach ($contrat->getContratsPayments() as $p) {
if ($p->getState() === 'complete' && $p->getType() !== 'caution') {
$dejaPaye += $p->getAmount();
}
}
$amount = $totalHt - $dejaPaye;
}
if ($amount > 0) {
$payment = new ContratsPayments();
$payment->setContrat($contrat)
->setType('etl_payment')
->setAmount($amount)
->setState('complete')
->setPaymentAt(new \DateTimeImmutable())
->setValidateAt(new \DateTimeImmutable())
->setCard(['type' => 'manuel', 'method' => $method])
->setPaymentId("MANUAL-" . uniqid());
$em->persist($payment);
$em->flush();
$this->addFlash('success', 'Paiement manuel enregistré.');
}
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/edl/start', name: 'etl_mission_edl_start', methods: ['POST'])]
public function eltMissionEdlStart(Contrats $contrat, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$etatLieux = $contrat->getEtatLieux();
if ($etatLieux) {
$etatLieux->setStatus('edl_progress');
$em->flush();
$this->addFlash('success', 'État des lieux commencé.');
}
return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/edl', name: 'etl_mission_edl', methods: ['GET'])]
public function eltEdl(Contrats $contrat): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
// Security check
if ($user instanceof Prestaire && $contrat->getPrestataire() !== $user) {
throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette mission.');
}
return $this->render('etl/edl.twig', [
'mission' => $contrat,
'etatLieux' => $contrat->getEtatLieux()
]);
}
#[Route('/etl/mission/{id}/edl/return', name: 'etl_mission_edl_return', methods: ['GET'])]
public function eltEdlReturn(Contrats $contrat): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
// Security check
if ($user instanceof Prestaire && $contrat->getPrestataire() !== $user) {
throw $this->createAccessDeniedException('Vous n\'avez pas accès à cette mission.');
}
return $this->render('etl/edl.twig', [
'mission' => $contrat,
'etatLieux' => $contrat->getEtatLieux(),
'isReturnEdl' => true // Flag to indicate it\'s a return EDL
]);
}
#[Route('/etl/mission/{id}/edl/comment', name: 'etl_edl_add_comment', methods: ['POST'])]
public function eltEdlAddComment(Contrats $contrat, Request $request, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$content = $request->request->get('content');
if ($content) {
$etatLieux = $contrat->getEtatLieux();
if($etatLieux->getStatus() == "return_edl_progress") {
$comment = new EtatLieuxReturnComment();
} else {
$comment = new EtatLieuxComment();
}
$comment->setContent($content);
$comment->setEtatLieux($etatLieux);
$em->persist($comment);
$em->flush();
$this->addFlash('success', 'Commentaire ajouté.');
}
return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]);
}
#[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;
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);
}
}
}
}
$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();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$photos = $request->files->get('photos');
$videos = $request->files->get('videos');
$etatLieux = $contrat->getEtatLieux();
$hasFiles = false;
if ($photos) {
if (!is_array($photos)) $photos = [$photos];
foreach ($photos as $uploadedFile) {
if ($uploadedFile instanceof UploadedFile) {
$this->compressImage($uploadedFile);
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;
}
}
}
}
if ($videos) {
if (!is_array($videos)) $videos = [$videos];
foreach ($videos as $uploadedFile) {
if ($uploadedFile instanceof UploadedFile) {
$this->compressVideo($uploadedFile);
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;
}
}
}
}
if ($hasFiles) {
$em->flush();
$this->addFlash('success', 'Fichiers ajoutés.');
}
return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/edl/file/{fileId}/delete', name: 'etl_edl_delete_file', methods: ['POST'])]
public function eltEdlDeleteFile(Contrats $contrat, int $fileId, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$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) {
$em->remove($file);
$em->flush();
$this->addFlash('success', 'Fichier supprimé.');
}
return $this->redirectToRoute('etl_mission_edl', ['id' => $contrat->getId()]);
}
#[Route('/etl/mission/{id}/edl/finish', name: 'etl_edl_finish', methods: ['POST'])]
public function eltEdlFinish(Contrats $contrat, EntityManagerInterface $em, KernelInterface $kernel, SignatureClient $signatureClient): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$etatLieux = $contrat->getEtatLieux();
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()]);
}
#[Route('/etl/mission/{id}/edl/return/start', name: 'etl_mission_edl_return_start', methods: ['POST'])]
public function eltMissionEdlReturnStart(Contrats $contrat, EntityManagerInterface $em): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
if ($user instanceof Prestaire && $contrat->getPrestataire() !== $user) {
throw $this->createAccessDeniedException("Vous n'avez pas accès à cette mission.");
}
$etatLieux = $contrat->getEtatLieux();
if ($etatLieux) {
$etatLieux->setStatus('return_edl_progress');
$em->flush();
$this->addFlash('success', 'État des lieux de retour commencé.');
}
return $this->redirectToRoute('etl_mission_edl_return', ['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');
}
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');
}
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
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$etatLieux = $contrat->getEtatLieux();
$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->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) {
}
}
}
return $this->render('etl/signed_entry_state.twig', [
'mission' => $contrat,
'etatLieux' => $etatLieux,
'providerSigned' => $providerSigned,
'customerSigned' => $customerSigned
]);
}
#[Route('/etl/sign/provider/{id}', name: 'etl_sign_provider', methods: ['GET'])]
public function eltSignProvider(EtatLieux $etatLieux, SignatureClient $signatureClient): Response
{
// Redirect to DocuSeal URL for Provider
$url = $signatureClient->getSigningUrl($etatLieux, 'Ludikevent'); // Role name from PDF
if ($url) {
return new RedirectResponse($url);
}
$this->addFlash('error', 'Lien de signature non disponible.');
return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $etatLieux->getContrat()->getId()]);
}
#[Route('/etl/sign/customer/{id}', name: 'etl_sign_customer', methods: ['GET'])]
public function eltSignCustomer(EtatLieux $etatLieux, SignatureClient $signatureClient): Response
{
// Redirect to DocuSeal URL for Customer
$url = $signatureClient->getSigningUrl($etatLieux, 'Client'); // Role name from PDF
if ($url) {
return new RedirectResponse($url);
}
$this->addFlash('error', 'Lien de signature non disponible.');
return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $etatLieux->getContrat()->getId()]);
}
#[Route('/etl/mission/{id}/edl/close', name: 'etl_edl_close', methods: ['POST'])]
public function eltMissionCloseEdl(Contrats $contrat, SignatureClient $signatureClient, EntityManagerInterface $em, Mailer $mailer, StorageInterface $storage): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
$etatLieux = $contrat->getEtatLieux();
if($etatLieux->getStatus() == "edl_return_done") {
if (!$etatLieux || !$etatLieux->getSignIdReturn() || !$etatLieux->getSignIdCustomerReturn()) {
$this->addFlash('error', 'Signatures manquantes.');
return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $contrat->getId()]);
}
// Get signed documents
$sub = $signatureClient->getSubmiter($etatLieux->getSignIdReturn());
$submissionId = $sub['submission_id'];
$submission = $signatureClient->getSubmition($submissionId);
$signedPdfUrl = $submission['documents'][0]['url'] ?? null;
$auditUrl = $submission['audit_log_url'] ?? null;
if ($signedPdfUrl) {
$tmpPath = sys_get_temp_dir() . '/edl_retour_signed_' . $contrat->getId() . '.pdf';
file_put_contents($tmpPath, file_get_contents($signedPdfUrl));
$file = new UploadedFile($tmpPath, 'edl_retour_signed_.pdf', 'application/pdf', null, true);
$etatLieux->setEtatLieuxSignReturnFile($file);
}
if ($auditUrl) {
$tmpPathAudit = sys_get_temp_dir() . '/edl_retour_audit_signed_' . $contrat->getId() . '.pdf';
file_put_contents($tmpPathAudit, file_get_contents($auditUrl));
$file = new UploadedFile($tmpPathAudit, 'edl_retour_audit_signed_.pdf', 'application/pdf', null, true);
$etatLieux->setEtatLieuxAuditReturnFile($file);
}
$etatLieux->setUpdatedAt(new \DateTimeImmutable());
$etatLieux->setStatus('edl_return_finised');
$contrat->setReservationState('finished');
$em->flush();
// Emails
$recipients = [
$contrat->getCustomer()->getEmail(),
'contact@ludikevent.fr'
];
if ($etatLieux->getPrestataire()) {
$recipients[] = $etatLieux->getPrestataire()->getEmail();
}
$attachments = [];
// Try resolve path from Vich
$signPath = $storage->resolvePath($etatLieux, 'etatLieuxSignReturnFile');
// 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_retour_signe.pdf');
} elseif (isset($tmpPath) && file_exists($tmpPath)) {
$attachments[] = DataPart::fromPath($tmpPath, 'Etat_des_retour_signe.pdf');
}
$auditPath = $storage->resolvePath($etatLieux, 'etatLieuxAuditReturnFile');
if ($auditPath && file_exists($auditPath)) {
$attachments[] = DataPart::fromPath($auditPath, 'Audit_Etat_des_retour_signe.pdf');
} elseif (isset($tmpPathAudit) && file_exists($tmpPathAudit)) {
$attachments[] = DataPart::fromPath($tmpPathAudit, 'Audit_Etat_des_retour_signe.pdf');
}
foreach (array_unique($recipients) as $email) {
$mailer->send(
$email,
'Destinataire',
"État des lieux validé - #" . $contrat->getNumReservation(),
"mails/etl/edl_retour_confirmation.twig",
[
'contrat' => $contrat,
'etatLieux' => $etatLieux
],
$attachments
);
}
$this->addFlash('success', 'État des lieux clôturé et envoyé.');
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
} else {
if (!$etatLieux || !$etatLieux->getSignIdDelivery() || !$etatLieux->getSignIdCustomer()) {
$this->addFlash('error', 'Signatures manquantes.');
return $this->redirectToRoute('etl_mission_signed_entry_state', ['id' => $contrat->getId()]);
}
// Get signed documents
$sub = $signatureClient->getSubmiter($etatLieux->getSignIdDelivery());
$submissionId = $sub['submission_id'];
$submission = $signatureClient->getSubmition($submissionId);
$signedPdfUrl = $submission['documents'][0]['url'] ?? null;
$auditUrl = $submission['audit_log_url'] ?? null;
if ($signedPdfUrl) {
$tmpPath = sys_get_temp_dir() . '/edl_signed_' . $contrat->getId() . '.pdf';
file_put_contents($tmpPath, file_get_contents($signedPdfUrl));
$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());
$etatLieux->setStatus('edl_validated');
$contrat->setReservationState('progress');
$em->flush();
// Emails
$recipients = [
$contrat->getCustomer()->getEmail(),
'contact@ludikevent.fr'
];
if ($etatLieux->getPrestataire()) {
$recipients[] = $etatLieux->getPrestataire()->getEmail();
}
$attachments = [];
// 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,
'Destinataire',
"État des lieux validé - #" . $contrat->getNumReservation(),
"mails/etl/edl_confirmation.twig",
[
'contrat' => $contrat,
'etatLieux' => $etatLieux
],
$attachments
);
}
$this->addFlash('success', 'État des lieux clôturé et envoyé.');
return $this->redirectToRoute('etl_contrat_view', ['id' => $contrat->getId()]);
}
}
#[Route('/etl/account', name: 'etl_account', methods: ['GET', 'POST'])]
public function eltAccount(
Request $request,
UserPasswordHasherInterface $passwordHasher,
EntityManagerInterface $entityManager
): Response
{
$user = $this->getUser();
if (!$user) {
return $this->redirectToRoute('etl_login');
}
if ($user instanceof Account) {
$this->addFlash('warning', 'Les administrateurs ne peuvent pas modifier leur mot de passe ici.');
return $this->redirectToRoute('etl_home');
}
$form = $this->createForm(PrestairePasswordType::class, $user);
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$hashedPassword = $passwordHasher->hashPassword(
$user,
$form->get('password')->getData()
);
$user->setPassword($hashedPassword);
$entityManager->flush();
$this->addFlash('success', 'Votre mot de passe a été modifié avec succès.');
return $this->redirectToRoute('etl_account');
}
return $this->render('etl/account.twig', [
'form' => $form->createView(),
]);
}
#[Route('/etl/connexion', name: 'etl_login')]
public function eltLogin(AuthenticationUtils $authenticationUtils): Response
{
if ($this->getUser()) {
return $this->redirectToRoute('etl_home');
}
return $this->render('etl/login.twig', [
'last_username' => $authenticationUtils->getLastUsername(),
'error' => $authenticationUtils->getLastAuthenticationError()
]);
}
#[Route('/etl/logout', name: 'elt_logout')]
public function eltLogout(): Response
{
// This method can be blank - it will be intercepted by the logout key on your firewall
return $this->redirectToRoute('etl_login');
}
#[Route('/etl/connect/keycloak', name: 'connect_keycloak_etl_start')]
public function connectKeycloakEtlStart(ClientRegistry $clientRegistry, Request $request): Response
{
$response = $clientRegistry
->getClient('keycloak_etl')
->redirect(['openid', 'profile', 'email']);
$response->headers->set('Cache-Control', 'no-store, no-cache, must-revalidate');
$response->headers->set('Pragma', 'no-cache');
$request->getSession()->save();
return $response;
}
#[Route('/etl/oauth/sso', name: 'connect_keycloak_etl_check')]
public function connectKeycloakEtlCheck(): Response
{
return new Response('Auth check', 200); // Intercepted by authenticator
}
private function compressImage(UploadedFile $file): UploadedFile
{
if (!extension_loaded('gd')) {
return $file;
}
$mime = $file->getMimeType();
$path = $file->getPathname();
// Simple compression logic
switch ($mime) {
case 'image/jpeg':
$image = @imagecreatefromjpeg($path);
if ($image) {
imagejpeg($image, $path, 75); // Quality 75
imagedestroy($image);
}
break;
case 'image/png':
$image = @imagecreatefrompng($path);
if ($image) {
// PNG compression 0-9
imagepng($image, $path, 6);
imagedestroy($image);
}
break;
}
return $file;
}
private function compressVideo(UploadedFile $file): UploadedFile
{
// Check if ffmpeg is available
$ffmpegPath = shell_exec('which ffmpeg');
if (empty($ffmpegPath)) {
return $file;
}
$inputPath = $file->getPathname();
$outputPath = $inputPath . '_compressed.mp4';
// Compress video using ffmpeg (CRF 28 for reasonable quality/size trade-off)
// -y to overwrite, -vcodec libx264, -crf 28, -preset fast
$command = sprintf(
'ffmpeg -y -i %s -vcodec libx264 -crf 28 -preset fast %s 2>&1',
escapeshellarg($inputPath),
escapeshellarg($outputPath)
);
exec($command, $output, $returnVar);
if ($returnVar === 0 && file_exists($outputPath)) {
// Replace original file with compressed one
if (rename($outputPath, $inputPath)) {
// Success
} else {
// Fallback cleanup
@unlink($outputPath);
}
}
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,
StorageInterface $storage,
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()]);
}
}