feat(templates-points-controle): Ajoute la gestion et l'application de modèles de points de contrôle aux produits.

This commit is contained in:
Serreau Jovann
2026-02-06 18:04:13 +01:00
parent 65e3045cbc
commit 38f8762efe
14 changed files with 448 additions and 13 deletions

View File

@@ -14,6 +14,7 @@ import PlaningLogestics from "./libs/PlaningLogestics.js";
import {SortableReorder} from "./libs/SortableReorder.js";
import { StripeCommissionCalculator } from "./libs/StripeCommissionCalculator.js";
import { ProductAddOption } from "./libs/ProductAddOption.js";
import { TemplateApply } from "./libs/TemplateApply.js";
import { LeafletMap } from "./tools/LeafletMap.js";
// --- CONFIGURATION SENTRY ---
@@ -45,6 +46,7 @@ const registerCustomElements = () => {
{ name: 'crm-editor', class: CrmEditor, extends: 'textarea' },
{ name: 'stripe-commission-calculator', class: StripeCommissionCalculator, extends: 'div' },
{ name: 'product-add-option', class: ProductAddOption, extends: 'button' },
{ name: 'template-apply', class: TemplateApply, extends: 'button' },
{ name: 'leaflet-map', class: LeafletMap }
];

View File

@@ -0,0 +1,26 @@
export class TemplateApply extends HTMLButtonElement {
constructor() {
super();
this.addEventListener('click', this.apply.bind(this));
}
apply() {
const selectorId = this.getAttribute('data-selector');
const urlPattern = this.getAttribute('data-url-pattern');
const selector = document.querySelector(selectorId);
if (!selector) return;
const templateId = selector.value;
if (!templateId) return;
if (!confirm('Appliquer ce modèle ? Cela ajoutera les points de contrôle au produit.')) return;
const form = document.createElement('form');
form.method = 'POST';
form.action = urlPattern.replace('TEMPLATE_ID', templateId);
document.body.appendChild(form);
form.submit();
}
}

View File

