feat(etl): Implémente le dashboard dynamique et la gestion des missions

This commit is contained in:
Serreau Jovann
2026-02-06 12:21:38 +01:00
parent 5ac0b80af9
commit d92642d1d7
10 changed files with 514 additions and 37 deletions

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260206200000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add status and account relation to etat_lieux';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('ALTER TABLE etat_lieux ADD account_id INT DEFAULT NULL');
$this->addSql("ALTER TABLE etat_lieux ADD status VARCHAR(50) DEFAULT 'delivery_progress' NOT NULL");
$this->addSql('ALTER TABLE etat_lieux ADD CONSTRAINT FK_D71603599B6B5FBA FOREIGN KEY (account_id) REFERENCES account (id)');
$this->addSql('CREATE INDEX IDX_D71603599B6B5FBA ON etat_lieux (account_id)');
}
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 FOREIGN KEY FK_D71603599B6B5FBA');
$this->addSql('DROP INDEX IDX_D71603599B6B5FBA ON etat_lieux');
$this->addSql('ALTER TABLE etat_lieux DROP account_id');
$this->addSql('ALTER TABLE etat_lieux DROP status');
}
}

View File

@@ -0,0 +1,32 @@
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
/**
* Auto-generated Migration: Please modify to your needs!
*/
final class Version20260206210000 extends AbstractMigration
{
public function getDescription(): string
{
return 'Update default status for etat_lieux';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql("ALTER TABLE etat_lieux ALTER status SET DEFAULT 'delivery_ready'");
$this->addSql("UPDATE etat_lieux SET status = 'delivery_ready' WHERE status = 'delivery_progress'");
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql("ALTER TABLE etat_lieux ALTER status SET DEFAULT 'delivery_progress'");
}
}

View File

