```
✨ feat(Formules.php): Ajoute relation OneToOne avec FormulesRestriction. ✨ feat(Dashboard/FormulesController.php): Gère restrictions formules et formulaire. 🎨 refactor(template/formules): Améliore interface configuration restriction formule. 🐛 fix(assets/RepeatLine.js): Corrige réinitialisation TomSelect et selects "Type". ✨ feat(assets/initTomSelect.js): Gère cache options et init TomSelect. ```
This commit is contained in:
@@ -42,39 +42,41 @@ export class RepeatLine extends HTMLDivElement {
|
|||||||
addRow() {
|
addRow() {
|
||||||
if (!this.rowHTML || this.$refs.rows.children.length >= this.$props.maxRows) return;
|
if (!this.rowHTML || this.$refs.rows.children.length >= this.$props.maxRows) return;
|
||||||
|
|
||||||
// Création de la nouvelle ligne
|
|
||||||
let newRow = this.createFromHTML(this.rowHTML);
|
let newRow = this.createFromHTML(this.rowHTML);
|
||||||
newRow.removeAttribute('id');
|
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 => {
|
newRow.querySelectorAll('select').forEach(select => {
|
||||||
// Supprimer les classes et éléments injectés par TomSelect si présents dans le template
|
// 1. Si c'est un select TomSelect (sans attribut 'is')
|
||||||
select.classList.remove('tomselect', 'ts-hidden-visually');
|
if (!select.hasAttribute('ds')) {
|
||||||
select.innerHTML = '<option value="">Sélectionner...</option>';
|
select.classList.remove('tomselect', 'ts-hidden-visually');
|
||||||
// Supprimer le wrapper TomSelect s'il a été cloné par erreur
|
select.innerHTML = '<option value="">Sélectionner...</option>';
|
||||||
const wrapper = select.nextElementSibling;
|
|
||||||
if (wrapper && wrapper.classList.contains('ts-wrapper')) {
|
const wrapper = select.nextElementSibling;
|
||||||
wrapper.remove();
|
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.setUpRow(newRow);
|
||||||
this.$refs.rows.appendChild(newRow);
|
this.$refs.rows.appendChild(newRow);
|
||||||
|
|
||||||
// Réinitialisation des valeurs
|
// Réinitialisation des autres champs
|
||||||
newRow.querySelectorAll('input,textarea,select').forEach(el => {
|
newRow.querySelectorAll('input,textarea').forEach(el => {
|
||||||
el.value = "";
|
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);
|
initTomSelect(newRow);
|
||||||
|
|
||||||
this.updateFieldNames();
|
this.updateFieldNames();
|
||||||
this.updateAddButton();
|
this.updateAddButton();
|
||||||
|
|
||||||
// Focus sur le premier élément de la nouvelle ligne
|
|
||||||
const firstInput = newRow.querySelector('input,textarea,select');
|
const firstInput = newRow.querySelector('input,textarea,select');
|
||||||
if (firstInput) firstInput.focus();
|
if (firstInput) firstInput.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
// Cache pour éviter les requêtes HTTP répétitives
|
|
||||||
import TomSelect from "tom-select";
|
import TomSelect from "tom-select";
|
||||||
|
|
||||||
|
// Cache séparé pour éviter les conflits entre produits et options
|
||||||
let productCache = null;
|
let productCache = null;
|
||||||
|
let optionsCache = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialise TomSelect sur un élément ou un groupe d'éléments
|
* Initialise TomSelect sur un élément ou un groupe d'éléments
|
||||||
*/
|
*/
|
||||||
export function initTomSelect(parent = document) {
|
export function initTomSelect(parent = document) {
|
||||||
parent.querySelectorAll('select').forEach((el) => {
|
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 ---
|
// --- CONFIGURATION PRODUITS ---
|
||||||
if (el.getAttribute('data-load') === "product") {
|
if (el.getAttribute('data-load') === "product") {
|
||||||
@@ -19,34 +22,23 @@ export function initTomSelect(parent = document) {
|
|||||||
searchField: 'name',
|
searchField: 'name',
|
||||||
options: data,
|
options: data,
|
||||||
maxOptions: null,
|
maxOptions: null,
|
||||||
// LORSQU'ON SÉLECTIONNE UN PRODUIT
|
|
||||||
// Dans admin.js, section onChange de TomSelect :
|
|
||||||
onChange: (id) => {
|
onChange: (id) => {
|
||||||
if (!id) return;
|
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 product = data.find(p => String(p.id) === String(id));
|
||||||
|
|
||||||
if (product) {
|
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');
|
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
|
|
||||||
// Ciblage précis des inputs
|
|
||||||
const priceInput = row.querySelector('input[name*="[price_ht]"]');
|
const priceInput = row.querySelector('input[name*="[price_ht]"]');
|
||||||
const priceSupInput = row.querySelector('input[name*="[price_sup_ht]"]');
|
const priceSupInput = row.querySelector('input[name*="[price_sup_ht]"]');
|
||||||
|
|
||||||
if (priceInput) {
|
if (priceInput) {
|
||||||
priceInput.value = product.price1day;
|
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('input', { bubbles: true }));
|
||||||
priceInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (priceSupInput) {
|
if (priceSupInput) {
|
||||||
priceSupInput.value = product.priceSup;
|
priceSupInput.value = product.priceSup;
|
||||||
priceSupInput.dispatchEvent(new Event('input', { bubbles: true }));
|
priceSupInput.dispatchEvent(new Event('input', { bubbles: true }));
|
||||||
priceSupInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -59,16 +51,11 @@ export function initTomSelect(parent = document) {
|
|||||||
<div class="text-[10px] text-slate-400">J1: ${escape(data.price1day)}€ | Sup: ${escape(data.priceSup)}€</div>
|
<div class="text-[10px] text-slate-400">J1: ${escape(data.price1day)}€ | Sup: ${escape(data.priceSup)}€</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
item: (data, escape) => `
|
item: (data, escape) => `<div class="text-blue-400 font-bold flex items-center gap-2"><span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>${escape(data.name)}</div>`
|
||||||
<div class="text-blue-400 font-bold flex items-center gap-2">
|
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
|
|
||||||
${escape(data.name)}
|
|
||||||
</div>`
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utilisation du cache ou fetch
|
|
||||||
if (productCache) {
|
if (productCache) {
|
||||||
setupSelect(productCache);
|
setupSelect(productCache);
|
||||||
} else {
|
} else {
|
||||||
@@ -80,6 +67,7 @@ export function initTomSelect(parent = document) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// --- CONFIGURATION OPTIONS ---
|
||||||
else if (el.getAttribute('data-load') === "options") {
|
else if (el.getAttribute('data-load') === "options") {
|
||||||
const setupSelect = (data) => {
|
const setupSelect = (data) => {
|
||||||
new TomSelect(el, {
|
new TomSelect(el, {
|
||||||
@@ -88,12 +76,8 @@ export function initTomSelect(parent = document) {
|
|||||||
searchField: 'name',
|
searchField: 'name',
|
||||||
options: data,
|
options: data,
|
||||||
maxOptions: null,
|
maxOptions: null,
|
||||||
// LORSQU'ON SÉLECTIONNE UN PRODUIT
|
|
||||||
// Dans admin.js, section onChange de TomSelect :
|
|
||||||
onChange: (id) => {
|
onChange: (id) => {
|
||||||
if (!id) return;
|
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 product = data.find(p => String(p.id) === String(id));
|
||||||
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
|
const row = el.closest('.form-repeater__row') || el.closest('fieldset');
|
||||||
if (!row) return;
|
if (!row) return;
|
||||||
@@ -101,11 +85,8 @@ export function initTomSelect(parent = document) {
|
|||||||
|
|
||||||
if (priceInput) {
|
if (priceInput) {
|
||||||
priceInput.value = product.price;
|
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('input', { bubbles: true }));
|
||||||
priceInput.dispatchEvent(new Event('change', { bubbles: true }));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
},
|
},
|
||||||
render: {
|
render: {
|
||||||
option: (data, escape) => `
|
option: (data, escape) => `
|
||||||
@@ -116,28 +97,23 @@ export function initTomSelect(parent = document) {
|
|||||||
<div class="text-[10px] text-slate-400">${escape(data.price)}€</div>
|
<div class="text-[10px] text-slate-400">${escape(data.price)}€</div>
|
||||||
</div>
|
</div>
|
||||||
</div>`,
|
</div>`,
|
||||||
item: (data, escape) => `
|
item: (data, escape) => `<div class="text-blue-400 font-bold flex items-center gap-2"><span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>${escape(data.name)}</div>`
|
||||||
<div class="text-blue-400 font-bold flex items-center gap-2">
|
|
||||||
<span class="w-1.5 h-1.5 rounded-full bg-blue-500"></span>
|
|
||||||
${escape(data.name)}
|
|
||||||
</div>`
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
// Utilisation du cache ou fetch
|
if (optionsCache) {
|
||||||
if (productCache) {
|
setupSelect(optionsCache);
|
||||||
setupSelect(productCache);
|
|
||||||
} else {
|
} else {
|
||||||
fetch("/crm/options/json")
|
fetch("/crm/options/json")
|
||||||
.then(r => r.json())
|
.then(r => r.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
productCache = data;
|
optionsCache = data;
|
||||||
setupSelect(data);
|
setupSelect(data);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// --- AUTRES SELECTS ---
|
// --- AUTRES SELECTS STANDARDS ---
|
||||||
else {
|
else {
|
||||||
new TomSelect(el, {
|
new TomSelect(el, {
|
||||||
controlInput: null,
|
controlInput: null,
|
||||||
|
|||||||
36
migrations/Version20260128142222.php
Normal file
36
migrations/Version20260128142222.php
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
<?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 Version20260128142222 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 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
34
migrations/Version20260128144635.php
Normal file
34
migrations/Version20260128144635.php
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
<?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 Version20260128144635 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('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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,29 +8,45 @@ use Symfony\Component\Console\Input\InputInterface;
|
|||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||||
use Symfony\Component\Process\Process;
|
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\Client;
|
||||||
use GeminiAPI\Resources\Parts\TextPart;
|
use GeminiAPI\Resources\Parts\TextPart;
|
||||||
|
|
||||||
#[AsCommand(
|
#[AsCommand(
|
||||||
name: 'app:git-log-update',
|
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
|
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
|
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||||
{
|
{
|
||||||
$io = new SymfonyStyle($input, $output);
|
$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
|
// 1. Récupération des infos Git
|
||||||
|
// On utilise $this->projectDir pour le safe.directory
|
||||||
$gitCmd = sprintf(
|
$gitCmd = sprintf(
|
||||||
'git config --global --add safe.directory %s && git log -1 --format="%%s|%%ci|%%h"',
|
'git config --global --add safe.directory %s && git log -1 --format="%%s|%%ci|%%h"',
|
||||||
$projectDir
|
$this->projectDir
|
||||||
);
|
);
|
||||||
|
|
||||||
$process = Process::fromShellCommandline($gitCmd);
|
$process = Process::fromShellCommandline($gitCmd);
|
||||||
|
$process->setWorkingDirectory($this->projectDir); // On force le dossier de travail
|
||||||
$process->run();
|
$process->run();
|
||||||
|
|
||||||
if (!$process->isSuccessful()) {
|
if (!$process->isSuccessful()) {
|
||||||
@@ -46,60 +62,41 @@ class GitSyncLogCommand extends Command
|
|||||||
// 2. Détermination du TYPE (feature, fix, optimise, new)
|
// 2. Détermination du TYPE (feature, fix, optimise, new)
|
||||||
$type = 'new';
|
$type = 'new';
|
||||||
$lowerMsg = strtolower($rawMessage);
|
$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)) {
|
// 3. Vérification anti-doublon
|
||||||
$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)
|
|
||||||
$data = [];
|
$data = [];
|
||||||
if (file_exists($filePath)) {
|
if (file_exists($filePath)) {
|
||||||
$data = json_decode(file_get_contents($filePath), true) ?? [];
|
$data = json_decode(file_get_contents($filePath), true) ?? [];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!empty($data) && $data[0]['hash'] === $commitHash) {
|
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;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Appel IA Gemini-3-Pro-Preview pour la reformulation
|
// 4. Appel IA Gemini
|
||||||
$friendlyMessage = $rawMessage;
|
$friendlyMessage = $rawMessage;
|
||||||
try {
|
try {
|
||||||
// Ta clé API
|
|
||||||
$client = new Client("AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg");
|
$client = new Client("AIzaSyDTPJERlUC47bcvhZU51Lwpqb1uxXS8SIg");
|
||||||
$model = 'gemini-3-pro-preview';
|
$model = 'gemini-3-pro-preview';
|
||||||
|
|
||||||
$prompt = "Tu es un expert en communication web pour Ludik Event. Ta mission est de transformer
|
$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.
|
un message de commit technique en une note de mise à jour élégante pour ton client.
|
||||||
|
|
||||||
MESSAGE TECHNIQUE : \"$rawMessage\"
|
MESSAGE TECHNIQUE : \"$rawMessage\"
|
||||||
|
DIRECTIVES : Court, positif, pas de 'Voici la phrase', uniquement le résultat final.";
|
||||||
|
|
||||||
DIRECTIVES :
|
$response = $client->withV1BetaVersion()->generativeModel($model)->generateContent(new TextPart($prompt));
|
||||||
1. Reformule pour un propriétaire de site non-technique.
|
if ($response->text()) {
|
||||||
2. Sois court, positif et rassurant.
|
$friendlyMessage = trim($response->text());
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
} catch (\Exception $e) {
|
} 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 = [
|
$newEntry = [
|
||||||
'type' => $type,
|
'type' => $type,
|
||||||
'message' => $friendlyMessage,
|
'message' => $friendlyMessage,
|
||||||
@@ -107,18 +104,29 @@ class GitSyncLogCommand extends Command
|
|||||||
'hash' => $commitHash
|
'hash' => $commitHash
|
||||||
];
|
];
|
||||||
|
|
||||||
// 6. Sauvegarde et rotation (5 max)
|
|
||||||
array_unshift($data, $newEntry);
|
array_unshift($data, $newEntry);
|
||||||
$data = array_slice($data, 0, 6);
|
$data = array_slice($data, 0, 6);
|
||||||
|
|
||||||
if (!is_dir('var')) {
|
$varDir = $this->projectDir . '/var';
|
||||||
mkdir('var', 0777, true);
|
if (!is_dir($varDir)) {
|
||||||
|
mkdir($varDir, 0777, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
file_put_contents($filePath, json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE));
|
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;
|
return Command::SUCCESS;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ namespace App\Controller\Dashboard;
|
|||||||
use App\Entity\Formules;
|
use App\Entity\Formules;
|
||||||
use App\Entity\FormulesOptionsInclus;
|
use App\Entity\FormulesOptionsInclus;
|
||||||
use App\Entity\FormulesProductInclus;
|
use App\Entity\FormulesProductInclus;
|
||||||
|
use App\Entity\FormulesRestriction;
|
||||||
use App\Entity\Options;
|
use App\Entity\Options;
|
||||||
use App\Entity\Product;
|
use App\Entity\Product;
|
||||||
use App\Entity\ProductDoc;
|
use App\Entity\ProductDoc;
|
||||||
@@ -59,6 +60,8 @@ class FormulesController extends AbstractController
|
|||||||
if ($this->isCsrfTokenValid('delete' . $formules->getId(), $request->query->get('_token'))) {
|
if ($this->isCsrfTokenValid('delete' . $formules->getId(), $request->query->get('_token'))) {
|
||||||
$nomFormule = $formules->getName();
|
$nomFormule = $formules->getName();
|
||||||
|
|
||||||
|
if($formules->getFormulesRestriction() instanceof FormulesRestriction)
|
||||||
|
$entityManager->remove($formules->getFormulesRestriction());
|
||||||
// 3. Suppression
|
// 3. Suppression
|
||||||
foreach ($formules->getFormulesProductIncluses() as $formulesProductInclus)
|
foreach ($formules->getFormulesProductIncluses() as $formulesProductInclus)
|
||||||
$entityManager->remove($formulesProductInclus);
|
$entityManager->remove($formulesProductInclus);
|
||||||
@@ -88,6 +91,15 @@ class FormulesController extends AbstractController
|
|||||||
$form->handleRequest($request);
|
$form->handleRequest($request);
|
||||||
if ($form->isSubmitted() && $form->isValid()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$entityManager->persist($formules);
|
$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();
|
$entityManager->flush();
|
||||||
$appLogger->record('CREATED', "Création de la formule : " . $formules->getName());
|
$appLogger->record('CREATED', "Création de la formule : " . $formules->getName());
|
||||||
$this->addFlash("success", "La formule a été créée avec succès.");
|
$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()]);
|
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[])
|
// 2. GESTION DES PRIX (Formulaire Manuel price[])
|
||||||
// On vérifie si le tableau 'price' existe dans la requête POST
|
// 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 = $this->createForm(FormulesType::class, $formules);
|
||||||
$form->handleRequest($request);
|
$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()) {
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
$entityManager->persist($formules);
|
$entityManager->persist($formules);
|
||||||
|
// 2. Sauvegarde globale
|
||||||
$entityManager->flush();
|
$entityManager->flush();
|
||||||
|
|
||||||
$appLogger->record('UPDATE', "Modification de la formule (infos) : " . $formules->getName());
|
$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]['product'] = $fc->getName();
|
||||||
$options[$key]['id'] = $fc->getId();
|
$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', [
|
return $this->render('dashboard/formules/view.twig', [
|
||||||
'formule' => $formules,
|
'formule' => $formules,
|
||||||
'form' => $form->createView(),
|
'form' => $form->createView(),
|
||||||
'type' => $formules->getType(),
|
'type' => $formules->getType(),
|
||||||
'lines' => $lines,
|
'lines' => $lines,
|
||||||
'option' => $options,
|
'option' => $options,
|
||||||
|
'restriction' => $restriction,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -69,6 +69,9 @@ class Formules
|
|||||||
#[ORM\OneToMany(targetEntity: FormulesOptionsInclus::class, mappedBy: 'formule')]
|
#[ORM\OneToMany(targetEntity: FormulesOptionsInclus::class, mappedBy: 'formule')]
|
||||||
private Collection $formulesOptionsIncluses;
|
private Collection $formulesOptionsIncluses;
|
||||||
|
|
||||||
|
#[ORM\OneToOne(mappedBy: 'formule', cascade: ['persist', 'remove'])]
|
||||||
|
private ?FormulesRestriction $formulesRestriction = null;
|
||||||
|
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->formulesProductIncluses = new ArrayCollection();
|
$this->formulesProductIncluses = new ArrayCollection();
|
||||||
@@ -307,4 +310,26 @@ class Formules
|
|||||||
|
|
||||||
return $this;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
96
src/Entity/FormulesRestriction.php
Normal file
96
src/Entity/FormulesRestriction.php
Normal file
@@ -0,0 +1,96 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Entity;
|
||||||
|
|
||||||
|
use App\Repository\FormulesRestrictionRepository;
|
||||||
|
use Doctrine\DBAL\Types\Types;
|
||||||
|
use Doctrine\ORM\Mapping as ORM;
|
||||||
|
|
||||||
|
#[ORM\Entity(repositoryClass: FormulesRestrictionRepository::class)]
|
||||||
|
class FormulesRestriction
|
||||||
|
{
|
||||||
|
#[ORM\Id]
|
||||||
|
#[ORM\GeneratedValue]
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $id = null;
|
||||||
|
|
||||||
|
#[ORM\OneToOne(inversedBy: 'formulesRestriction', cascade: ['persist', 'remove'])]
|
||||||
|
private ?Formules $formule = null;
|
||||||
|
|
||||||
|
#[ORM\Column]
|
||||||
|
private ?int $nbStructureMax = null;
|
||||||
|
|
||||||
|
#[ORM\Column(type: Types::ARRAY, nullable: true)]
|
||||||
|
private ?array $restrictionConfig = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?int $nbAlimentaireMax = null;
|
||||||
|
|
||||||
|
#[ORM\Column(nullable: true)]
|
||||||
|
private ?int $nbBarhumsMax = null;
|
||||||
|
|
||||||
|
public function getId(): ?int
|
||||||
|
{
|
||||||
|
return $this->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;
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Repository/FormulesRestrictionRepository.php
Normal file
43
src/Repository/FormulesRestrictionRepository.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Repository;
|
||||||
|
|
||||||
|
use App\Entity\FormulesRestriction;
|
||||||
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||||
|
use Doctrine\Persistence\ManagerRegistry;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends ServiceEntityRepository<FormulesRestriction>
|
||||||
|
*/
|
||||||
|
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()
|
||||||
|
// ;
|
||||||
|
// }
|
||||||
|
}
|
||||||
@@ -1 +1,113 @@
|
|||||||
dazdazdazdaz
|
<form action="{{ path('app_crm_formules_view', {id: formule.id}) }}" method="POST" class="w-full backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-10 shadow-2xl relative overflow-hidden mt-6">
|
||||||
|
<div class="absolute -top-24 -right-24 w-48 h-48 bg-blue-500/10 rounded-full blur-3xl pointer-events-none"></div>
|
||||||
|
|
||||||
|
<div class="relative z-10 flex flex-col md:flex-row md:items-start gap-8">
|
||||||
|
{# STRUCTURES #}
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="nbStructureMax" class="block text-lg font-medium text-white/80 mb-3 ml-2">Nombre max structures</label>
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-5 flex items-center pointer-events-none text-blue-400/60 group-focus-within:text-blue-400 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="number" name="nbStructureMax" id="nbStructureMax" value="{{ formule.formulesRestriction.nbStructureMax|default(0) }}" min="0" class="block w-full pl-14 pr-6 py-4 bg-white/10 border border-white/10 rounded-2xl text-white text-xl font-semibold focus:ring-2 focus:ring-blue-500/50 outline-none transition-all">
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-white/40 italic leading-relaxed">Limite totale de structures gonflables ou rigides.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# ALIMENTAIRE #}
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="nbAlimentaireMax" class="block text-lg font-medium text-white/80 mb-3 ml-2">Nombre max alimentaire</label>
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-5 flex items-center pointer-events-none text-orange-400/60 group-focus-within:text-orange-400 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18 18.246 18.477 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="number" name="nbAlimentaireMax" id="nbAlimentaireMax" value="{{ formule.formulesRestriction.nbAlimentaireMax|default(0) }}" min="0" class="block w-full pl-14 pr-6 py-4 bg-white/10 border border-white/10 rounded-2xl text-white text-xl font-semibold focus:ring-2 focus:ring-orange-500/50 outline-none transition-all">
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-white/40 italic leading-relaxed">Machines à barbe à papa, popcorn, buffets, etc.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# BARNUMS #}
|
||||||
|
<div class="flex-1">
|
||||||
|
<label for="nbBarhumsMax" class="block text-lg font-medium text-white/80 mb-3 ml-2">Nombre max barnums</label>
|
||||||
|
<div class="relative group">
|
||||||
|
<div class="absolute inset-y-0 left-0 pl-5 flex items-center pointer-events-none text-purple-400/60 group-focus-within:text-purple-400 transition-colors">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<input type="number" name="nbBarhumsMax" id="nbBarhumsMax" value="{{ formule.formulesRestriction.nbBarhumsMax|default(0) }}" min="0" class="block w-full pl-14 pr-6 py-4 bg-white/10 border border-white/10 rounded-2xl text-white text-xl font-semibold focus:ring-2 focus:ring-purple-500/50 outline-none transition-all">
|
||||||
|
</div>
|
||||||
|
<p class="mt-4 text-sm text-white/40 italic leading-relaxed">Tentes de réception et protections extérieures.</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center self-center md:self-auto pt-2">
|
||||||
|
<button type="submit" class="px-8 py-4 bg-blue-600 hover:bg-blue-500 text-white font-bold rounded-2xl transition-all shadow-lg active:scale-95">
|
||||||
|
Enregistrer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form action="{{ path('app_crm_formules_view', {id: formule.id}) }}" method="POST" class="w-full backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl relative overflow-hidden mt-6">
|
||||||
|
<div class="w-full form-repeater" data-component="repeater" is="repeat-line">
|
||||||
|
<div class="flex items-center justify-between mb-6 px-4">
|
||||||
|
<h4 class="text-sm font-black text-white uppercase tracking-widest">Détail des produits autorisés</h4>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ol class="form-repeater__rows space-y-4" data-ref="rows">
|
||||||
|
{% for key,line in restriction %}
|
||||||
|
<li class="form-repeater__row group animate-in slide-in-from-right-5 duration-300">
|
||||||
|
<fieldset class="backdrop-blur-md bg-white/5 border border-white/10 rounded-[2.5rem] p-6 hover:border-blue-500/30 transition-all shadow-xl">
|
||||||
|
<div class="grid grid-cols-1 lg:grid-cols-12 gap-5 items-end">
|
||||||
|
|
||||||
|
{# 1. PRODUIT #}
|
||||||
|
<div class="lg:col-span-7">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block">Produit / Prestation</label>
|
||||||
|
<div class="relative flex items-center">
|
||||||
|
<input readonly type="text" name="rest[{{ key }}][product]" value="{{ line.product }}" required class="w-full bg-slate-950/50 border border-white/10 rounded-2xl text-white focus:border-purple-500 transition-all py-3 pl-5 pr-24 text-sm outline-none">
|
||||||
|
<div class="absolute right-2 flex items-center gap-1.5">
|
||||||
|
<button is="search-productformule" type="button" class="p-2 bg-blue-500/10 hover:bg-blue-500 text-blue-400 hover:text-white rounded-xl transition-all"><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="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"/></svg></button>
|
||||||
|
<button is="search-optionsformule" type="button" class="p-2 bg-purple-500/10 hover:bg-purple-500 text-purple-400 hover:text-white rounded-xl transition-all"><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="M8 16l2.879-2.879m0 0a3 3 0 104.243-4.242 3 3 0 00-4.243 4.242zM21 21l-6-6" /></svg></button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 2. TYPE #}
|
||||||
|
<div class="lg:col-span-4">
|
||||||
|
<label class="text-[10px] font-black text-slate-400 uppercase tracking-widest ml-1 mb-2 block">Catégorie de restriction</label>
|
||||||
|
<select ds required name="rest[{{ key }}][type]" class="w-full bg-slate-950/50 border border-white/10 rounded-2xl text-white focus:border-blue-500 transition-all py-3 px-4 text-sm outline-none appearance-none cursor-pointer">
|
||||||
|
<option {% if line.type =="structure" %}selected{% endif%} value="structure">Structure</option>
|
||||||
|
<option {% if line.type =="alimentaire" %}selected{% endif%} value="alimentaire">Alimentaire</option>
|
||||||
|
<option {% if line.type =="barhnums" %}selected{% endif%} value="barhnums">Barnums</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# 3. SUPPRIMER #}
|
||||||
|
<div class="lg:col-span-1 flex justify-center">
|
||||||
|
<button type="button" data-ref="removeButton" class="p-3 bg-red-500/10 hover:bg-red-500 text-red-500 hover:text-white rounded-xl transition-all">
|
||||||
|
<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"/></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</fieldset>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
|
||||||
|
<div class="mt-6 px-4 space-y-4">
|
||||||
|
<button type="button" data-ref="addButton" class="w-full py-4 border-2 border-dashed border-white/10 hover:border-purple-500/50 bg-white/5 hover:bg-purple-500/10 rounded-3xl text-purple-400 text-[10px] font-black uppercase tracking-[0.4em] transition-all flex items-center justify-center space-x-3 group">
|
||||||
|
<span class="text-xl group-hover:rotate-90 transition-transform">+</span>
|
||||||
|
<span>Ajouter une prestation</span>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit" class="group w-full py-5 bg-blue-600 hover:bg-blue-500 text-white font-black text-[11px] uppercase tracking-[0.3em] rounded-2xl shadow-xl transition-all flex items-center justify-center">
|
||||||
|
<span>Sauvegarder les modifications</span>
|
||||||
|
<svg class="w-4 h-4 ml-3 transform group-hover:translate-x-1 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7l5 5m0 0l-5 5m5-5H6" /></svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|||||||
@@ -77,8 +77,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<form action="{{ path('app_crm_formules_view', {id: formule.id}) }}" method="POST"
|
<form action="{{ path('app_crm_formules_view', {id: formule.id}) }}" method="POST" class="w-full backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl relative overflow-hidden mt-6">
|
||||||
class="w-full backdrop-blur-2xl bg-white/5 border border-white/10 rounded-[2.5rem] p-8 shadow-2xl relative overflow-hidden mt-6">
|
|
||||||
<div class="w-full form-repeater" data-component="options" is="repeat-line">
|
<div class="w-full form-repeater" data-component="options" is="repeat-line">
|
||||||
<div class="flex items-center justify-between mb-6 px-4">
|
<div class="flex items-center justify-between mb-6 px-4">
|
||||||
<h4 class="text-sm font-black text-white uppercase tracking-widest">Détail des options inclus</h4>
|
<h4 class="text-sm font-black text-white uppercase tracking-widest">Détail des options inclus</h4>
|
||||||
|
|||||||
Reference in New Issue
Block a user