✨ 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:
@@ -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 }
|
||||
];
|
||||
|
||||
|
||||
26
assets/libs/TemplateApply.js
Normal file
26
assets/libs/TemplateApply.js
Normal 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();
|
||||
}
|
||||
}
|
||||
69
migrations/Version20260206170047.php
Normal file
69
migrations/Version20260206170047.php
Normal 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');
|
||||
}
|
||||
}
|
||||
@@ -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)',
|
||||
|
||||
@@ -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.");
|
||||
|
||||
|
||||
111
src/Controller/Dashboard/TemplatePointControleController.php
Normal file
111
src/Controller/Dashboard/TemplatePointControleController.php
Normal 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()]);
|
||||
}
|
||||
}
|
||||
51
src/Entity/TemplatePointControle.php
Normal file
51
src/Entity/TemplatePointControle.php
Normal 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;
|
||||
}
|
||||
}
|
||||
18
src/Repository/TemplatePointControleRepository.php
Normal file
18
src/Repository/TemplatePointControleRepository.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -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,13 +45,16 @@
|
||||
{% 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' }}">
|
||||
|
||||
@@ -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">
|
||||
|
||||
36
templates/dashboard/template_point_controle/edit.twig
Normal file
36
templates/dashboard/template_point_controle/edit.twig
Normal 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 %}
|
||||
54
templates/dashboard/template_point_controle/index.twig
Normal file
54
templates/dashboard/template_point_controle/index.twig
Normal 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 %}
|
||||
37
templates/dashboard/template_point_controle/new.twig
Normal file
37
templates/dashboard/template_point_controle/new.twig
Normal 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 Vérifier la soufflerie 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 %}
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user