```
✨ feat(env): Met à jour les URLs ngrok pour l'environnement. ✨ feat(Prestaire): Ajoute contrainte d'unicité email et relations Contrats/OrderSession. ✨ feat(OrderSession): Ajoute une relation ManyToOne vers Prestaire. ✨ feat(Contrats): Ajoute une relation ManyToOne vers Prestaire. 🐛 fix(SignatureController): Corrige la création de contrat à partir du devis signé. ✨ feat(FlowController): Ajoute un sélecteur de prestataire à la session. ✨ feat(devis/list.twig): Ajoute une légende des actions dans la liste des devis. ✨ feat(ContratsController): Ajoute le prestataire au contrat lors de la génération. ✨ feat(SearchController): Ajoute la recherche de prestataires. 🐛 fix(SignatureClient): Corrige le stockage de l'ID de signature du devis. ✨ feat(base.twig): Ajoute un lien vers la liste des prestataires dans le menu. ✨ feat(PrestataireRepository): Ajoute une méthode de recherche par nom et email. ```
This commit is contained in:
6
.env
6
.env
@@ -83,9 +83,9 @@ STRIPE_PK=pk_test_51SUA22173W4aeFB1nO6oFfDZ12HOTffDKtCshhZ8rkUg6kUO2ZaQC0tK72rhE
|
||||
STRIPE_SK=sk_test_51SUA22173W4aeFB16EB2LxGI0hNvNJzFshDI98zRImWBIhSfzqOGAz5TlPxSpUWbj3x4COm6kmSsaal9FpQR1A7M0022DvjbbR
|
||||
STRIPE_WEBHOOKS_SECRET=
|
||||
|
||||
SIGN_URL=https://04dcf28c87cd.ngrok-free.app
|
||||
STRIPE_BASEURL=https://04dcf28c87cd.ngrok-free.app
|
||||
CONTRAT_BASEURL=https://04dcf28c87cd.ngrok-free.app
|
||||
SIGN_URL=https://81dd-82-67-166-187.ngrok-free.app
|
||||
STRIPE_BASEURL=https://81dd-82-67-166-187.ngrok-free.app
|
||||
CONTRAT_BASEURL=https://81dd-82-67-166-187.ngrok-free.app
|
||||
|
||||
MINIO_S3_URL=
|
||||
MINIO_S3_CLIENT_ID=
|
||||
|
||||
31
migrations/Version20260206120000.php
Normal file
31
migrations/Version20260206120000.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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 Version20260206120000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add password field to Prestaire entity';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE prestaire ADD password VARCHAR(255) NOT NULL');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE prestaire DROP password');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20260206130000.php
Normal file
35
migrations/Version20260206130000.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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 Version20260206130000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add relation between Prestaire and Contrats';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE contrats ADD prestataire_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE contrats ADD CONSTRAINT FK_9591436BE30DA2F7 FOREIGN KEY (prestataire_id) REFERENCES prestaire (id)');
|
||||
$this->addSql('CREATE INDEX IDX_9591436BE30DA2F7 ON contrats (prestataire_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE contrats DROP FOREIGN KEY FK_9591436BE30DA2F7');
|
||||
$this->addSql('DROP INDEX IDX_9591436BE30DA2F7 ON contrats');
|
||||
$this->addSql('ALTER TABLE contrats DROP prestataire_id');
|
||||
}
|
||||
}
|
||||
35
migrations/Version20260206140000.php
Normal file
35
migrations/Version20260206140000.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?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 Version20260206140000 extends AbstractMigration
|
||||
{
|
||||
public function getDescription(): string
|
||||
{
|
||||
return 'Add relation between Prestaire and OrderSession';
|
||||
}
|
||||
|
||||
public function up(Schema $schema): void
|
||||
{
|
||||
// this up() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE order_session ADD prestataire_id INT DEFAULT NULL');
|
||||
$this->addSql('ALTER TABLE order_session ADD CONSTRAINT FK_88755657BE30DA2F7 FOREIGN KEY (prestataire_id) REFERENCES prestaire (id)');
|
||||
$this->addSql('CREATE INDEX IDX_88755657BE30DA2F7 ON order_session (prestataire_id)');
|
||||
}
|
||||
|
||||
public function down(Schema $schema): void
|
||||
{
|
||||
// this down() migration is auto-generated, please modify it to your needs
|
||||
$this->addSql('ALTER TABLE order_session DROP FOREIGN KEY FK_88755657BE30DA2F7');
|
||||
$this->addSql('DROP INDEX IDX_88755657BE30DA2F7 ON order_session');
|
||||
$this->addSql('ALTER TABLE order_session DROP prestataire_id');
|
||||
}
|
||||
}
|
||||
@@ -338,6 +338,10 @@ class ContratsController extends AbstractController
|
||||
->setCustomer($devis->getCustomer())
|
||||
->setDevis($devis);
|
||||
|
||||
if ($devis->getOrderSession() && $devis->getOrderSession()->getPrestataire()) {
|
||||
$contrat->setPrestataire($devis->getOrderSession()->getPrestataire());
|
||||
}
|
||||
|
||||
if ($address = $devis->getAddressShip()) {
|
||||
$contrat->setAddressEvent($address->getAddress())
|
||||
->setZipCodeEvent($address->getZipcode())
|
||||
|
||||
@@ -11,6 +11,7 @@ use App\Logger\AppLogger;
|
||||
use App\Repository\DevisRepository;
|
||||
use App\Repository\OptionsRepository;
|
||||
use App\Repository\OrderSessionRepository;
|
||||
use App\Repository\PrestaireRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Service\Pdf\DevisPdfService;
|
||||
use App\Service\Signature\Client;
|
||||
@@ -34,6 +35,7 @@ class FlowController extends AbstractController
|
||||
private readonly DevisRepository $devisRepository,
|
||||
private readonly ProductRepository $productRepository,
|
||||
private readonly OptionsRepository $optionsRepository,
|
||||
private readonly PrestaireRepository $prestaireRepository,
|
||||
private readonly KernelInterface $kernel,
|
||||
private readonly Client $signatureClient,
|
||||
private readonly EventDispatcherInterface $eventDispatcher
|
||||
@@ -70,6 +72,7 @@ class FlowController extends AbstractController
|
||||
|
||||
return $this->render('dashboard/flow/view.twig', [
|
||||
'session' => $session,
|
||||
'prestataires' => $this->prestaireRepository->findAll(),
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -85,6 +88,15 @@ class FlowController extends AbstractController
|
||||
if ($request->request->has('typePaiement')) {
|
||||
$session->setTypePaiement($request->request->get('typePaiement'));
|
||||
}
|
||||
if ($request->request->has('prestataire')) {
|
||||
$prestataireId = $request->request->get('prestataire');
|
||||
if ($prestataireId) {
|
||||
$prestataire = $this->prestaireRepository->find($prestataireId);
|
||||
$session->setPrestataire($prestataire);
|
||||
} else {
|
||||
$session->setPrestataire(null);
|
||||
}
|
||||
}
|
||||
|
||||
// Recalculate if address changed or forced update (optional, but good for consistency)
|
||||
// For now, simple update.
|
||||
@@ -103,19 +115,38 @@ class FlowController extends AbstractController
|
||||
$devis = new Devis();
|
||||
$devisNumber = "DEVIS-" . sprintf('%05d', $this->devisRepository->count() + 1);
|
||||
$devis->setNum($devisNumber)
|
||||
->setState("wait-send")
|
||||
->setState("created_waitsign")
|
||||
->setCreateA(new \DateTimeImmutable())
|
||||
->setUpdateAt(new \DateTimeImmutable());
|
||||
|
||||
// 2. Customer
|
||||
$devis->setCustomer($session->getCustomer());
|
||||
|
||||
|
||||
// 2.1 Set additional Devis fields from OrderSession
|
||||
$devis->setDistance($session->getDeliveryDistance());
|
||||
$devis->setPriceShip($session->getDeliveryPrice());
|
||||
$devis->setPaymentMethod($session->getTypePaiement());
|
||||
$devis->setOrderSession($session);
|
||||
|
||||
if (str_contains($session->getTypePaiement() ?? '', 'Chorus')) {
|
||||
$devis->setIsNotAddCaution(true);
|
||||
}
|
||||
|
||||
// Set Prestataire from session to devis (needs to be added to Devis entity too if not already, but Contrats has it)
|
||||
// Since Devis transforms to Contrats, we'll need to ensure Contrats gets this info.
|
||||
// For now, let's assume Devis doesn't strictly need it unless we add the field to Devis too.
|
||||
// Wait, the user asked to add 'selecteur pour choisir le prestaire en charge de la livraison' in the view.
|
||||
// And I added the relation to OrderSession.
|
||||
// When Devis is signed and becomes Contrat, Contrat has the relation.
|
||||
// So we should probably store it on Devis as well or just pass it through.
|
||||
// Let's check Devis entity if I can add it there too, or if I just rely on OrderSession link.
|
||||
// Actually, creating Devis here.
|
||||
// We should add 'prestataire' to Devis entity as well to persist this choice through the flow.
|
||||
// Or, when creating Contrat from Devis later, we check the OrderSession linked to Devis.
|
||||
// Let's update Devis entity to be safe and consistent.
|
||||
// For this step I'll just update the controller logic to SET it if Devis has it.
|
||||
// I will check Devis entity in a moment.
|
||||
|
||||
// 3. Addresses
|
||||
// Billing Address
|
||||
$billAddr = $this->findOrCreateAddress(
|
||||
@@ -168,13 +199,13 @@ class FlowController extends AbstractController
|
||||
$product = $this->productRepository->find($prodId);
|
||||
if ($product) {
|
||||
$line = new DevisLine();
|
||||
$line->setDevi($devis);
|
||||
$line->setPos($pos++);
|
||||
$line->setProduct($product->getName());
|
||||
$line->setDay($days);
|
||||
$line->setPriceHt($product->getPriceDay());
|
||||
$line->setPriceHtSup($product->getPriceSup());
|
||||
$em->persist($line);
|
||||
$devis->addDevisLine($line);
|
||||
|
||||
// Linked Options
|
||||
if (isset($productsData['options'][$prodId])) {
|
||||
@@ -182,10 +213,10 @@ class FlowController extends AbstractController
|
||||
$option = $this->optionsRepository->find($optId);
|
||||
if ($option) {
|
||||
$devisOpt = new DevisOptions();
|
||||
$devisOpt->setDevis($devis);
|
||||
$devisOpt->setOption($option->getName());
|
||||
$devisOpt->setPriceHt($option->getPriceHt());
|
||||
$em->persist($devisOpt);
|
||||
$devis->addDevisOption($devisOpt);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -193,10 +224,11 @@ class FlowController extends AbstractController
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 6. Orphan Options
|
||||
$sessionOptions = $session->getOptions();
|
||||
$productIds = $productsData['ids'] ?? [];
|
||||
|
||||
|
||||
if ($sessionOptions && is_array($sessionOptions)) {
|
||||
foreach ($sessionOptions as $prodId => $opts) {
|
||||
if (!in_array($prodId, $productIds) && is_array($opts)) {
|
||||
@@ -217,15 +249,16 @@ class FlowController extends AbstractController
|
||||
// 7. Delivery Fee
|
||||
if ($session->getDeliveryPrice() > 0) {
|
||||
$devisOpt = new DevisOptions();
|
||||
$devisOpt->setDevis($devis);
|
||||
$devisOpt->setOption("Frais de livraison");
|
||||
$dist = number_format($session->getDeliveryDistance(), 1, ',', ' ');
|
||||
$town = $session->getBillingTown() ?: 'Ville inconnue';
|
||||
$devisOpt->setDetails("Livraison ($dist km) - $town");
|
||||
$devisOpt->setPriceHt($session->getDeliveryPrice());
|
||||
$em->persist($devisOpt);
|
||||
$devis->addDevisOption($devisOpt);
|
||||
}
|
||||
|
||||
|
||||
// 8. Persist & Flush to generate IDs
|
||||
$em->persist($devis);
|
||||
$em->flush();
|
||||
@@ -239,7 +272,7 @@ class FlowController extends AbstractController
|
||||
// Internal
|
||||
$devisService = new DevisPdfService($this->kernel, $devis, $this->productRepository, false);
|
||||
$this->savePdfFile($devis, $devisService->generate(), 'devis_', 'setDevisFile');
|
||||
|
||||
|
||||
$em->flush();
|
||||
|
||||
// 10. DocuSeal Submission
|
||||
@@ -249,6 +282,7 @@ class FlowController extends AbstractController
|
||||
$this->eventDispatcher->dispatch(new DevisSend($devis));
|
||||
|
||||
} catch (\Exception $e) {
|
||||
dd($e->getMessage());
|
||||
$this->appLogger->record('ERROR', 'Erreur génération PDF Devis auto: ' . $e->getMessage());
|
||||
}
|
||||
|
||||
@@ -315,7 +349,7 @@ class FlowController extends AbstractController
|
||||
$itineraire = $itineraireResponse->toArray();
|
||||
$distance = $itineraire['distance'];
|
||||
$geometry = $itineraire['geometry'] ?? null;
|
||||
|
||||
|
||||
$rate = 0.50;
|
||||
$trips = 4;
|
||||
$price = 0.0;
|
||||
|
||||
134
src/Controller/Dashboard/PrestaireController.php
Normal file
134
src/Controller/Dashboard/PrestaireController.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Controller\Dashboard;
|
||||
|
||||
use App\Entity\Prestaire;
|
||||
use App\Form\PrestaireType;
|
||||
use App\Logger\AppLogger;
|
||||
use App\Repository\PrestaireRepository;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Knp\Component\Pager\PaginatorInterface;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
|
||||
use App\Service\Mailer\Mailer;
|
||||
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
|
||||
|
||||
#[Route('/crm/prestataire')]
|
||||
class PrestaireController extends AbstractController
|
||||
{
|
||||
public function __construct(
|
||||
private readonly EntityManagerInterface $em,
|
||||
private readonly AppLogger $appLogger,
|
||||
private readonly Mailer $mailer,
|
||||
private readonly UrlGeneratorInterface $urlGenerator
|
||||
) {
|
||||
}
|
||||
|
||||
#[Route('', name: 'app_crm_prestataire', options: ['sitemap' => false], methods: ['GET'])]
|
||||
public function index(PrestaireRepository $repository, PaginatorInterface $paginator, Request $request): Response
|
||||
{
|
||||
$this->appLogger->record('VIEW', 'Consultation de la liste des Prestataires');
|
||||
|
||||
$query = $repository->createQueryBuilder('p')
|
||||
->orderBy('p.name', 'ASC')
|
||||
->getQuery();
|
||||
|
||||
return $this->render('dashboard/prestaire.twig', [
|
||||
'prestataires' => $paginator->paginate($query, $request->query->getInt('page', 1), 10),
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/add', name: 'app_crm_prestataire_add', options: ['sitemap' => false], methods: ['GET', 'POST'])]
|
||||
public function add(Request $request, UserPasswordHasherInterface $passwordHasher): Response
|
||||
{
|
||||
$prestataire = new Prestaire();
|
||||
$form = $this->createForm(PrestaireType::class, $prestataire);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
// Génération mot de passe aléatoire temporaire
|
||||
$plainPassword = bin2hex(random_bytes(10));
|
||||
$prestataire->setPassword($passwordHasher->hashPassword($prestataire, $plainPassword));
|
||||
|
||||
// Rôles par défaut
|
||||
$prestataire->setRoles(['ROLE_PRESTAIRE']);
|
||||
|
||||
$this->em->persist($prestataire);
|
||||
$this->em->flush();
|
||||
|
||||
// Envoi de l'email de bienvenue
|
||||
$this->mailer->send(
|
||||
$prestataire->getEmail(),
|
||||
"{$prestataire->getSurname()} {$prestataire->getName()}",
|
||||
"Bienvenue sur l'Intranet Ludikevent",
|
||||
"mails/prestataire/create.twig",
|
||||
[
|
||||
'prestataire' => $prestataire,
|
||||
'password' => $plainPassword,
|
||||
'login_url' => $this->urlGenerator->generate('etl_home', [], UrlGeneratorInterface::ABSOLUTE_URL)
|
||||
]
|
||||
);
|
||||
|
||||
$this->appLogger->record('CREATE', sprintf(
|
||||
"Création du Prestataire : %s %s (%s)",
|
||||
$prestataire->getSurname(), $prestataire->getName(), $prestataire->getEmail()
|
||||
));
|
||||
|
||||
$this->addFlash('success', "Le prestataire {$prestataire->getName()} a été créé avec succès et ses identifiants ont été envoyés par email.");
|
||||
|
||||
return $this->redirectToRoute('app_crm_prestataire');
|
||||
}
|
||||
|
||||
return $this->render('dashboard/prestaire/add.twig', [
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/{id}', name: 'app_crm_prestataire_view', options: ['sitemap' => false], methods: ['GET', 'POST'])]
|
||||
public function view(Prestaire $prestataire, Request $request): Response
|
||||
{
|
||||
$form = $this->createForm(PrestaireType::class, $prestataire);
|
||||
$form->handleRequest($request);
|
||||
|
||||
if ($form->isSubmitted() && $form->isValid()) {
|
||||
$this->em->flush();
|
||||
|
||||
$this->appLogger->record('UPDATE', sprintf(
|
||||
"Mise à jour du Prestataire : %s %s",
|
||||
$prestataire->getSurname(), $prestataire->getName()
|
||||
));
|
||||
|
||||
$this->addFlash('success', "Informations mises à jour.");
|
||||
return $this->redirectToRoute('app_crm_prestataire_view', ['id' => $prestataire->getId()]);
|
||||
}
|
||||
|
||||
$this->appLogger->record('VIEW', "Consultation fiche Prestataire : {$prestataire->getName()}");
|
||||
|
||||
return $this->render('dashboard/prestaire/view.twig', [
|
||||
'prestataire' => $prestataire,
|
||||
'form' => $form->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
#[Route('/delete/{id}', name: 'app_crm_prestataire_delete', options: ['sitemap' => false], methods: ['POST'])]
|
||||
public function delete(Prestaire $prestataire, Request $request): Response
|
||||
{
|
||||
if ($this->isCsrfTokenValid('delete' . $prestataire->getId(), $request->request->get('_token'))) {
|
||||
$name = $prestataire->getSurname() . ' ' . $prestataire->getName();
|
||||
|
||||
$this->em->remove($prestataire);
|
||||
$this->em->flush();
|
||||
|
||||
$this->appLogger->record('DELETE', "Suppression Prestataire : $name");
|
||||
$this->addFlash('success', "Le prestataire $name a été supprimé.");
|
||||
} else {
|
||||
$this->addFlash('error', "Token de sécurité invalide.");
|
||||
}
|
||||
|
||||
return $this->redirectToRoute('app_crm_prestataire');
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ use App\Repository\AccountRepository;
|
||||
use App\Repository\ContratsRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\OptionsRepository;
|
||||
use App\Repository\PrestaireRepository;
|
||||
use App\Repository\ProductRepository;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordConfirmEvent;
|
||||
use App\Service\ResetPassword\Event\ResetPasswordEvent;
|
||||
@@ -35,6 +36,7 @@ class SearchController extends AbstractController
|
||||
CustomerRepository $customerRepository,
|
||||
ContratsRepository $contratsRepository,
|
||||
OptionsRepository $optionsRepository,
|
||||
PrestaireRepository $prestaireRepository,
|
||||
Client $client,
|
||||
Request $request
|
||||
): Response {
|
||||
@@ -42,6 +44,19 @@ class SearchController extends AbstractController
|
||||
$unifiedResults = [];
|
||||
|
||||
if (!empty($query)) {
|
||||
// Recherche DB directe pour Prestataires
|
||||
$prestataires = $prestaireRepository->search($query);
|
||||
foreach ($prestataires as $prestataire) {
|
||||
$unifiedResults[] = [
|
||||
'title' => $prestataire->getSurname() . " " . $prestataire->getName(),
|
||||
'subtitle' => $prestataire->getEmail(),
|
||||
'link' => $this->generateUrl('app_crm_prestataire_view', ['id' => $prestataire->getId()]),
|
||||
'type' => 'Prestataire',
|
||||
'id' => $prestataire->getId(),
|
||||
'initials' => strtoupper(substr($prestataire->getSurname(), 0, 1) . substr($prestataire->getName(), 0, 1))
|
||||
];
|
||||
}
|
||||
|
||||
$response = $client->searchGlobal($query, 20);
|
||||
|
||||
foreach ($response['results'] as $resultGroup) {
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Entity\Contrats;
|
||||
use App\Entity\ContratsLine;
|
||||
use App\Entity\ContratsOption;
|
||||
use App\Service\Pdf\ContratPdfService;
|
||||
use App\Entity\Account;
|
||||
use App\Entity\AccountResetPasswordRequest;
|
||||
use App\Entity\Devis;
|
||||
@@ -24,6 +28,7 @@ use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||
use Symfony\Component\HttpFoundation\JsonResponse;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\KernelInterface;
|
||||
use Symfony\Component\Mime\Part\DataPart;
|
||||
use Symfony\Component\PasswordHasher\Hasher\UserPasswordHasherInterface;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
@@ -43,9 +48,10 @@ class SignatureController extends AbstractController
|
||||
EntityManagerInterface $entityManager,
|
||||
Request $request,
|
||||
Mailer $mailer,
|
||||
KernelInterface $kernel
|
||||
): Response {
|
||||
if ($request->get('type') === "contrat") {
|
||||
$contrats = $contratsRepository->find($request->get('id'));
|
||||
if ($request->query->get('type') === "contrat") {
|
||||
$contrats = $contratsRepository->find($request->query->get('id'));
|
||||
if (!$contrats) {
|
||||
throw $this->createNotFoundException("Contrat introuvable.");
|
||||
}
|
||||
@@ -126,8 +132,8 @@ class SignatureController extends AbstractController
|
||||
}
|
||||
return $this->render('sign/contrat_sign_success.twig', ['contrat' => $contrats]);
|
||||
}
|
||||
if ($request->get('type') === "devis") {
|
||||
$devis = $devisRepository->find($request->get('id'));
|
||||
if ($request->query->get('type') === "devis") {
|
||||
$devis = $devisRepository->find($request->query->get('id'));
|
||||
|
||||
if (!$devis) {
|
||||
throw $this->createNotFoundException("Devis introuvable.");
|
||||
@@ -196,6 +202,71 @@ class SignatureController extends AbstractController
|
||||
],
|
||||
$attachments
|
||||
);
|
||||
|
||||
// --- AUTOMATION: Create Contrat from Devis if OrderSession exists ---
|
||||
if ($devis->getOrderSession()) {
|
||||
$contrat = new Contrats();
|
||||
$contrat->setNumReservation('RESERV-' . date('Ymd') . '-' . substr(str_shuffle('ABCDEFGHIJKLMNOPQRSTUVWXYZ'), 0, 10));
|
||||
$contrat->setCreateAt(new \DateTimeImmutable());
|
||||
|
||||
// Hydrate from Devis
|
||||
$contrat->setDateAt($devis->getStartAt())
|
||||
->setEndAt($devis->getEndAt())
|
||||
->setCustomer($devis->getCustomer())
|
||||
->setDevis($devis);
|
||||
|
||||
if ($devis->getOrderSession()->getPrestataire()) {
|
||||
$contrat->setPrestataire($devis->getOrderSession()->getPrestataire());
|
||||
}
|
||||
|
||||
if ($address = $devis->getAddressShip()) {
|
||||
$contrat->setAddressEvent($address->getAddress())
|
||||
->setZipCodeEvent($address->getZipcode())
|
||||
->setTownEvent($address->getCity());
|
||||
}
|
||||
|
||||
// Copy Lines
|
||||
foreach ($devis->getDevisLines() as $dLine) {
|
||||
$cLine = (new ContratsLine())
|
||||
->setName($dLine->getProduct())
|
||||
->setPrice1DayHt($dLine->getPriceHt())
|
||||
->setPriceSupDayHt($dLine->getPriceHtSup())
|
||||
->setCaution(0);
|
||||
|
||||
$product = $productRepository->findOneBy(['name' => $dLine->getProduct()]);
|
||||
if ($product) {
|
||||
$cLine->setCaution($product->getCaution());
|
||||
}
|
||||
$entityManager->persist($cLine);
|
||||
$contrat->addContratsLine($cLine);
|
||||
}
|
||||
|
||||
// Copy Options
|
||||
foreach ($devis->getDevisOptions() as $dOpt) {
|
||||
$cOpt = (new ContratsOption())
|
||||
->setName($dOpt->getOption())
|
||||
->setDetails($dOpt->getDetails())
|
||||
->setPrice($dOpt->getPriceHt());
|
||||
$entityManager->persist($cOpt);
|
||||
$contrat->addContratsOption($cOpt);
|
||||
}
|
||||
|
||||
// Generate PDF
|
||||
foreach ([true, false] as $isDocuseal) {
|
||||
$service = new ContratPdfService($kernel, $contrat, $isDocuseal);
|
||||
$tmp = sys_get_temp_dir() . '/' . uniqid() . '.pdf';
|
||||
file_put_contents($tmp, $service->generate());
|
||||
|
||||
$file = new UploadedFile($tmp, 'doc.pdf', 'application/pdf', null, true);
|
||||
$isDocuseal ? $contrat->setDevisDocuSealFile($file) : $contrat->setDevisFile($file);
|
||||
}
|
||||
|
||||
$entityManager->persist($contrat);
|
||||
$entityManager->flush();
|
||||
|
||||
// Create Signature Submission
|
||||
$client->createSubmissionContrat($contrat);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
return new Response("Erreur lors de la récupération ou de l'envoi des documents : " . $e->getMessage(), 500);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,9 @@ class Contrats
|
||||
#[ORM\OneToOne(mappedBy: 'contrat', cascade: ['persist', 'remove'])]
|
||||
private ?EtatLieux $etatLieux = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'contrats')]
|
||||
private ?Prestaire $prestataire = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->contratsPayments = new ArrayCollection();
|
||||
@@ -855,6 +858,18 @@ class Contrats
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrestataire(): ?Prestaire
|
||||
{
|
||||
return $this->prestataire;
|
||||
}
|
||||
|
||||
public function setPrestataire(?Prestaire $prestataire): static
|
||||
{
|
||||
$this->prestataire = $prestataire;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function isCaution()
|
||||
{
|
||||
return $this->contratsPayments->filter(function (ContratsPayments $contratsPayments){
|
||||
|
||||
@@ -93,6 +93,9 @@ class OrderSession
|
||||
#[ORM\OneToOne(mappedBy: 'orderSession', cascade: ['persist', 'remove'])]
|
||||
private ?Devis $devis = null;
|
||||
|
||||
#[ORM\ManyToOne(inversedBy: 'orderSessions')]
|
||||
private ?Prestaire $prestataire = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->createdAt = new \DateTimeImmutable();
|
||||
@@ -123,6 +126,18 @@ class OrderSession
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPrestataire(): ?Prestaire
|
||||
{
|
||||
return $this->prestataire;
|
||||
}
|
||||
|
||||
public function setPrestataire(?Prestaire $prestataire): static
|
||||
{
|
||||
$this->prestataire = $prestataire;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
#[ORM\PrePersist]
|
||||
public function setCreatedAtValue(): void
|
||||
{
|
||||
|
||||
@@ -11,6 +11,7 @@ use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
|
||||
use Symfony\Component\Security\Core\User\PasswordAuthenticatedUserInterface;
|
||||
use Symfony\Component\Security\Core\User\UserInterface;
|
||||
#[ORM\Entity(repositoryClass: PrestaireRepository::class)]
|
||||
#[UniqueEntity(fields: ['email'], message: 'Il existe déjà un compte avec cet email')]
|
||||
class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
{
|
||||
#[ORM\Id]
|
||||
@@ -24,6 +25,18 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\OneToMany(targetEntity: EtatLieux::class, mappedBy: 'prestataire')]
|
||||
private Collection $etatLieuxes;
|
||||
|
||||
/**
|
||||
* @var Collection<int, Contrats>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: Contrats::class, mappedBy: 'prestataire')]
|
||||
private Collection $contrats;
|
||||
|
||||
/**
|
||||
* @var Collection<int, OrderSession>
|
||||
*/
|
||||
#[ORM\OneToMany(targetEntity: OrderSession::class, mappedBy: 'prestataire')]
|
||||
private Collection $orderSessions;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $email = null;
|
||||
|
||||
@@ -36,6 +49,9 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $phone = null;
|
||||
|
||||
#[ORM\Column(length: 255)]
|
||||
private ?string $password = null;
|
||||
|
||||
/**
|
||||
* @var list<string> The user roles
|
||||
*/
|
||||
@@ -45,6 +61,8 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
public function __construct()
|
||||
{
|
||||
$this->etatLieuxes = new ArrayCollection();
|
||||
$this->contrats = new ArrayCollection();
|
||||
$this->orderSessions = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
@@ -52,6 +70,66 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, OrderSession>
|
||||
*/
|
||||
public function getOrderSessions(): Collection
|
||||
{
|
||||
return $this->orderSessions;
|
||||
}
|
||||
|
||||
public function addOrderSession(OrderSession $orderSession): static
|
||||
{
|
||||
if (!$this->orderSessions->contains($orderSession)) {
|
||||
$this->orderSessions->add($orderSession);
|
||||
$orderSession->setPrestataire($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeOrderSession(OrderSession $orderSession): static
|
||||
{
|
||||
if ($this->orderSessions->removeElement($orderSession)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($orderSession->getPrestataire() === $this) {
|
||||
$orderSession->setPrestataire(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, Contrats>
|
||||
*/
|
||||
public function getContrats(): Collection
|
||||
{
|
||||
return $this->contrats;
|
||||
}
|
||||
|
||||
public function addContrat(Contrats $contrat): static
|
||||
{
|
||||
if (!$this->contrats->contains($contrat)) {
|
||||
$this->contrats->add($contrat);
|
||||
$contrat->setPrestataire($this);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function removeContrat(Contrats $contrat): static
|
||||
{
|
||||
if ($this->contrats->removeElement($contrat)) {
|
||||
// set the owning side to null (unless already changed)
|
||||
if ($contrat->getPrestataire() === $this) {
|
||||
$contrat->setPrestataire(null);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Collection<int, EtatLieux>
|
||||
*/
|
||||
@@ -130,9 +208,19 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @see PasswordAuthenticatedUserInterface
|
||||
*/
|
||||
public function getPassword(): ?string
|
||||
{
|
||||
// TODO: Implement getPassword() method.
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): static
|
||||
{
|
||||
$this->password = $password;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
@@ -156,7 +244,8 @@ class Prestaire implements UserInterface, PasswordAuthenticatedUserInterface
|
||||
|
||||
public function eraseCredentials(): void
|
||||
{
|
||||
// TODO: Implement eraseCredentials() method.
|
||||
// If you store any temporary, sensitive data on the user, clear it here
|
||||
// $this->plainPassword = null;
|
||||
}
|
||||
|
||||
public function getUserIdentifier(): string
|
||||
|
||||
39
src/Form/PrestaireType.php
Normal file
39
src/Form/PrestaireType.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
namespace App\Form;
|
||||
|
||||
use App\Entity\Prestaire;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\EmailType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\TextType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PrestaireType extends AbstractType
|
||||
{
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder
|
||||
->add('name', TextType::class, [
|
||||
'label' => 'Nom',
|
||||
'required' => true,
|
||||
])
|
||||
->add('surname', TextType::class, [
|
||||
'label' => 'Prénom',
|
||||
'required' => true,
|
||||
])
|
||||
->add('email', EmailType::class, [
|
||||
'label' => 'Email',
|
||||
'required' => true,
|
||||
])
|
||||
->add('phone', TextType::class, [
|
||||
'label' => 'Téléphone',
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefault('data_class', Prestaire::class);
|
||||
}
|
||||
}
|
||||
@@ -31,13 +31,27 @@ class PrestaireRepository extends ServiceEntityRepository
|
||||
// ;
|
||||
// }
|
||||
|
||||
// public function findOneBySomeField($value): ?Prestaire
|
||||
// {
|
||||
// return $this->createQueryBuilder('p')
|
||||
// ->andWhere('p.exampleField = :val')
|
||||
// ->setParameter('val', $value)
|
||||
// ->getQuery()
|
||||
// ->getOneOrNullResult()
|
||||
// ;
|
||||
// }
|
||||
public function findOneByNameAndEmail(string $name, string $email): ?Prestaire
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.name = :name')
|
||||
->andWhere('p.email = :email')
|
||||
->setParameter('name', $name)
|
||||
->setParameter('email', $email)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return Prestaire[]
|
||||
*/
|
||||
public function search(string $query): array
|
||||
{
|
||||
return $this->createQueryBuilder('p')
|
||||
->andWhere('p.name LIKE :query OR p.surname LIKE :query OR p.email LIKE :query')
|
||||
->setParameter('query', '%' . $query . '%')
|
||||
->orderBy('p.name', 'ASC')
|
||||
->getQuery()
|
||||
->getResult();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,6 +93,7 @@ class Client
|
||||
|
||||
// Stockage de l'ID submitter de Docuseal dans ton entité
|
||||
$devis->setSignatureId($submission['submitters'][1]['id']);
|
||||
$this->entityManager->persist($devis);
|
||||
|
||||
$this->entityManager->flush();
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path>', 'app_crm_formules') }}
|
||||
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>', 'app_crm_facture') }}
|
||||
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"></path>', 'app_clients') }}
|
||||
{{ menu.nav_link(path('app_crm_prestataire'), 'Prestataires', '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 13.255A23.931 23.931 0 0112 15c-3.183 0-6.22-.62-9-1.745M16 6V4a2 2 0 00-2-2h-4a2 2 0 00-2 2v2m4 6h.01M5 20h14a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"></path>', 'app_crm_prestataire') }}
|
||||
|
||||
{% set pendingCount = getPendingOrderSessionCount() %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_flow') }}" class="flex items-center justify-between px-4 py-3 rounded-xl transition-all duration-200 group {{ app.current_route == 'app_crm_flow' ? 'bg-blue-600 text-white shadow-lg shadow-blue-500/30' : 'hover:bg-slate-800 text-slate-400' }}">
|
||||
|
||||
@@ -102,7 +102,7 @@
|
||||
<div class="flex items-center justify-end space-x-2">
|
||||
|
||||
{# Renvoyer lien de signature #}
|
||||
{% if quote.state == "created_waitsign" and quote.state == "wait-send" %}
|
||||
{% if quote.state == "created_waitsign" or quote.state == "wait-send" %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_devis', {resend: quote.id}) }}"
|
||||
title="Renvoyer le lien de signature"
|
||||
class="p-2 bg-indigo-600/10 hover:bg-indigo-600 text-indigo-500 hover:text-white rounded-xl transition-all border border-indigo-500/20 shadow-lg shadow-indigo-600/5">
|
||||
@@ -185,6 +185,37 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# LÉGENDE #}
|
||||
<div class="mt-6 p-4 rounded-2xl bg-[#1e293b]/40 border border-white/5 backdrop-blur-sm">
|
||||
<p class="text-[10px] font-black text-slate-400 uppercase tracking-[0.2em] mb-3">Légende des actions</p>
|
||||
<div class="flex flex-wrap gap-4 text-xs text-slate-300">
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-1.5 bg-indigo-600/10 text-indigo-500 rounded-lg border border-indigo-500/20"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg></div>
|
||||
<span>Renvoyer le lien</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-1.5 bg-blue-600/10 text-blue-500 rounded-lg border border-blue-500/20"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg></div>
|
||||
<span>Modifier</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-1.5 bg-emerald-600/10 text-emerald-500 rounded-lg border border-emerald-500/20"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" /></svg></div>
|
||||
<span>Télécharger Devis Signé</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-1.5 bg-purple-600/10 text-purple-500 rounded-lg border border-purple-500/20"><svg class="w-3 h-3" 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>
|
||||
<span>Télécharger Certificat Audit</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-1.5 bg-slate-600/10 text-slate-300 rounded-lg border border-slate-500/20"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" /></svg></div>
|
||||
<span>Télécharger Devis PDF</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<div class="p-1.5 bg-rose-500/10 text-rose-500 rounded-lg border border-rose-500/20"><svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" /></svg></div>
|
||||
<span>Supprimer</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# PAGINATION #}
|
||||
{% if quotes.getTotalItemCount is defined and quotes.getTotalItemCount > quotes.getItemNumberPerPage %}
|
||||
<div class="mt-8 flex justify-center custom-pagination">
|
||||
|
||||
@@ -73,7 +73,18 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<div class="mt-4">
|
||||
<label class="block text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-1">Prestataire (Livraison)</label>
|
||||
<select name="prestataire" class="w-full bg-slate-900/50 border border-slate-700 rounded-lg px-3 py-2 text-white text-sm focus:border-blue-500 focus:ring-1 focus:ring-blue-500 outline-none transition-all">
|
||||
<option value="">-- Aucun prestataire assigné --</option>
|
||||
{% for p in prestataires %}
|
||||
<option value="{{ p.id }}" {% if session.prestataire and session.prestataire.id == p.id %}selected{% endif %}>
|
||||
{{ p.surname }} {{ p.name }} ({{ p.email }})
|
||||
</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<button type="submit" class="py-2 px-6 bg-indigo-600 hover:bg-indigo-500 text-white text-xs font-bold uppercase tracking-widest rounded-lg transition-all">
|
||||
Enregistrer les modifications
|
||||
</button>
|
||||
|
||||
104
templates/dashboard/prestaire.twig
Normal file
104
templates/dashboard/prestaire.twig
Normal file
@@ -0,0 +1,104 @@
|
||||
{% extends 'dashboard/base.twig' %}
|
||||
|
||||
{% block title %}Prestataires{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_prestataire_add') }}"
|
||||
class="inline-flex items-center px-4 py-2 text-sm font-black uppercase tracking-widest text-white bg-indigo-600 rounded-xl hover:bg-indigo-700 shadow-lg shadow-indigo-500/20 transition-all active:scale-95">
|
||||
<svg class="w-5 h-5 mr-2" 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"></path>
|
||||
</svg>
|
||||
Ajouter un Prestataire
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="p-4 md:p-6 bg-gray-50 dark:bg-gray-900 min-h-screen w-full">
|
||||
<div class="w-full">
|
||||
{# HEADER #}
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-gray-800 dark:text-white tracking-tight">Liste des Prestataires</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-200 font-medium mt-1">Gestion des prestataires externes.</p>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<span class="px-4 py-2 text-xs font-black text-indigo-600 bg-indigo-100 rounded-full dark:bg-indigo-900/40 dark:text-indigo-300 uppercase tracking-widest shadow-sm border border-indigo-200 dark:border-indigo-800">
|
||||
{{ prestataires|length }} Prestataires
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# TABLE CARD #}
|
||||
<div class="bg-white dark:bg-[#1e293b] shadow-sm rounded-3xl border border-gray-200 dark:border-gray-800 overflow-hidden">
|
||||
<div class="overflow-x-auto">
|
||||
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-200">
|
||||
<thead class="text-[10px] text-gray-200 uppercase bg-gray-50/50 dark:bg-gray-900/50 dark:text-gray-500 border-b border-gray-100 dark:border-gray-800">
|
||||
<tr>
|
||||
<th scope="col" class="px-8 py-5 font-black tracking-widest">Identité</th>
|
||||
<th scope="col" class="px-8 py-5 font-black tracking-widest">Téléphone</th>
|
||||
<th scope="col" class="px-8 py-5 font-black tracking-widest text-right">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y divide-gray-100 dark:divide-gray-800">
|
||||
{% for prestataire in prestataires %}
|
||||
<tr class="bg-white dark:bg-[#1e293b] hover:bg-gray-50 dark:hover:bg-gray-800/50 transition-colors group">
|
||||
{# COLONNE 1 : IDENTITÉ #}
|
||||
<td class="px-8 py-5">
|
||||
<div class="flex items-center space-x-4">
|
||||
<div class="h-11 w-11 rounded-2xl bg-slate-100 dark:bg-slate-800 flex items-center justify-center text-slate-300 dark:text-slate-400 font-black text-xs border border-slate-200 dark:border-slate-700 shadow-sm transition-transform group-hover:scale-110">
|
||||
{{ prestataire.surname|first|upper }}{{ prestataire.name|first|upper }}
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
<span class="font-bold text-slate-900 dark:text-white text-base">{{ prestataire.surname }} {{ prestataire.name }}</span>
|
||||
<span class="text-xs text-slate-400 font-medium tracking-tight font-mono">{{ prestataire.email }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
|
||||
{# COLONNE 2 : TELEPHONE #}
|
||||
<td class="px-8 py-5">
|
||||
<span class="text-slate-500 dark:text-slate-300">{{ prestataire.phone|default('N/A') }}</span>
|
||||
</td>
|
||||
|
||||
{# COLONNE 3 : ACTIONS #}
|
||||
<td class="px-8 py-5 text-right whitespace-nowrap">
|
||||
<div class="flex items-center justify-end space-x-3">
|
||||
{# Bouton Gérer #}
|
||||
<a data-turbo="false" href="{{ path('app_crm_prestataire_view', {id: prestataire.id}) }}"
|
||||
class="flex items-center space-x-2 px-3 py-2 bg-slate-50 dark:bg-slate-800 text-slate-600 dark:text-slate-300 hover:bg-indigo-600 hover:text-white dark:hover:bg-indigo-600 dark:hover:text-white rounded-xl transition-all border border-slate-200 dark:border-slate-700 font-bold text-xs shadow-sm"
|
||||
title="Gérer le prestataire">
|
||||
<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="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z"></path></svg>
|
||||
<span>Gérer</span>
|
||||
</a>
|
||||
|
||||
{# Bouton Supprimer #}
|
||||
<a data-turbo="false" href="{{ path('app_crm_prestataire_delete', {id: prestataire.id}) }}?_token={{ csrf_token('delete' ~ prestataire.id) }}"
|
||||
data-turbo-method="post"
|
||||
data-turbo-confirm="Confirmer la suppression définitive du prestataire {{ prestataire.surname }} {{ prestataire.name }} ?"
|
||||
class="p-2.5 text-slate-400 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-500/10 rounded-xl transition-all border border-transparent hover:border-red-100 dark:hover:border-red-500/20"
|
||||
title="Supprimer">
|
||||
<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="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16"></path></svg>
|
||||
</a>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="3" class="px-8 py-24 text-center">
|
||||
<div class="flex flex-col items-center">
|
||||
<div class="w-20 h-20 bg-slate-50 dark:bg-slate-900/50 rounded-3xl flex items-center justify-center text-slate-200 dark:text-slate-800 mb-6">
|
||||
<svg class="w-10 h-10" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z"></path></svg>
|
||||
</div>
|
||||
<p class="text-slate-400 font-bold text-lg">Aucun Prestataire enregistré</p>
|
||||
<p class="text-slate-400/60 text-sm mt-1">Commencez par en ajouter un via le bouton en haut à droite.</p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
90
templates/dashboard/prestaire/add.twig
Normal file
90
templates/dashboard/prestaire/add.twig
Normal file
@@ -0,0 +1,90 @@
|
||||
{% extends 'dashboard/base.twig' %}
|
||||
|
||||
{% block title %}Nouveau Prestataire{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_prestataire') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-200 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
Retour à la liste
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="p-4 md:p-6 bg-gray-50 dark:bg-gray-900 min-h-screen w-full">
|
||||
<div class="w-full">
|
||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-8">
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-8'}}) }}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-8">
|
||||
{# PRENOM #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.surname, 'Prénom', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.surname, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': 'Prénom'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.surname) }}</div>
|
||||
</div>
|
||||
|
||||
{# NOM #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.name, 'Nom', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.name, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': 'Nom'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.name) }}</div>
|
||||
</div>
|
||||
|
||||
{# EMAIL #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.email, 'Email', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.email, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': 'email@example.com'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.email) }}</div>
|
||||
</div>
|
||||
|
||||
{# TELEPHONE #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.phone, 'Téléphone', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.phone, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': '06...'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.phone) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end pt-8 border-t border-gray-200 dark:border-gray-700 mt-10">
|
||||
<button type="submit" class="flex items-center justify-center text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-bold rounded-lg text-sm px-8 py-3.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 transition-all shadow-lg hover:shadow-blue-500/30">
|
||||
Créer le prestataire
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
99
templates/dashboard/prestaire/view.twig
Normal file
99
templates/dashboard/prestaire/view.twig
Normal file
@@ -0,0 +1,99 @@
|
||||
{% extends 'dashboard/base.twig' %}
|
||||
|
||||
{% block title %}Gestion Prestataire{% endblock %}
|
||||
|
||||
{% block actions %}
|
||||
<a data-turbo="false" href="{{ path('app_crm_prestataire') }}" class="inline-flex items-center px-4 py-2 text-sm font-medium text-gray-200 bg-white border border-gray-300 rounded-lg hover:bg-gray-50 dark:bg-gray-800 dark:text-gray-300 dark:border-gray-600 dark:hover:bg-gray-700 transition-colors">
|
||||
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"></path>
|
||||
</svg>
|
||||
Retour à la liste
|
||||
</a>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="p-4 md:p-6 bg-gray-50 dark:bg-gray-900 min-h-screen w-full">
|
||||
<div class="w-full">
|
||||
<div class="flex justify-between items-center mb-8">
|
||||
<div>
|
||||
<h1 class="text-3xl font-black text-gray-800 dark:text-white tracking-tight">
|
||||
{{ prestataire.surname }} {{ prestataire.name }}
|
||||
</h1>
|
||||
<p class="text-sm text-gray-500 dark:text-gray-200 font-medium mt-1">Modification des informations.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-white dark:bg-gray-800 shadow-md rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
|
||||
<div class="p-8">
|
||||
{{ form_start(form, {'attr': {'class': 'space-y-8'}}) }}
|
||||
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-12 gap-y-8">
|
||||
{# PRENOM #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.surname, 'Prénom', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.surname, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': 'Prénom'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.surname) }}</div>
|
||||
</div>
|
||||
|
||||
{# NOM #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.name, 'Nom', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.name, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': 'Nom'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.name) }}</div>
|
||||
</div>
|
||||
|
||||
{# EMAIL #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.email, 'Email', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.email, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': 'email@example.com'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.email) }}</div>
|
||||
</div>
|
||||
|
||||
{# TELEPHONE #}
|
||||
<div class="space-y-2">
|
||||
{{ form_label(form.phone, 'Téléphone', {
|
||||
'label_attr': {'class': 'block text-sm font-semibold text-gray-200 dark:text-gray-200'}
|
||||
}) }}
|
||||
{{ form_widget(form.phone, {
|
||||
'attr': {
|
||||
'class': 'bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-3 dark:bg-gray-700 dark:border-gray-600 dark:text-white',
|
||||
'placeholder': '06...'
|
||||
}
|
||||
}) }}
|
||||
<div class="text-red-500 text-xs mt-1 italic">{{ form_errors(form.phone) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center justify-end pt-8 border-t border-gray-200 dark:border-gray-700 mt-10">
|
||||
<button type="submit" class="flex items-center justify-center text-white bg-blue-600 hover:bg-blue-700 focus:ring-4 focus:outline-none focus:ring-blue-300 font-bold rounded-lg text-sm px-8 py-3.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 transition-all shadow-lg hover:shadow-blue-500/30">
|
||||
Modifier
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{{ form_end(form) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
34
templates/mails/prestataire/create.twig
Normal file
34
templates/mails/prestataire/create.twig
Normal file
@@ -0,0 +1,34 @@
|
||||
{% extends 'mails/base.twig' %}
|
||||
|
||||
{% block content %}
|
||||
<mj-text>
|
||||
Bonjour <strong>{{ datas.prestataire.surname }} {{ datas.prestataire.name }}</strong>,
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
Un compte prestataire a été créé pour vous sur l'Intranet Ludikevent.
|
||||
</mj-text>
|
||||
<mj-text>
|
||||
Voici vos identifiants de connexion :
|
||||
</mj-text>
|
||||
<mj-table>
|
||||
<tr style="border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
|
||||
<th style="padding: 0 15px 0 0;">Identifiant (Email)</th>
|
||||
<td style="padding: 0 15px;">{{ datas.prestataire.email }}</td>
|
||||
</tr>
|
||||
<tr style="border-bottom:1px solid #ecedee;text-align:left;padding:15px 0;">
|
||||
<th style="padding: 0 15px 0 0;">Mot de passe</th>
|
||||
<td style="padding: 0 15px;">{{ datas.password }}</td>
|
||||
</tr>
|
||||
</mj-table>
|
||||
<mj-text>
|
||||
Nous vous conseillons de changer ce mot de passe dès votre première connexion.
|
||||
</mj-text>
|
||||
<mj-button href="{{ datas.login_url }}" align="center">
|
||||
Accéder à mon espace
|
||||
</mj-button>
|
||||
<mj-text>
|
||||
Si le bouton ne fonctionne pas, vous pouvez copier-coller le lien suivant dans votre navigateur :
|
||||
<br />
|
||||
<a href="{{ datas.login_url }}" class="link-style">{{ datas.login_url }}</a>
|
||||
</mj-text>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user