@@ -0,0 +1,69 @@
<?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 Version20260206170047 extends AbstractMigration
{
public function getDescription(): string
{
return '';
}
public function up(Schema $schema): void
{
// this up() migration is auto-generated, please modify it to your needs
$this->addSql('CREATE TABLE template_point_controle (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, points JSON NOT NULL, PRIMARY KEY (id))');
$this->addSql('ALTER INDEX idx_9591436be30da2f7 RENAME TO IDX_7268396CBE3DB2B7');
$this->addSql('ALTER INDEX idx_8b27c52bbe30da2f7 RENAME TO IDX_8B27C52BBE3DB2B7');
$this->addSql('COMMENT ON COLUMN etat_lieux.updated_at IS \'\'');
$this->addSql('ALTER INDEX idx_d71603599b6b5fba RENAME TO IDX_D8D384179B6B5FBA');
$this->addSql('ALTER TABLE etat_lieux_comment ALTER id DROP DEFAULT');
$this->addSql('ALTER TABLE etat_lieux_comment ALTER id ADD GENERATED BY DEFAULT AS IDENTITY');
$this->addSql('COMMENT ON COLUMN etat_lieux_comment.created_at IS \'\'');
$this->addSql('ALTER INDEX idx_etat_lieux_comment RENAME TO IDX_C7341FDB3F1DAE3C');
$this->addSql('ALTER TABLE etat_lieux_file ALTER id DROP DEFAULT');
$this->addSql('ALTER TABLE etat_lieux_file ALTER id ADD GENERATED BY DEFAULT AS IDENTITY');
$this->addSql('COMMENT ON COLUMN etat_lieux_file.created_at IS \'\'');
$this->addSql('ALTER INDEX idx_5f7e1c87d6f39243 RENAME TO IDX_A76FE88B3F1DAE3C');
$this->addSql('ALTER TABLE etat_lieux_point_control ALTER id DROP DEFAULT');
$this->addSql('ALTER TABLE etat_lieux_point_control ALTER id ADD GENERATED BY DEFAULT AS IDENTITY');
$this->addSql('ALTER INDEX idx_etat_lieux_point_control RENAME TO IDX_84DA2663F1DAE3C');
$this->addSql('ALTER INDEX idx_88755657be30da2f7 RENAME TO IDX_263E7C9FBE3DB2B7');
$this->addSql('ALTER TABLE product_point_controll ALTER id DROP DEFAULT');
$this->addSql('ALTER TABLE product_point_controll ALTER id ADD GENERATED BY DEFAULT AS IDENTITY');
$this->addSql('ALTER INDEX idx_9e3c8f8f4584665a RENAME TO IDX_B6E9A5844584665A');
}
public function down(Schema $schema): void
{
// this down() migration is auto-generated, please modify it to your needs
$this->addSql('DROP TABLE template_point_controle');
$this->addSql('ALTER INDEX idx_7268396cbe3db2b7 RENAME TO idx_9591436be30da2f7');
$this->addSql('ALTER INDEX idx_8b27c52bbe3db2b7 RENAME TO idx_8b27c52bbe30da2f7');
$this->addSql('COMMENT ON COLUMN etat_lieux.updated_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER INDEX idx_d8d384179b6b5fba RENAME TO idx_d71603599b6b5fba');
$this->addSql('ALTER TABLE etat_lieux_comment ALTER id SET DEFAULT nextval(\'etat_lieux_comment_id_seq\'::regclass)');
$this->addSql('ALTER TABLE etat_lieux_comment ALTER id DROP IDENTITY');
$this->addSql('COMMENT ON COLUMN etat_lieux_comment.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER INDEX idx_c7341fdb3f1dae3c RENAME TO idx_etat_lieux_comment');
$this->addSql('ALTER TABLE etat_lieux_file ALTER id SET DEFAULT nextval(\'etat_lieux_file_id_seq\'::regclass)');
$this->addSql('ALTER TABLE etat_lieux_file ALTER id DROP IDENTITY');
$this->addSql('COMMENT ON COLUMN etat_lieux_file.created_at IS \'(DC2Type:datetime_immutable)\'');
$this->addSql('ALTER INDEX idx_a76fe88b3f1dae3c RENAME TO idx_5f7e1c87d6f39243');
$this->addSql('ALTER TABLE etat_lieux_point_control ALTER id SET DEFAULT nextval(\'etat_lieux_point_control_id_seq\'::regclass)');
$this->addSql('ALTER TABLE etat_lieux_point_control ALTER id DROP IDENTITY');
$this->addSql('ALTER INDEX idx_84da2663f1dae3c RENAME TO idx_etat_lieux_point_control');
$this->addSql('ALTER INDEX idx_263e7c9fbe3db2b7 RENAME TO idx_88755657be30da2f7');
$this->addSql('ALTER TABLE product_point_controll ALTER id SET DEFAULT nextval(\'product_point_controll_id_seq\'::regclass)');
$this->addSql('ALTER TABLE product_point_controll ALTER id DROP IDENTITY');
$this->addSql('ALTER INDEX idx_b6e9a5844584665a RENAME TO idx_9e3c8f8f4584665a');
}
}

View File