@@ -3,9 +3,13 @@
namespace App\Controller;
use App\Entity\Account;
use App\Entity\Contrats;
use App\Entity\ContratsPayments;
use App\Entity\EtatLieux;
use App\Entity\Prestaire;
use App\Form\PrestairePasswordType;
use App\Repository\ContratsRepository;
use App\Service\Mailer\Mailer;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\RedirectResponse;
@@ -29,19 +33,117 @@ class EtlController extends AbstractController
$missions = [];
$states = ['ready', 'pending'];
$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) {
// Admins see all active missions
$missions = $contratsRepository->findBy(['reservationState' => $states], ['dateAt' => 'ASC']);
$missions = $contratsRepository->findBy(['reservationState' => $states], ['dateAt' => 'ASC'], 5);
} elseif ($user instanceof Prestaire) {
// Providers see only their missions
$missions = $contratsRepository->findBy(['reservationState' => $states, 'prestataire' => $user], ['dateAt' => 'ASC']);
$missions = $contratsRepository->findBy(['reservationState' => $states, 'prestataire' => $user], ['dateAt' => 'ASC'], 5);
}
return $this->render('etl/home.twig', [
'missions' => $missions,
'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.');
}
return $this->render('etl/view.twig', [
'mission' => $contrat
]);
}
#[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/account', name: 'etl_account', methods: ['GET', 'POST'])]
public function eltAccount(
Request $request,

View File

@@ -75,6 +75,13 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, TwoF
*/
#[ORM\OneToMany(targetEntity: AuditLog::class, mappedBy: 'account')]
private Collection $auditLogs;
/**
* @var Collection<int, EtatLieux>
*/
#[ORM\OneToMany(targetEntity: EtatLieux::class, mappedBy: 'account')]
private Collection $etatLieuxes;
#[ORM\Column(type: 'string', nullable: true)]
private ?string $confirmationToken;
@@ -82,6 +89,7 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, TwoF
{
$this->accountLoginRegisters = new ArrayCollection();
$this->auditLogs = new ArrayCollection();
$this->etatLieuxes = new ArrayCollection();
}
public function getId(): ?int
@@ -89,6 +97,36 @@ class Account implements UserInterface, PasswordAuthenticatedUserInterface, TwoF
return $this->id;
}
/**
* @return Collection<int, EtatLieux>
*/
public function getEtatLieuxes(): Collection
{
return $this->etatLieuxes;
}
public function addEtatLieux(EtatLieux $etatLieux): static
{
if (!$this->etatLieuxes->contains($etatLieux)) {
$this->etatLieuxes->add($etatLieux);
$etatLieux->setAccount($this);
}
return $this;
}
public function removeEtatLieux(EtatLieux $etatLieux): static
{
if ($this->etatLieuxes->removeElement($etatLieux)) {
// set the owning side to null (unless already changed)
if ($etatLieux->getAccount() === $this) {
$etatLieux->setAccount(null);
}
}
return $this;
}
public function getUsername(): ?string
{
return $this->username;

View File

@@ -19,11 +19,41 @@ class EtatLieux
#[ORM\ManyToOne(inversedBy: 'etatLieuxes')]
private ?Prestaire $prestataire = null;
#[ORM\ManyToOne(inversedBy: 'etatLieuxes')]
private ?Account $account = null;
#[ORM\Column(length: 50, options: ['default' => 'delivery_ready'])]
private string $status = 'delivery_ready';
public function getId(): ?int
{
return $this->id;
}
public function getStatus(): string
{
return $this->status;
}
public function setStatus(string $status): static
{
$this->status = $status;
return $this;
}
public function getAccount(): ?Account
{
return $this->account;
}
public function setAccount(?Account $account): static
{
$this->account = $account;
return $this;
}
public function getContrat(): ?Contrats
{
return $this->contrat;

View File

@@ -50,7 +50,7 @@
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" /></svg>
<span class="text-[9px] font-bold uppercase tracking-widest">Accueil</span>
</a>
<a href="#" class="flex flex-col items-center gap-1 text-slate-400 hover:text-slate-600">
<a href="{{ path('etl_contrats') }}" class="flex flex-col items-center gap-1 text-slate-400 hover:text-slate-600">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg>
<span class="text-[9px] font-bold uppercase tracking-widest">Missions</span>
</a>

View File

@@ -0,0 +1,54 @@
{% extends 'etl/base.twig' %}
{% block title %}Mes Missions{% endblock %}
{% block body %}
<div class="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-black text-slate-900 tracking-tight">Mes Missions</h1>
<span class="bg-blue-100 text-blue-600 text-xs font-bold px-3 py-1 rounded-full">{{ missions|length }}</span>
</div>
<div class="space-y-4">
{% for mission in missions %}
<a href="{{ path('etl_contrat_view', {id: mission.id}) }}" class="block bg-white rounded-[1.5rem] p-5 border border-slate-100 shadow-sm hover:border-blue-100 transition-all group">
<div class="flex justify-between items-start mb-4">
<div>
<p class="text-[10px] font-black uppercase tracking-widest text-slate-400 mb-1">
{{ mission.dateAt|date('d/m H:i') }} <span class="text-slate-300 mx-1">➜</span> {{ mission.endAt|date('d/m H:i') }}
</p>
<h3 class="font-bold text-slate-900 group-hover:text-blue-600 transition-colors">{{ mission.addressEvent }}</h3>
<p class="text-xs text-slate-500">{{ mission.zipCodeEvent }} {{ mission.townEvent }}</p>
</div>
<span class="inline-flex items-center justify-center w-8 h-8 rounded-full bg-blue-50 text-blue-600 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7" /></svg>
</span>
</div>
<div class="flex items-center gap-3 pt-4 border-t border-slate-50">
<div class="flex flex-col">
<span class="text-[9px] font-bold text-slate-400 uppercase">Client</span>
<span class="text-xs font-bold text-slate-700">{{ mission.customer.surname }} {{ mission.customer.name }}</span>
</div>
<div class="ml-auto">
<span class="px-2 py-1 rounded-lg text-[9px] font-black uppercase tracking-wide
{{ mission.reservationState == 'ready' ? 'bg-emerald-50 text-emerald-600' : 'bg-amber-50 text-amber-600' }}">
{{ mission.reservationState }}
</span>
</div>
</div>
</a>
{% else %}
<div class="bg-white rounded-[2rem] border border-slate-100 p-8 text-center">
<div class="inline-flex p-4 bg-slate-50 rounded-full text-slate-300 mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg>
</div>
<p class="text-xs font-bold text-slate-900">Aucune mission</p>
<p class="text-[10px] text-slate-400 mt-1">Vous n'avez aucune mission assignée pour le moment.</p>
</div>
{% endfor %}
</div>
</div>
{% endblock %}

View File

@@ -15,11 +15,11 @@
<div class="mt-6 flex items-center gap-4">
<div class="bg-white/20 backdrop-blur-md rounded-xl px-4 py-2 flex flex-col">
<span class="text-[9px] font-bold uppercase tracking-wide opacity-80">Missions</span>
<span class="text-lg font-black">0</span>
<span class="text-lg font-black">{{ totalMissions }}</span>
</div>
<div class="bg-white/20 backdrop-blur-md rounded-xl px-4 py-2 flex flex-col">
<span class="text-[9px] font-bold uppercase tracking-wide opacity-80">À venir</span>
<span class="text-lg font-black">0</span>
<span class="text-lg font-black">{{ upcomingMissions }}</span>
</div>
</div>
</div>
@@ -28,37 +28,50 @@
<div class="space-y-4">
<h3 class="px-2 text-xs font-black text-slate-400 uppercase tracking-widest">Accès Rapide</h3>
<div class="grid grid-cols-2 gap-4">
<a href="#" class="bg-white p-5 rounded-[1.5rem] border border-slate-100 shadow-sm hover:border-blue-100 transition-all group active:scale-95">
<div class="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center text-blue-600 mb-3 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg>
<div class="grid grid-cols-2 gap-4">
<a href="{{ path('etl_contrats') }}" class="bg-white p-5 rounded-[1.5rem] border border-slate-100 shadow-sm hover:border-blue-100 transition-all group active:scale-95">
<div class="w-10 h-10 bg-blue-50 rounded-xl flex items-center justify-center text-blue-600 mb-3 group-hover:bg-blue-600 group-hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" /></svg>
</div>
<p class="text-xs font-bold text-slate-900">Mes Missions</p>
<p class="text-[9px] text-slate-400 font-medium mt-0.5">Planning & Détails</p>
</a>
<a href="{{ path('etl_contrats') }}" class="bg-white p-5 rounded-[1.5rem] border border-slate-100 shadow-sm hover:border-blue-100 transition-all group active:scale-95">
<div class="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center text-emerald-600 mb-3 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
</div>
<p class="text-xs font-bold text-slate-900">Validations</p>
<p class="text-[9px] text-slate-400 font-medium mt-0.5">États des lieux</p>
</a>
</div>
</div>
<p class="text-xs font-bold text-slate-900">Mes Missions</p>
<p class="text-[9px] text-slate-400 font-medium mt-0.5">Planning & Détails</p>
</a>
<a href="#" class="bg-white p-5 rounded-[1.5rem] border border-slate-100 shadow-sm hover:border-blue-100 transition-all group active:scale-95">
<div class="w-10 h-10 bg-emerald-50 rounded-xl flex items-center justify-center text-emerald-600 mb-3 group-hover:bg-emerald-600 group-hover:text-white transition-colors">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" /></svg>
{# UPCOMING LIST #}
<div class="space-y-4">
<h3 class="px-2 text-xs font-black text-slate-400 uppercase tracking-widest">Prochainement</h3>
{% if missions|length > 0 %}
<div class="space-y-3">
{% for mission in missions %}
<a href="{{ path('etl_contrat_view', {id: mission.id}) }}" class="block bg-white rounded-[1.5rem] p-4 border border-slate-100 flex items-center justify-between hover:bg-slate-50 transition-colors">
<div>
<p class="text-[9px] font-black uppercase text-blue-500 mb-0.5">
{{ mission.dateAt|date('d/m H:i') }} <span class="text-slate-300 mx-1">➜</span> {{ mission.endAt|date('d/m H:i') }}
</p>
<p class="text-xs font-bold text-slate-900">{{ mission.townEvent }}</p>
</div>
<span class="text-xs text-slate-400 font-mono">{{ mission.zipCodeEvent }}</span>
</a>
{% endfor %} </div>
{% else %} <div class="bg-white rounded-[2rem] border border-slate-100 p-8 text-center">
<div class="inline-flex p-4 bg-slate-50 rounded-full text-slate-300 mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
<p class="text-xs font-bold text-slate-900">Aucune mission à venir</p>
<p class="text-[10px] text-slate-400 mt-1">Votre planning est vide pour le moment.</p>
</div>
{% endif %}
</div>
<p class="text-xs font-bold text-slate-900">Validations</p>
<p class="text-[9px] text-slate-400 font-medium mt-0.5">États des lieux</p>
</a>
</div>
</div>
{# UPCOMING LIST (Placeholder) #}
<div class="space-y-4">
<h3 class="px-2 text-xs font-black text-slate-400 uppercase tracking-widest">Prochainement</h3>
<div class="bg-white rounded-[2rem] border border-slate-100 p-8 text-center">
<div class="inline-flex p-4 bg-slate-50 rounded-full text-slate-300 mb-3">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" /></svg>
</div>
<p class="text-xs font-bold text-slate-900">Aucune mission à venir</p>
<p class="text-[10px] text-slate-400 mt-1">Votre planning est vide pour le moment.</p>
</div>
</div>
</div>
{% endblock %}

151
templates/etl/view.twig Normal file
View File

@@ -0,0 +1,151 @@
{% extends 'etl/base.twig' %}
{% block title %}Mission #{{ mission.numReservation }}{% endblock %}
{% block body %}
<div class="space-y-6 animate-in fade-in slide-in-from-bottom-4 duration-500">
{# HEADER #}
<div class="flex items-center gap-4">
<a href="{{ path('etl_home') }}" class="w-10 h-10 bg-white rounded-xl border border-slate-100 flex items-center justify-center text-slate-400 hover:text-blue-600 transition-all shadow-sm">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7" /></svg>
</a>
<div>
<h1 class="text-xl font-black text-slate-900 tracking-tight">Détails Mission</h1>
<p class="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Réf: #{{ mission.numReservation }}</p>
</div>
</div>
{# ACTION LIVRAISON #}
{% if not mission.etatLieux or mission.etatLieux.status == 'delivery_ready' %}
<form action="{{ path('etl_mission_start', {id: mission.id}) }}" method="post">
<button type="submit" class="w-full py-4 bg-indigo-600 hover:bg-indigo-500 text-white rounded-2xl font-black uppercase text-sm tracking-widest shadow-lg shadow-indigo-600/30 transition-all active:scale-95 flex items-center justify-center gap-3">
<svg class="w-5 h-5 animate-bounce" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" /></svg>
Commencer la livraison
</button>
</form>
{% elseif mission.etatLieux.status == 'delivery_progress' %}
<div class="w-full py-4 bg-amber-500/10 border border-amber-500/20 text-amber-500 rounded-2xl font-black uppercase text-sm tracking-widest flex items-center justify-center gap-3">
<span class="relative flex h-3 w-3">
<span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-amber-400 opacity-75"></span>
<span class="relative inline-flex rounded-full h-3 w-3 bg-amber-500"></span>
</span>
Livraison en cours
</div>
{% endif %}
{# DATES #}
<div class="bg-blue-600 rounded-[2rem] p-6 text-white shadow-xl shadow-blue-600/20 relative overflow-hidden">
<div class="absolute top-0 right-0 -mr-8 -mt-8 w-32 h-32 bg-white/10 rounded-full blur-2xl"></div>
<div class="flex justify-between items-center relative z-10">
<div class="text-center">
<p class="text-[9px] font-black uppercase tracking-widest opacity-70 mb-1">Début</p>
<p class="text-lg font-black">{{ mission.dateAt|date('d/m') }}</p>
<p class="text-[10px] font-bold">{{ mission.dateAt|date('H:i') }}</p>
</div>
<div class="h-8 w-px bg-white/20"></div>
<div class="text-center">
<p class="text-[9px] font-black uppercase tracking-widest opacity-70 mb-1">Fin</p>
<p class="text-lg font-black">{{ mission.endAt|date('d/m') }}</p>
<p class="text-[10px] font-bold">{{ mission.endAt|date('H:i') }}</p>
</div>
</div>
</div>
{# CLIENT #}
<div class="bg-white rounded-[2rem] p-6 border border-slate-100 shadow-sm">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">Client</h3>
<div class="flex items-center justify-between">
<div>
<p class="font-bold text-slate-900 text-lg">{{ mission.customer.surname }} {{ mission.customer.name }}</p>
</div>
<a href="tel:{{ mission.customer.phone }}" class="w-12 h-12 bg-emerald-50 text-emerald-600 rounded-full flex items-center justify-center hover:bg-emerald-600 hover:text-white transition-all shadow-lg shadow-emerald-600/20">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 5a2 2 0 012-2h3.28a1 1 0 01.948.684l1.498 4.493a1 1 0 01-.502 1.21l-2.257 1.13a11.042 11.042 0 005.516 5.516l1.13-2.257a1 1 0 011.21-.502l4.493 1.498a1 1 0 01.684.949V19a2 2 0 01-2 2h-1C9.716 21 3 14.284 3 6V5z" /></svg>
</a>
</div>
</div>
{# ADRESSE & GPS #}
<div class="bg-white rounded-[2rem] p-6 border border-slate-100 shadow-sm">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">Lieu de l'événement</h3>
<div class="mb-6">
<p class="font-bold text-slate-900 text-sm leading-relaxed">
{{ mission.addressEvent }}<br>
{% if mission.address2Event %}{{ mission.address2Event }}<br>{% endif %}
{{ mission.zipCodeEvent }} {{ mission.townEvent }}
</p>
</div>
<div class="grid grid-cols-2 gap-4">
<a href="https://www.google.com/maps/dir/?api=1&destination={{ (mission.addressEvent ~ ' ' ~ mission.zipCodeEvent ~ ' ' ~ mission.townEvent)|url_encode }}" target="_blank" class="flex items-center justify-center gap-2 py-3 bg-slate-50 hover:bg-blue-50 text-slate-600 hover:text-blue-600 rounded-xl border border-slate-200 transition-all text-xs font-bold uppercase tracking-wide">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zm0 9.5c-1.38 0-2.5-1.12-2.5-2.5s1.12-2.5 2.5-2.5 2.5 1.12 2.5 2.5-1.12 2.5-2.5 2.5z"/></svg>
Maps
</a>
<a href="https://waze.com/ul?q={{ (mission.addressEvent ~ ' ' ~ mission.zipCodeEvent ~ ' ' ~ mission.townEvent)|url_encode }}&navigate=yes" target="_blank" class="flex items-center justify-center gap-2 py-3 bg-slate-50 hover:bg-indigo-50 text-slate-600 hover:text-indigo-600 rounded-xl border border-slate-200 transition-all text-xs font-bold uppercase tracking-wide">
<svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor"><path d="M12.0001 2.25C17.385 2.25 21.7501 6.61509 21.7501 12C21.7501 17.3849 17.385 21.75 12.0001 21.75C6.61522 21.75 2.25012 17.3849 2.25012 12C2.25012 6.61509 6.61522 2.25 12.0001 2.25ZM12.0001 19.5C13.435 19.5 14.6713 18.7885 15.3932 17.7083L14.7663 16.0373C14.1867 16.7118 13.1678 17.25 12.0001 17.25C9.79098 17.25 8.00012 15.4591 8.00012 13.25C8.00012 11.0409 9.79098 9.25 12.0001 9.25C14.1524 9.25 15.9082 10.9485 15.995 13.0763L16.0001 13.25L17.8751 13.25C17.8751 10.0053 15.2448 7.375 12.0001 7.375C8.75545 7.375 6.12512 10.0053 6.12512 13.25C6.12512 16.4947 8.75545 19.125 12.0001 19.125V19.5Z" /></svg>
Waze
</a>
</div>
</div>
{# PRODUCTS & OPTIONS #}
<div class="bg-white rounded-[2rem] p-6 border border-slate-100 shadow-sm">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">Matériel & Options</h3>
<div class="space-y-3">
{% for line in mission.contratsLines %}
{% if 'livraison' not in line.name|lower %}
<div class="flex items-center gap-3 p-3 bg-slate-50 rounded-xl">
<div class="w-8 h-8 bg-blue-100 text-blue-600 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 7l-8-4-8 4m16 0l-8 4m8-4v10l-8 4m0-10L4 7m8 4v10M4 7v10l8 4" /></svg>
</div>
<div>
<p class="text-sm font-bold text-slate-900">{{ line.name }}</p>
{% if line.caution > 0 %}
<p class="text-[9px] text-slate-400 font-medium">Caution: {{ line.caution }}€</p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
{% for option in mission.contratsOptions %}
{% if 'livraison' not in option.name|lower %}
<div class="flex items-center gap-3 p-3 bg-slate-50 rounded-xl border border-slate-100">
<div class="w-8 h-8 bg-purple-100 text-purple-600 rounded-lg flex items-center justify-center shrink-0">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" /></svg>
</div>
<div>
<p class="text-sm font-bold text-slate-900">{{ option.name }}</p>
{% if option.details %}
<p class="text-[9px] text-slate-400 font-medium">{{ option.details }}</p>
{% endif %}
</div>
</div>
{% endif %}
{% endfor %}
</div>
</div>
{# DETAILS / NOTES #}
{% if mission.details or mission.notes %}
<div class="bg-white rounded-[2rem] p-6 border border-slate-100 shadow-sm">
<h3 class="text-xs font-black text-slate-400 uppercase tracking-widest mb-4">Notes & Détails</h3>
{% if mission.details %}
<div class="mb-4">
<p class="text-[9px] font-bold text-slate-400 uppercase mb-1">Détails</p>
<p class="text-sm text-slate-700">{{ mission.details }}</p>
</div>
{% endif %}
{% if mission.notes %}
<div>
<p class="text-[9px] font-bold text-slate-400 uppercase mb-1">Notes internes</p>
<p class="text-sm text-slate-700 italic">{{ mission.notes }}</p>
</div>
{% endif %}
</div>
{% endif %}
</div>
{% endblock %}

View File

@@ -0,0 +1,20 @@
{% extends 'mails/base.twig' %}
{% block content %}
<mj-text>
Bonjour <strong>{{ datas.contrat.customer.surname }} {{ datas.contrat.customer.name }}</strong>,
</mj-text>
<mj-text>
Nous avons le plaisir de vous informer que votre commande liée à la réservation <strong>#{{ datas.contrat.numReservation }}</strong> est en route !
</mj-text>
<mj-text>
Notre équipe logistique a pris en charge le matériel et se dirige vers le lieu de livraison prévu :
</mj-text>
<mj-text align="center" font-style="italic" color="#555">
{{ datas.contrat.addressEvent }}<br>
{{ datas.contrat.zipCodeEvent }} {{ datas.contrat.townEvent }}
</mj-text>
<mj-text>
Merci de vous assurer qu'une personne est présente pour réceptionner la livraison.
</mj-text>
{% endblock %}