diff --git a/assets/libs/RepeatLine.js b/assets/libs/RepeatLine.js
index 6515963..b46051b 100644
--- a/assets/libs/RepeatLine.js
+++ b/assets/libs/RepeatLine.js
@@ -42,39 +42,41 @@ export class RepeatLine extends HTMLDivElement {
addRow() {
if (!this.rowHTML || this.$refs.rows.children.length >= this.$props.maxRows) return;
- // Création de la nouvelle ligne
let newRow = this.createFromHTML(this.rowHTML);
newRow.removeAttribute('id');
- // Nettoyage spécifique pour TomSelect avant insertion
- // Si on clone une ligne qui avait déjà TomSelect, on reset le select
newRow.querySelectorAll('select').forEach(select => {
- // Supprimer les classes et éléments injectés par TomSelect si présents dans le template
- select.classList.remove('tomselect', 'ts-hidden-visually');
- select.innerHTML = 'Sélectionner... ';
- // Supprimer le wrapper TomSelect s'il a été cloné par erreur
- const wrapper = select.nextElementSibling;
- if (wrapper && wrapper.classList.contains('ts-wrapper')) {
- wrapper.remove();
+ // 1. Si c'est un select TomSelect (sans attribut 'is')
+ if (!select.hasAttribute('ds')) {
+ select.classList.remove('tomselect', 'ts-hidden-visually');
+ select.innerHTML = 'Sélectionner... ';
+
+ const wrapper = select.nextElementSibling;
+ if (wrapper && wrapper.classList.contains('ts-wrapper')) {
+ wrapper.remove();
+ }
+ }
+ // 2. Si c'est votre select "Type" (avec l'attribut 'is')
+ else {
+ // On ne touche PAS au innerHTML pour garder les options (Structure, etc.)
+ select.value = ""; // On remet juste à zéro la sélection
}
});
this.setUpRow(newRow);
this.$refs.rows.appendChild(newRow);
- // Réinitialisation des valeurs
- newRow.querySelectorAll('input,textarea,select').forEach(el => {
+ // Réinitialisation des autres champs
+ newRow.querySelectorAll('input,textarea').forEach(el => {
el.value = "";
- if (el.tagName === 'SELECT') el.selectedIndex = 0;
});
- // --- INITIALISATION TOMSELECT SUR LA NOUVELLE LIGNE ---
+ // Initialisation TomSelect uniquement sur ce qui doit l'être
initTomSelect(newRow);
this.updateFieldNames();
this.updateAddButton();
- // Focus sur le premier élément de la nouvelle ligne
const firstInput = newRow.querySelector('input,textarea,select');
if (firstInput) firstInput.focus();
}
diff --git a/assets/libs/initTomSelect.js b/assets/libs/initTomSelect.js
index 1e792c5..917df8e 100644
--- a/assets/libs/initTomSelect.js
+++ b/assets/libs/initTomSelect.js
@@ -1,14 +1,17 @@
-// Cache pour éviter les requêtes HTTP répétitives
import TomSelect from "tom-select";
+// Cache séparé pour éviter les conflits entre produits et options
let productCache = null;
+let optionsCache = null;
/**
* Initialise TomSelect sur un élément ou un groupe d'éléments
*/
export function initTomSelect(parent = document) {
parent.querySelectorAll('select').forEach((el) => {
- if (el.tomselect) return;
+ // --- CLAUSES DE GARDE ---
+ // On ignore si déjà initialisé OU si l'élément possède l'attribut "is"
+ if (el.tomselect || el.hasAttribute('ds')) return;
// --- CONFIGURATION PRODUITS ---
if (el.getAttribute('data-load') === "product") {
@@ -19,34 +22,23 @@ export function initTomSelect(parent = document) {
searchField: 'name',
options: data,
maxOptions: null,
- // LORSQU'ON SÉLECTIONNE UN PRODUIT
- // Dans admin.js, section onChange de TomSelect :
onChange: (id) => {
if (!id) return;
-
- // On s'assure de trouver le produit (id peut être string ou int)
const product = data.find(p => String(p.id) === String(id));
-
if (product) {
- // On remonte au parent le plus proche (le bloc de ligne du devis)
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
if (!row) return;
- // Ciblage précis des inputs
const priceInput = row.querySelector('input[name*="[price_ht]"]');
const priceSupInput = row.querySelector('input[name*="[price_sup_ht]"]');
if (priceInput) {
priceInput.value = product.price1day;
- // Indispensable pour que d'autres scripts (calcul totaux) voient le changement
priceInput.dispatchEvent(new Event('input', { bubbles: true }));
- priceInput.dispatchEvent(new Event('change', { bubbles: true }));
}
-
if (priceSupInput) {
priceSupInput.value = product.priceSup;
priceSupInput.dispatchEvent(new Event('input', { bubbles: true }));
- priceSupInput.dispatchEvent(new Event('change', { bubbles: true }));
}
}
},
@@ -59,16 +51,11 @@ export function initTomSelect(parent = document) {
J1: ${escape(data.price1day)}€ | Sup: ${escape(data.priceSup)}€
`,
- item: (data, escape) => `
-
-
- ${escape(data.name)}
-
`
+ item: (data, escape) => ` ${escape(data.name)}
`
}
});
};
- // Utilisation du cache ou fetch
if (productCache) {
setupSelect(productCache);
} else {
@@ -80,6 +67,7 @@ export function initTomSelect(parent = document) {
});
}
}
+ // --- CONFIGURATION OPTIONS ---
else if (el.getAttribute('data-load') === "options") {
const setupSelect = (data) => {
new TomSelect(el, {
@@ -88,12 +76,8 @@ export function initTomSelect(parent = document) {
searchField: 'name',
options: data,
maxOptions: null,
- // LORSQU'ON SÉLECTIONNE UN PRODUIT
- // Dans admin.js, section onChange de TomSelect :
onChange: (id) => {
if (!id) return;
-
- // On s'assure de trouver le produit (id peut être string ou int)
const product = data.find(p => String(p.id) === String(id));
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
if (!row) return;
@@ -101,11 +85,8 @@ export function initTomSelect(parent = document) {
if (priceInput) {
priceInput.value = product.price;
- // Indispensable pour que d'autres scripts (calcul totaux) voient le changement
priceInput.dispatchEvent(new Event('input', { bubbles: true }));
- priceInput.dispatchEvent(new Event('change', { bubbles: true }));
}
-
},
render: {
option: (data, escape) => `
@@ -116,28 +97,23 @@ export function initTomSelect(parent = document) {
${escape(data.price)}€
`,
- item: (data, escape) => `
-
-
- ${escape(data.name)}
-
`
+ item: (data, escape) => ` ${escape(data.name)}
`
}
});
};
- // Utilisation du cache ou fetch
- if (productCache) {
- setupSelect(productCache);
+ if (optionsCache) {
+ setupSelect(optionsCache);
} else {
fetch("/crm/options/json")
.then(r => r.json())
.then(data => {
- productCache = data;
+ optionsCache = data;
setupSelect(data);
});
}
}
- // --- AUTRES SELECTS ---
+ // --- AUTRES SELECTS STANDARDS ---
else {
new TomSelect(el, {
controlInput: null,
diff --git a/migrations/Version20260128142222.php b/migrations/Version20260128142222.php
new file mode 100644
index 0000000..12e72c5
--- /dev/null
+++ b/migrations/Version20260128142222.php
@@ -0,0 +1,36 @@
+addSql('CREATE TABLE formules_restriction (id SERIAL NOT NULL, formule_id INT DEFAULT NULL, nb_structure_max INT NOT NULL, restriction_config TEXT DEFAULT NULL, PRIMARY KEY(id))');
+ $this->addSql('CREATE UNIQUE INDEX UNIQ_D3436D392A68F4D1 ON formules_restriction (formule_id)');
+ $this->addSql('COMMENT ON COLUMN formules_restriction.restriction_config IS \'(DC2Type:array)\'');
+ $this->addSql('ALTER TABLE formules_restriction ADD CONSTRAINT FK_D3436D392A68F4D1 FOREIGN KEY (formule_id) REFERENCES formules (id) NOT DEFERRABLE INITIALLY IMMEDIATE');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('CREATE SCHEMA public');
+ $this->addSql('ALTER TABLE formules_restriction DROP CONSTRAINT FK_D3436D392A68F4D1');
+ $this->addSql('DROP TABLE formules_restriction');
+ }
+}
diff --git a/migrations/Version20260128144635.php b/migrations/Version20260128144635.php
new file mode 100644
index 0000000..f811bfb
--- /dev/null
+++ b/migrations/Version20260128144635.php
@@ -0,0 +1,34 @@
+addSql('ALTER TABLE formules_restriction ADD nb_alimentaire_max INT DEFAULT NULL');
+ $this->addSql('ALTER TABLE formules_restriction ADD nb_barhums_max INT DEFAULT NULL');
+ }
+
+ public function down(Schema $schema): void
+ {
+ // this down() migration is auto-generated, please modify it to your needs
+ $this->addSql('CREATE SCHEMA public');
+ $this->addSql('ALTER TABLE formules_restriction DROP nb_alimentaire_max');
+ $this->addSql('ALTER TABLE formules_restriction DROP nb_barhums_max');
+ }
+}
diff --git a/src/Command/GitSyncLogCommand.php b/src/Command/GitSyncLogCommand.php
index 132a8dc..4c5ae79 100644
--- a/src/Command/GitSyncLogCommand.php
+++ b/src/Command/GitSyncLogCommand.php
@@ -8,29 +8,45 @@ use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Process\Process;
-// Utilisation des namespaces que tu as fournis
+use Symfony\Component\HttpKernel\KernelInterface; // Pour le dossier projet
+use Symfony\Contracts\HttpClient\HttpClientInterface;
use GeminiAPI\Client;
use GeminiAPI\Resources\Parts\TextPart;
#[AsCommand(
name: 'app:git-log-update',
- description: 'Archive le dernier commit avec reformulation IA pour le client.',
+ description: 'Archive le dernier commit avec reformulation IA et notification Discord.',
)]
class GitSyncLogCommand extends Command
{
+ private HttpClientInterface $httpClient;
+ private string $projectDir;
+
+ // On injecte le Kernel pour le chemin du projet et HttpClient pour Discord
+ public function __construct(HttpClientInterface $httpClient, KernelInterface $kernel)
+ {
+ parent::__construct();
+ $this->httpClient = $httpClient;
+ $this->projectDir = $kernel->getProjectDir();
+ }
+
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
- $filePath = 'var/update.json';
- $projectDir = '/srv/app';
+
+ // Chemins dynamiques basés sur ProjectDir
+ $filePath = $this->projectDir . '/var/update.json';
+ $discordWebhook = 'https://discord.com/api/webhooks/1447983279902031963/O6P5oHVHFe2t2MgjFmOW-tOVrvdLf3JQDPAj8snlgKIrfGc8uJQKAHgqRJJjyoSsFYCR';
// 1. Récupération des infos Git
+ // On utilise $this->projectDir pour le safe.directory
$gitCmd = sprintf(
'git config --global --add safe.directory %s && git log -1 --format="%%s|%%ci|%%h"',
- $projectDir
+ $this->projectDir
);
$process = Process::fromShellCommandline($gitCmd);
+ $process->setWorkingDirectory($this->projectDir); // On force le dossier de travail
$process->run();
if (!$process->isSuccessful()) {
@@ -46,60 +62,41 @@ class GitSyncLogCommand extends Command
// 2. Détermination du TYPE (feature, fix, optimise, new)
$type = 'new';
$lowerMsg = strtolower($rawMessage);
+ if (preg_match('/(fix|bug|patch|correct)/', $lowerMsg)) $type = 'fix';
+ elseif (preg_match('/(feat|add|create|nouveau|new)/', $lowerMsg)) $type = 'feature';
+ elseif (preg_match('/(perf|opti|refactor|clean|speed)/', $lowerMsg)) $type = 'optimise';
- if (preg_match('/(fix|bug|patch|correct)/', $lowerMsg)) {
- $type = 'fix';
- } elseif (preg_match('/(feat|add|create|nouveau|new)/', $lowerMsg)) {
- $type = 'feature';
- } elseif (preg_match('/(perf|opti|refactor|clean|speed)/', $lowerMsg)) {
- $type = 'optimise';
- }
-
- // 3. Vérification anti-doublon (Basée sur le Hash)
+ // 3. Vérification anti-doublon
$data = [];
if (file_exists($filePath)) {
$data = json_decode(file_get_contents($filePath), true) ?? [];
}
if (!empty($data) && $data[0]['hash'] === $commitHash) {
- $io->info("Le commit [$commitHash] est déjà dans le journal client.");
+ $io->info("Le commit [$commitHash] est déjà à jour.");
return Command::SUCCESS;
}
- // 4. Appel IA Gemini-3-Pro-Preview pour la reformulation
+ // 4. Appel IA Gemini
$friendlyMessage = $rawMessage;
try {
- // Ta clé API
$client = new Client("AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg");
$model = 'gemini-3-pro-preview';
$prompt = "Tu es un expert en communication web pour Ludik Event. Ta mission est de transformer
un message de commit technique en une note de mise à jour élégante pour ton client.
-
MESSAGE TECHNIQUE : \"$rawMessage\"
+ DIRECTIVES : Court, positif, pas de 'Voici la phrase', uniquement le résultat final.";
- DIRECTIVES :
- 1. Reformule pour un propriétaire de site non-technique.
- 2. Sois court, positif et rassurant.
- 3. Ne commence JAMAIS par 'Voici la phrase' ou 'Mise à jour'.
- 4. Donne uniquement le texte final prêt à être affiché.
-
- RÉSULTAT ATTENDU :";
-
- $response = $client->withV1BetaVersion()->generativeModel($model)->generateContent(
- new TextPart($prompt)
- );
-
- // Adaptation selon la structure de retour du SDK
- $aiText = $response->text();
- if ($aiText) {
- $friendlyMessage = trim($aiText);
+ $response = $client->withV1BetaVersion()->generativeModel($model)->generateContent(new TextPart($prompt));
+ if ($response->text()) {
+ $friendlyMessage = trim($response->text());
}
} catch (\Exception $e) {
- $io->warning("L'IA n'a pas pu traiter le message. Utilisation du texte brut.");
+ $io->warning("L'IA n'a pas pu traiter le message.");
}
- // 5. Création de l'entrée JSON
+ // 5. Mise à jour du fichier JSON
$newEntry = [
'type' => $type,
'message' => $friendlyMessage,
@@ -107,18 +104,29 @@ class GitSyncLogCommand extends Command
'hash' => $commitHash
];
- // 6. Sauvegarde et rotation (5 max)
array_unshift($data, $newEntry);
$data = array_slice($data, 0, 6);
- if (!is_dir('var')) {
- mkdir('var', 0777, true);
+ $varDir = $this->projectDir . '/var';
+ if (!is_dir($varDir)) {
+ mkdir($varDir, 0777, true);
}
file_put_contents($filePath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
- $io->success("Journal client mis à jour avec succès (Type: $type).");
+ // 6. Envoi Discord
+ try {
+ $discordMessage = "📢 **Mise à jour sera prochaine mise en ligne sur votre intranet**\n\n> " . $friendlyMessage;
+ $this->httpClient->request('POST', $discordWebhook, [
+ 'json' => ['content' => $discordMessage]
+ ]);
+ $io->note("Notification Discord envoyée.");
+ } catch (\Exception $e) {
+ $io->error("Erreur Discord : " . $e->getMessage());
+ }
+
+ $io->success("Journal client mis à jour avec succès.");
return Command::SUCCESS;
}
}
diff --git a/src/Controller/Dashboard/FormulesController.php b/src/Controller/Dashboard/FormulesController.php
index 69f66ee..8b48919 100644
--- a/src/Controller/Dashboard/FormulesController.php
+++ b/src/Controller/Dashboard/FormulesController.php
@@ -5,6 +5,7 @@ namespace App\Controller\Dashboard;
use App\Entity\Formules;
use App\Entity\FormulesOptionsInclus;
use App\Entity\FormulesProductInclus;
+use App\Entity\FormulesRestriction;
use App\Entity\Options;
use App\Entity\Product;
use App\Entity\ProductDoc;
@@ -59,6 +60,8 @@ class FormulesController extends AbstractController
if ($this->isCsrfTokenValid('delete' . $formules->getId(), $request->query->get('_token'))) {
$nomFormule = $formules->getName();
+ if($formules->getFormulesRestriction() instanceof FormulesRestriction)
+ $entityManager->remove($formules->getFormulesRestriction());
// 3. Suppression
foreach ($formules->getFormulesProductIncluses() as $formulesProductInclus)
$entityManager->remove($formulesProductInclus);
@@ -88,6 +91,15 @@ class FormulesController extends AbstractController
$form->handleRequest($request);
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($formules);
+ if($formules->getType() == "free") {
+ $formuleRestriction = new FormulesRestriction();
+ $formuleRestriction->setFormule($formules);
+ $formuleRestriction->setNbStructureMax(0);
+ $formuleRestriction->setNbAlimentaireMax(0);
+ $formuleRestriction->setNbBarhumsMax(0);
+ $formuleRestriction->setRestrictionConfig([]);
+ $entityManager->persist($formuleRestriction);
+ }
$entityManager->flush();
$appLogger->record('CREATED', "Création de la formule : " . $formules->getName());
$this->addFlash("success", "La formule a été créée avec succès.");
@@ -159,6 +171,17 @@ class FormulesController extends AbstractController
return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
}
+ if ($request->isMethod('POST') && $request->request->has('rest')) {
+ $rest = $request->request->all('rest');
+ $f = $formules->getFormulesRestriction()->setRestrictionConfig($rest);
+ $entityManager->persist($f);
+ $entityManager->flush();
+
+ $appLogger->record('UPDATE', "Mise à jour des restriction pour : " . $formules->getName());
+ $this->addFlash("success", "Les restriction ont été mis à jour.");
+
+ return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
+ }
// 2. GESTION DES PRIX (Formulaire Manuel price[])
// On vérifie si le tableau 'price' existe dans la requête POST
@@ -183,8 +206,41 @@ class FormulesController extends AbstractController
$form = $this->createForm(FormulesType::class, $formules);
$form->handleRequest($request);
+
+ if ($request->isMethod('POST') && $request->request->has('nbStructureMax')) {
+ // Vérification du type de formule
+ if ($formules->getType() === "free") {
+ $nbStructureMax = $request->request->get('nbStructureMax');
+ $nbBarhumsMax = $request->request->get('nbBarhumsMax');
+ $nbAlimentaireMax = $request->request->get('nbAlimentaireMax');
+ $rc = $formules->getFormulesRestriction();
+
+ // Si la restriction existe, on met à jour la valeur
+ if ($rc) {
+ $rc->setNbStructureMax((int)$nbStructureMax);
+ $rc->setNbBarhumsMax((int)$nbBarhumsMax);
+ $rc->setNbAlimentaireMax((int)$nbAlimentaireMax);
+
+ // Persistance des modifications
+ $entityManager->persist($rc);
+ $entityManager->flush();
+
+ // Log de l'activité
+ $appLogger->record('UPDATE', "Modification de la formule (restriction) : " . $formules->getName());
+
+ $this->addFlash("success", "La restriction de la formule a été modifiée avec succès.");
+ } else {
+ $this->addFlash("error", "Aucune restriction trouvée pour cette formule.");
+ }
+ } else {
+ $this->addFlash("warning", "Le nombre de structures max n'est modifiable que pour les formules gratuites.");
+ }
+
+ return $this->redirectToRoute('app_crm_formules_view', ['id' => $formules->getId()]);
+ }
if ($form->isSubmitted() && $form->isValid()) {
$entityManager->persist($formules);
+ // 2. Sauvegarde globale
$entityManager->flush();
$appLogger->record('UPDATE', "Modification de la formule (infos) : " . $formules->getName());
@@ -211,12 +267,22 @@ class FormulesController extends AbstractController
$options[$key]['product'] = $fc->getName();
$options[$key]['id'] = $fc->getId();
}
+ $restriction =[
+ [
+ 'type' => 'structure',
+ 'product' => ''
+ ]
+ ];
+ if(!empty($formules->getFormulesRestriction()->getRestrictionConfig())){
+ $restriction = $formules->getFormulesRestriction()->getRestrictionConfig();
+ }
return $this->render('dashboard/formules/view.twig', [
'formule' => $formules,
'form' => $form->createView(),
'type' => $formules->getType(),
'lines' => $lines,
'option' => $options,
+ 'restriction' => $restriction,
]);
}
diff --git a/src/Entity/Formules.php b/src/Entity/Formules.php
index 7487593..d348421 100644
--- a/src/Entity/Formules.php
+++ b/src/Entity/Formules.php
@@ -69,6 +69,9 @@ class Formules
#[ORM\OneToMany(targetEntity: FormulesOptionsInclus::class, mappedBy: 'formule')]
private Collection $formulesOptionsIncluses;
+ #[ORM\OneToOne(mappedBy: 'formule', cascade: ['persist', 'remove'])]
+ private ?FormulesRestriction $formulesRestriction = null;
+
public function __construct()
{
$this->formulesProductIncluses = new ArrayCollection();
@@ -307,4 +310,26 @@ class Formules
return $this;
}
+
+ public function getFormulesRestriction(): ?FormulesRestriction
+ {
+ return $this->formulesRestriction;
+ }
+
+ public function setFormulesRestriction(?FormulesRestriction $formulesRestriction): static
+ {
+ // unset the owning side of the relation if necessary
+ if ($formulesRestriction === null && $this->formulesRestriction !== null) {
+ $this->formulesRestriction->setFormule(null);
+ }
+
+ // set the owning side of the relation if necessary
+ if ($formulesRestriction !== null && $formulesRestriction->getFormule() !== $this) {
+ $formulesRestriction->setFormule($this);
+ }
+
+ $this->formulesRestriction = $formulesRestriction;
+
+ return $this;
+ }
}
diff --git a/src/Entity/FormulesRestriction.php b/src/Entity/FormulesRestriction.php
new file mode 100644
index 0000000..8e81c6c
--- /dev/null
+++ b/src/Entity/FormulesRestriction.php
@@ -0,0 +1,96 @@
+id;
+ }
+
+ public function getFormule(): ?Formules
+ {
+ return $this->formule;
+ }
+
+ public function setFormule(?Formules $formule): static
+ {
+ $this->formule = $formule;
+
+ return $this;
+ }
+
+ public function getNbStructureMax(): ?int
+ {
+ return $this->nbStructureMax;
+ }
+
+ public function setNbStructureMax(int $nbStructureMax): static
+ {
+ $this->nbStructureMax = $nbStructureMax;
+
+ return $this;
+ }
+
+ public function getRestrictionConfig(): ?array
+ {
+ return $this->restrictionConfig;
+ }
+
+ public function setRestrictionConfig(?array $restrictionConfig): static
+ {
+ $this->restrictionConfig = $restrictionConfig;
+
+ return $this;
+ }
+
+ public function getNbAlimentaireMax(): ?int
+ {
+ return $this->nbAlimentaireMax;
+ }
+
+ public function setNbAlimentaireMax(?int $nbAlimentaireMax): static
+ {
+ $this->nbAlimentaireMax = $nbAlimentaireMax;
+
+ return $this;
+ }
+
+ public function getNbBarhumsMax(): ?int
+ {
+ return $this->nbBarhumsMax;
+ }
+
+ public function setNbBarhumsMax(?int $nbBarhumsMax): static
+ {
+ $this->nbBarhumsMax = $nbBarhumsMax;
+
+ return $this;
+ }
+}
diff --git a/src/Repository/FormulesRestrictionRepository.php b/src/Repository/FormulesRestrictionRepository.php
new file mode 100644
index 0000000..c7ac51f
--- /dev/null
+++ b/src/Repository/FormulesRestrictionRepository.php
@@ -0,0 +1,43 @@
+
+ */
+class FormulesRestrictionRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, FormulesRestriction::class);
+ }
+
+// /**
+// * @return FormulesRestriction[] Returns an array of FormulesRestriction objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('f')
+// ->andWhere('f.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('f.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?FormulesRestriction
+// {
+// return $this->createQueryBuilder('f')
+// ->andWhere('f.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/templates/dashboard/formules/config-free.twig b/templates/dashboard/formules/config-free.twig
index c0a8b4d..1b4b491 100644
--- a/templates/dashboard/formules/config-free.twig
+++ b/templates/dashboard/formules/config-free.twig
@@ -1 +1,113 @@
-dazdazdazdaz
+
+
+
diff --git a/templates/dashboard/formules/config-pack.twig b/templates/dashboard/formules/config-pack.twig
index 179c625..7773380 100644
--- a/templates/dashboard/formules/config-pack.twig
+++ b/templates/dashboard/formules/config-pack.twig
@@ -77,8 +77,7 @@
-