@@ -92,6 +92,7 @@ class CustomerController extends AbstractController
} catch (\Exception $e) {
$this->addFlash('warning', 'Erreur création Stripe : ' . $e->getMessage());
}
$this->em->flush();
$this->appLogger->record('CREATE', sprintf(
'Nouveau client créé : %s %s (Type: %s)',

View File

@@ -121,6 +121,7 @@ class ProductController extends AbstractController
$em->flush();
$stripe->createProduct($product);
$em->flush();
$logger->record('CREATE', "Nouveau produit : [{$product->getRef()}] {$product->getName()}");
$bus->dispatch(new DumpSitemapMessage());
@@ -132,7 +133,7 @@ class ProductController extends AbstractController
}
#[Route(path: '/crm/products/edit/{id}', name: 'app_crm_product_edit', methods: ['GET', 'POST'])]
public function productEdit(Product $product, EntityManagerInterface $em, AppLogger $logger, Request $request, Client $stripe, \App\Repository\ProductBlockedRepository $blockedRepo, \App\Repository\ProductReserveRepository $reserveRepo, OptionsRepository $optionsRepo): Response
public function productEdit(Product $product, EntityManagerInterface $em, AppLogger $logger, Request $request, Client $stripe, \App\Repository\ProductBlockedRepository $blockedRepo, \App\Repository\ProductReserveRepository $reserveRepo, OptionsRepository $optionsRepo, \App\Repository\TemplatePointControleRepository $templateRepo): Response
{
// 0. Toggle Publish
if ($request->query->get('act') === 'togglePublish') {
@@ -346,7 +347,8 @@ class ProductController extends AbstractController
'formBlocked' => $formBlocked->createView(),
'product' => $product,
'is_edit' => true,
'optionsList' => $optionsRepo->findBy([], ['name' => 'ASC'])
'optionsList' => $optionsRepo->findBy([], ['name' => 'ASC']),
'templates' => $templateRepo->findAll()
]);
}
#[Route(path: '/crm/products/delete/{id}', name: 'app_crm_product_delete', methods: ['POST'])]
@@ -378,6 +380,7 @@ class ProductController extends AbstractController
$em->flush();
$stripe->createOptions($option);
$em->flush();
$logger->record('CREATE', "Nouvelle option : {$option->getName()}");
$this->addFlash('success', "L'option {$option->getName()} a été ajoutée.");

View File

@@ -0,0 +1,111 @@
<?php
namespace App\Controller\Dashboard;
use App\Entity\Product;
use App\Entity\ProductPointControll;
use App\Entity\TemplatePointControle;
use App\Form\TemplatePointControleType;
use App\Repository\TemplatePointControleRepository;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[Route('/crm/template-point-controle')]
#[IsGranted('ROLE_ADMIN')]
class TemplatePointControleController extends AbstractController
{
#[Route('/', name: 'app_template_point_controle_index', methods: ['GET'])]
public function index(TemplatePointControleRepository $templatePointControleRepository): Response
{
return $this->render('dashboard/template_point_controle/index.twig', [
'templates' => $templatePointControleRepository->findAll(),
]);
}
#[Route('/new', name: 'app_template_point_controle_new', methods: ['GET', 'POST'])]
public function new(Request $request, EntityManagerInterface $entityManager): Response
{
$template = new TemplatePointControle();
if ($request->isMethod('POST')) {
$name = $request->request->get('name');
$pointsStr = $request->request->get('points');
$points = array_filter(array_map('trim', explode("
", $pointsStr)));
$template->setName($name);
$template->setPoints(array_values($points));
$entityManager->persist($template);
$entityManager->flush();
return $this->redirectToRoute('app_template_point_controle_index', [], Response::HTTP_SEE_OTHER);
}
return $this->render('dashboard/template_point_controle/new.twig', [
'template' => $template,
]);
}
#[Route('/{id}/edit', name: 'app_template_point_controle_edit', methods: ['GET', 'POST'])]
public function edit(Request $request, TemplatePointControle $template, EntityManagerInterface $entityManager): Response
{
if ($request->isMethod('POST')) {
$name = $request->request->get('name');
$pointsStr = $request->request->get('points');
$points = array_filter(array_map('trim', explode("
", $pointsStr)));
$template->setName($name);
$template->setPoints(array_values($points));
$entityManager->flush();
return $this->redirectToRoute('app_template_point_controle_index', [], Response::HTTP_SEE_OTHER);
}
return $this->render('dashboard/template_point_controle/edit.twig', [
'template' => $template,
]);
}
#[Route('/{id}', name: 'app_template_point_controle_delete', methods: ['POST'])]
public function delete(Request $request, TemplatePointControle $template, EntityManagerInterface $entityManager): Response
{
if ($this->isCsrfTokenValid('delete'.$template->getId(), $request->request->get('_token'))) {
$entityManager->remove($template);
$entityManager->flush();
}
return $this->redirectToRoute('app_template_point_controle_index', [], Response::HTTP_SEE_OTHER);
}
#[Route('/apply/{id}/product/{productId}', name: 'app_template_point_controle_apply', methods: ['POST'])]
public function apply(TemplatePointControle $template, int $productId, EntityManagerInterface $entityManager): Response
{
$product = $entityManager->getRepository(Product::class)->find($productId);
if (!$product) {
throw $this->createNotFoundException('Product not found');
}
foreach ($template->getPoints() as $pointName) {
$point = new ProductPointControll();
$point->setName($pointName);
$point->setProduct($product);
$entityManager->persist($point);
}
$entityManager->flush();
$this->addFlash('success', 'Template appliqué avec succès !');
return $this->redirectToRoute('app_crm_product_edit', ['id' => $product->getId()]);
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Entity;
use App\Repository\TemplatePointControleRepository;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity(repositoryClass: TemplatePointControleRepository::class)]
class TemplatePointControle
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column]
private ?int $id = null;
#[ORM\Column(length: 255)]
private ?string $name = null;
#[ORM\Column(type: Types::JSON)]
private array $points = [];
public function getId(): ?int
{
return $this->id;
}
public function getName(): ?string
{
return $this->name;
}
public function setName(string $name): static
{
$this->name = $name;
return $this;
}
public function getPoints(): array
{
return $this->points;
}
public function setPoints(array $points): static
{
$this->points = $points;
return $this;
}
}

View File

@@ -0,0 +1,18 @@
<?php
namespace App\Repository;
use App\Entity\TemplatePointControle;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\Persistence\ManagerRegistry;
/**
* @extends ServiceEntityRepository<TemplatePointControle>
*/
class TemplatePointControleRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, TemplatePointControle::class);
}
}

View File

@@ -14,7 +14,7 @@
{% if not is_online() %}
<div class="bg-red-600 text-white text-center py-2 px-4 font-bold text-sm sticky top-0 z-[60]">
Votre site internet n'est actuellement pas en ligne. Aucun référencement n'est possible. Pour demander sa mise en ligne, rendez-vous dans votre intranet pour effectuer la demande d'activation.
Votre site internet n'est actuellement pas en ligne. Aucun référencement n'est possible et aucun paiement ne sera possible. Pour demander sa mise en ligne, rendez-vous dans votre intranet pour effectuer la demande d'activation.
</div>
{% endif %}
@@ -45,14 +45,17 @@
{% endmacro %}
{% import _self as menu %}
{{ menu.nav_link(path('app_crm'), 'Dashboard', '<path 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"></path>', 'app_crm') }}
{{ menu.nav_link(path('app_crm_reservation'), 'Planing de réservation', '<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_reservation') }}
{{ menu.nav_link(path('app_crm_product'), 'Produits', '<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_product') }}
{{ 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') }}
{{ menu.nav_link(path('app_crm'), 'Dashboard', '<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6A2.25 2.25 0 016 3.75h2.25A2.25 2.25 0 0110.5 6v2.25a2.25 2.25 0 01-2.25 2.25H6a2.25 2.25 0 01-2.25-2.25V6zM3.75 15.75A2.25 2.25 0 016 13.5h2.25a2.25 2.25 0 012.25 2.25V18a2.25 2.25 0 01-2.25 2.25H6A2.25 2.25 0 013.75 18v-2.25zM13.5 6a2.25 2.25 0 012.25-2.25H18A2.25 2.25 0 0120.25 6v2.25A2.25 2.25 0 0118 10.5h-2.25a2.25 2.25 0 01-2.25-2.25V6zM13.5 15.75a2.25 2.25 0 012.25-2.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-2.25A2.25 2.25 0 0113.5 18v-2.25z" />', 'app_crm') }}
{{ menu.nav_link(path('app_crm_reservation'), 'Planing de réservation', '<path stroke-linecap="round" stroke-linejoin="round" d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 012.25-2.25h13.5A2.25 2.25 0 0121 7.5v11.25m-18 0A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75m-18 0v-7.5A2.25 2.25 0 015.25 9h13.5A2.25 2.25 0 0121 11.25v7.5" />', 'app_crm_reservation') }}
{{ menu.nav_link(path('app_template_point_controle_index'), 'Modèles de contrôle', '<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />', 'app_template_point_controle_index') }}
{{ menu.nav_link(path('app_crm_product'), 'Produits', '<path stroke-linecap="round" stroke-linejoin="round" d="M20.25 7.5l-.625 10.632a2.25 2.25 0 01-2.247 2.118H6.622a2.25 2.25 0 01-2.247-2.118L3.75 7.5M10 11.25h4M3.375 7.5h17.25c.621 0 1.125-.504 1.125-1.125v-1.5c0-.621-.504-1.125-1.125-1.125H3.375c-.621 0-1.125.504-1.125 1.125v1.5c0 .621.504 1.125 1.125 1.125z" />', 'app_crm_product') }}
{{ menu.nav_link(path('app_crm_formules'), 'Formules', '<path stroke-linecap="round" stroke-linejoin="round" d="M21 7.5l-9-5.25L3 7.5m18 0l-9 5.25m9-5.25v9l-9 5.25M3 7.5l9 5.25M3 7.5v9l9 5.25m0-9v9" />', 'app_crm_formules') }}
{{ menu.nav_link(path('app_crm_facture'), 'Facture', '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 14.25v-2.625a3.375 3.375 0 00-3.375-3.375h-1.5A1.125 1.125 0 0113.5 7.125v-1.5a3.375 3.375 0 00-3.375-3.375H8.25m2.25 0H5.625c-.621 0-1.125.504-1.125 1.125v17.25c0 .621.504 1.125 1.125 1.125h12.75c.621 0 1.125-.504 1.125-1.125V11.25a9 9 0 00-9-9z" />', 'app_crm_facture') }}
{{ menu.nav_link(path('app_crm_customer'), 'Clients', '<path stroke-linecap="round" stroke-linejoin="round" d="M15 19.128a9.38 9.38 0 002.625.372 9.337 9.337 0 004.121-.952 4.125 4.125 0 00-7.533-2.493M15 19.128v-.003c0-1.113-.285-2.16-.786-3.07M15 19.128v.106A12.318 12.318 0 018.624 21c-2.331 0-4.512-.645-6.374-1.766l-.001-.109a6.375 6.375 0 0111.964-3.07M12 6.375a3.375 3.375 0 11-6.75 0 3.375 3.375 0 016.75 0zm8.25 2.25a2.625 2.625 0 11-5.25 0 2.625 2.625 0 015.25 0z" />', 'app_crm_customer') }}
{{ menu.nav_link(path('app_crm_devis'), 'Devis', '<path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12c0-1.232-.046-2.453-.138-3.662a4.006 4.006 0 00-3.7-3.7 48.678 48.678 0 00-7.324 0 4.006 4.006 0 00-3.7 3.7c-.017.22-.032.441-.046.662M19.5 12l3-3m-3 3l-3-3m-12 3c0 1.232.046 2.453.138 3.662a4.006 4.006 0 003.7 3.7 48.656 48.656 0 007.324 0 4.006 4.006 0 003.7-3.7c.017-.22.032-.441.046-.662M4.5 12l3 3m-3-3l-3 3" />', 'app_crm_devis') }}
{{ menu.nav_link(path('app_crm_contrats'), 'Contrat de location', '<path stroke-linecap="round" stroke-linejoin="round" d="M11.35 3.836c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />', 'app_crm_contrats') }}
{{ menu.nav_link(path('app_crm_prestataire'), 'Prestataires', '<path stroke-linecap="round" stroke-linejoin="round" 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' }}">
<div class="flex items-center space-x-3">
@@ -223,4 +226,4 @@
</div>
</body>
</html>
</html>

View File

@@ -618,6 +618,30 @@
Points de Contrôle (Entretien)
</h3>
{# TEMPLATE SELECTION #}
{% if templates is defined and templates|length > 0 %}
<div class="mb-8 p-6 bg-slate-900/30 rounded-2xl border border-white/5 backdrop-blur-sm">
<label class="text-[10px] font-black text-slate-300 uppercase tracking-widest mb-3 block flex items-center gap-2">
<svg class="w-3 h-3 text-teal-500" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7v8a2 2 0 002 2h6M8 7V5a2 2 0 012-2h4.586a1 1 0 01.707.293l4.414 4.414a1 1 0 01.293.707V15a2 2 0 01-2 2h-2M8 7H6a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2v-2"></path></svg>
Importer un modèle
</label>
<div class="flex flex-col sm:flex-row gap-4">
<select id="template-selector" class="flex-1 bg-slate-900/50 border-white/5 rounded-2xl text-white py-3 px-4 text-sm focus:ring-teal-500/20 focus:border-teal-500 transition-all outline-none">
<option value="">Choisir un modèle...</option>
{% for t in templates %}
<option value="{{ t.id }}">{{ t.name }} ({{ t.points|length }} points)</option>
{% endfor %}
</select>
<button is="template-apply" type="button"
data-selector="#template-selector"
data-url-pattern="{{ path('app_template_point_controle_apply', {'id': 'TEMPLATE_ID', 'productId': product.id}) }}"
class="px-6 py-3 bg-teal-600 hover:bg-teal-500 text-white text-[10px] font-black uppercase tracking-widest rounded-2xl transition-all shadow-lg shadow-teal-600/20 whitespace-nowrap">
Appliquer
</button>
</div>
</div>
{% endif %}
<div class="space-y-3 mb-8">
{% for point in product.productPointControlls %}
<div class="flex items-center justify-between p-4 bg-white/5 border border-white/5 rounded-2xl group hover:bg-white/10 transition-all">

View File

@@ -0,0 +1,36 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Éditer le modèle{% endblock %}
{% block body %}
<div class="max-w-2xl mx-auto space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-white">Éditer le modèle</h1>
<a href="{{ path('app_template_point_controle_index') }}" class="text-slate-400 hover:text-white transition-colors">Retour</a>
</div>
<div class="bg-slate-800 rounded-xl p-6 shadow-sm border border-slate-700">
<form method="post" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-slate-300 mb-2">Nom du modèle</label>
<input type="text" id="name" name="name" value="{{ template.name }}" required
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all">
</div>
<div>
<label for="points" class="block text-sm font-medium text-slate-300 mb-2">Points de contrôle (un par ligne)</label>
<textarea id="points" name="points" rows="10" required
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all font-mono text-sm">{{ template.points|join('
') }}</textarea>
<p class="mt-1 text-xs text-slate-500">Saisissez chaque point de contrôle sur une nouvelle ligne.</p>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
Mettre à jour
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,54 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Modèles de points de contrôle{% endblock %}
{% block body %}
<div class="space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-white">Modèles de points de contrôle</h1>
<a href="{{ path('app_template_point_controle_new') }}" class="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors">
Nouveau modèle
</a>
</div>
<div class="bg-slate-800 rounded-xl overflow-hidden shadow-sm border border-slate-700">
<table class="w-full text-left text-sm text-slate-400">
<thead class="bg-slate-900/50 text-xs uppercase font-medium text-slate-300">
<tr>
<th class="px-6 py-4">Nom</th>
<th class="px-6 py-4">Points</th>
<th class="px-6 py-4 text-right">Actions</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-700">
{% for template in templates %}
<tr class="hover:bg-slate-700/30 transition-colors">
<td class="px-6 py-4 font-medium text-white">{{ template.name }}</td>
<td class="px-6 py-4">
<ul class="list-disc list-inside">
{% for point in template.points|slice(0, 3) %}
<li>{{ point }}</li>
{% endfor %}
{% if template.points|length > 3 %}
<li class="list-none text-xs italic opacity-70">+ {{ template.points|length - 3 }} autres...</li>
{% endif %}
</ul>
</td>
<td class="px-6 py-4 text-right space-x-2">
<a href="{{ path('app_template_point_controle_edit', {'id': template.id}) }}" class="text-blue-400 hover:text-blue-300 transition-colors">Éditer</a>
<form method="post" action="{{ path('app_template_point_controle_delete', {'id': template.id}) }}" class="inline-block" onsubmit="return confirm('Êtes-vous sûr ?');">
<input type="hidden" name="_token" value="{{ csrf_token('delete' ~ template.id) }}">
<button class="text-red-400 hover:text-red-300 transition-colors">Supprimer</button>
</form>
</td>
</tr>
{% else %}
<tr>
<td colspan="3" class="px-6 py-8 text-center italic opacity-50">Aucun modèle trouvé.</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
{% endblock %}

View File

@@ -0,0 +1,37 @@
{% extends 'dashboard/base.twig' %}
{% block title %}Nouveau modèle de points de contrôle{% endblock %}
{% block body %}
<div class="max-w-2xl mx-auto space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-2xl font-bold text-white">Nouveau modèle</h1>
<a href="{{ path('app_template_point_controle_index') }}" class="text-slate-400 hover:text-white transition-colors">Retour</a>
</div>
<div class="bg-slate-800 rounded-xl p-6 shadow-sm border border-slate-700">
<form method="post" class="space-y-6">
<div>
<label for="name" class="block text-sm font-medium text-slate-300 mb-2">Nom du modèle</label>
<input type="text" id="name" name="name" required
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all"
placeholder="Ex: Contrôle structure gonflable">
</div>
<div>
<label for="points" class="block text-sm font-medium text-slate-300 mb-2">Points de contrôle (un par ligne)</label>
<textarea id="points" name="points" rows="10" required
class="w-full bg-slate-900 border border-slate-700 rounded-lg px-4 py-2 text-white focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition-all font-mono text-sm"
placeholder="Vérifier les coutures&#10;Vérifier la soufflerie&#10;Nettoyer la bâche..."></textarea>
<p class="mt-1 text-xs text-slate-500">Saisissez chaque point de contrôle sur une nouvelle ligne.</p>
</div>
<div class="flex justify-end pt-4">
<button type="submit" class="px-6 py-2 bg-blue-600 text-white font-medium rounded-lg hover:bg-blue-700 transition-colors">
Créer le modèle
</button>
</div>
</form>
</div>
</div>
{% endblock %}

View File

@@ -85,7 +85,7 @@
{% if not is_online() %}
<div class="bg-red-600 text-white text-center py-2 px-4 font-bold text-sm sticky top-0 z-[60]">
Votre site internet n'est actuellement pas en ligne. Aucun référencement n'est possible. Pour demander sa mise en ligne, rendez-vous dans votre intranet pour effectuer la demande d'activation.
Votre site internet n'est actuellement pas en ligne. Aucun référencement n'est possible et aucun paiement ne sera possible. Pour demander sa mise en ligne, rendez-vous dans votre intranet pour effectuer la demande d'activation.
</div>
{% endif %}