feat(admin.js): Ajoute CrmEditor pour l'édition WYSIWYG et l'initialise.
🎨 style(templates): Intègre CrmEditor pour la description des produits et formules.
```
This commit is contained in:
Serreau Jovann
2026-01-28 10:30:47 +01:00
parent 6f06cb975e
commit 08334b0298
6 changed files with 211 additions and 71 deletions

View File

@@ -4,6 +4,7 @@ import * as Turbo from "@hotwired/turbo";
import { RepeatLine } from "./libs/RepeatLine.js";
import { DevisManager } from "./libs/DevisManager.js";
import { CrmEditor } from "./libs/CrmEditor.js";
import { initTomSelect } from "./libs/initTomSelect.js";
import { SearchProduct,SearchOptions } from "./libs/SearchProduct.js";
import { SearchProductDevis,SearchOptionsDevis } from "./libs/SearchProductDevis.js";
@@ -99,6 +100,9 @@ function initAdminLayout() {
if (!customElements.get('search-optionsdevis')) {
customElements.define('search-optionsdevis', SearchOptionsDevis, { extends: 'button' });
}
if (!customElements.get('crm-editor')) {
customElements.define('crm-editor', CrmEditor, { extends: 'textarea' });
}
// S
// Sidebar & UI
const sidebar = document.getElementById('sidebar');

121
assets/libs/CrmEditor.js Normal file
View File

@@ -0,0 +1,121 @@
export class CrmEditor extends HTMLTextAreaElement {
connectedCallback() {
this.style.display = 'none';
this.editorContainer = document.createElement('div');
this.editorContainer.className = "w-full rounded-2xl overflow-hidden ring-1 ring-white/20 bg-slate-950/40 backdrop-blur-2xl shadow-2xl transition-all duration-300 focus-within:ring-blue-500/50 my-4";
this.editorContainer.innerHTML = `
<div class="flex items-center gap-1.5 p-3 border-b border-white/10 bg-black/40">
<button type="button" data-cmd="undo" class="w-9 h-9 flex items-center justify-center hover:bg-white/10 rounded-lg transition-all text-white active:scale-90">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path stroke-linecap="round" stroke-linejoin="round" d="M3 10h10a8 8 0 018 8v2M3 10l6 6m-6-6l6-6" /></svg>
</button>
<div class="w-px h-5 bg-white/10 mx-1"></div>
<button type="button" data-cmd="bold" id="btn-bold" class="w-9 h-9 flex items-center justify-center rounded-lg transition-all text-white font-bold hover:bg-white/10">B</button>
<button type="button" data-cmd="underline" id="btn-underline" class="w-9 h-9 flex items-center justify-center rounded-lg transition-all text-white underline hover:bg-white/10">U</button>
<div class="relative w-9 h-9 flex items-center justify-center hover:bg-white/10 rounded-lg transition-all">
<input type="color" id="text-color-input" class="absolute inset-0 w-full h-full opacity-0 cursor-pointer">
<div id="color-preview" class="flex flex-col items-center">
<span class="text-white font-bold text-xs">A</span>
<div class="w-4 h-0.5 bg-red-500 mt-0.5 rounded-full shadow-sm" id="color-bar"></div>
</div>
</div>
<div class="w-px h-5 bg-white/10 mx-1"></div>
<button type="button" data-cmd="justifyLeft" id="btn-left" class="w-9 h-9 flex items-center justify-center rounded-lg transition-all text-white hover:bg-white/10">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M4 6h16M4 12h10M4 18h16" /></svg>
</button>
<button type="button" data-cmd="justifyCenter" id="btn-center" class="w-9 h-9 flex items-center justify-center rounded-lg transition-all text-white hover:bg-white/10">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M4 6h16M7 12h10M4 18h16" /></svg>
</button>
<button type="button" data-cmd="justifyRight" id="btn-right" class="w-9 h-9 flex items-center justify-center rounded-lg transition-all text-white hover:bg-white/10">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24" stroke-width="2"><path d="M4 6h16M10 12h10M4 18h16" /></svg>
</button>
</div>
<div id="wysiwyg-area" contenteditable="true" class="p-6 min-h-[300px] focus:outline-none bg-white text-slate-900 prose prose-slate max-w-none shadow-inner">${this.value || '<p><br></p>'}</div>
<div class="flex items-center justify-between px-4 py-2 border-t border-white/10 bg-black/20 text-[11px] font-medium tracking-wide">
<div id="char-counter" class="flex items-center gap-2">
<span id="char-count" class="text-white bg-white/10 px-2 py-0.5 rounded-full">0 caractères</span>
<span id="seo-warning" class="text-orange-400 opacity-0 transition-opacity italic">Texte insuffisant pour un référencement optimal</span>
</div>
<div class="text-white/40 uppercase tracking-widest font-bold">EsyWysiwyg 1.0</div>
</div>
`;
this.insertAdjacentElement('afterend', this.editorContainer);
this.wysiwyg = this.editorContainer.querySelector('#wysiwyg-area');
this.colorInput = this.editorContainer.querySelector('#text-color-input');
this.colorBar = this.editorContainer.querySelector('#color-bar');
this.initEvents();
this.updateStats();
}
initEvents() {
// Boutons standards
const buttons = this.editorContainer.querySelectorAll('button[data-cmd]');
buttons.forEach(btn => {
btn.onmousedown = (e) => {
e.preventDefault();
document.execCommand(btn.getAttribute('data-cmd'), false, null);
this.syncValue();
this.checkActiveStates();
};
});
// Gestion de la couleur
this.colorInput.oninput = (e) => {
const color = e.target.value;
this.colorBar.style.backgroundColor = color;
document.execCommand('foreColor', false, color);
this.syncValue();
};
this.wysiwyg.addEventListener('keyup', () => {
this.checkActiveStates();
this.updateStats();
});
this.wysiwyg.addEventListener('input', () => {
this.syncValue();
this.updateStats();
});
}
updateStats() {
const text = this.wysiwyg.innerText || "";
const count = text.trim().length;
this.editorContainer.querySelector('#char-count').innerText = `${count} caractère${count > 1 ? 's' : ''}`;
const warning = this.editorContainer.querySelector('#seo-warning');
(count > 0 && count <= 100) ? warning.classList.remove('opacity-0') : warning.classList.add('opacity-0');
}
checkActiveStates() {
const activeStyles = ['bg-white/20', 'ring-1', 'ring-white/30'];
const commands = [
{ id: 'bold', btn: '#btn-bold' },
{ id: 'underline', btn: '#btn-underline' },
{ id: 'justifyLeft', btn: '#btn-left' },
{ id: 'justifyCenter', btn: '#btn-center' },
{ id: 'justifyRight', btn: '#btn-right' }
];
commands.forEach(c => {
const el = this.editorContainer.querySelector(c.btn);
if (el) {
document.queryCommandState(c.id) ? el.classList.add(...activeStyles) : el.classList.remove(...activeStyles);
}
});
}
syncValue() {
this.value = this.wysiwyg.innerHTML;
this.dispatchEvent(new Event('input', { bubbles: true }));
this.dispatchEvent(new Event('change', { bubbles: true }));
}
}

View File

@@ -1,71 +1,4 @@
{# SECTION 03 : FORMULAIRE TARIFICATION (AUTONOME) #}
<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">
liste product inclus in packed
list options inclus in packed
{# Ligne décorative émeraude pour le "Money" #}
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-emerald-500/40 to-transparent"></div>
<div class="flex items-center justify-between mb-8">
<h3 class="text-[10px] font-black text-emerald-400 uppercase tracking-[0.2em] flex items-center">
<span class="w-6 h-6 bg-emerald-500/10 rounded flex items-center justify-center mr-3 font-mono text-emerald-500">03</span>
Grille Tarifaire & Caution
</h3>
{# Badge de rappel du type #}
<span class="text-[9px] font-bold text-slate-500 uppercase px-3 py-1 bg-white/5 rounded-lg border border-white/10 italic">
Tarification dégressive
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{# Prix 1 Jour #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 1 Jour</label>
<div class="relative">
<input type="number" step="0.01" name="price[1j]" value="{{ formule.price1j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Prix 2 Jours #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 2 Jours</label>
<div class="relative">
<input type="number" step="0.01" name="price[2j]" value="{{ formule.price2j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Prix 5 Jours #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 5 Jours</label>
<div class="relative">
<input type="number" step="0.01" name="price[5j]" value="{{ formule.price5j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Caution #}
<div class="space-y-2">
<label class="text-[10px] font-black text-rose-400 uppercase tracking-widest ml-1 italic">Garantie (Caution)</label>
<div class="relative">
<input type="number" step="1" name="price[caution]" value="{{ formule.caution|default('0') }}"
class="w-full bg-rose-500/5 border border-rose-500/20 rounded-xl py-4 pl-5 pr-12 text-rose-500 font-black focus:ring-2 focus:ring-rose-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-rose-500/50">€</span>
</div>
</div>
</div>
{# Bouton de sauvegarde spécifique aux prix #}
<div class="flex justify-end">
<button type="submit" class="group flex items-center space-x-3 px-8 py-4 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl transition-all shadow-lg shadow-emerald-900/20 active:scale-95">
<span class="text-[10px] font-black uppercase tracking-widest">Mettre à jour la tarification</span>
<svg class="w-4 h-4 transform group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</form>

View File

@@ -108,7 +108,7 @@
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Description catalogue</label>
{{ form_widget(form.description, {
'attr': {'class': 'w-full bg-white/5 border border-white/10 rounded-xl py-4 px-5 text-white focus:ring-2 focus:ring-blue-500/50 outline-none transition-all min-h-[150px]'}
'attr': {'is':'crm-editor','class': 'w-full bg-white/5 border border-white/10 rounded-xl py-4 px-5 text-white focus:ring-2 focus:ring-blue-500/50 outline-none transition-all min-h-[150px]'}
}) }}
</div>
@@ -128,6 +128,78 @@
{{ form_end(form) }}
</div>
{# SECTION 03 : FORMULAIRE TARIFICATION (AUTONOME) #}
<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">
{# Ligne décorative émeraude pour le "Money" #}
<div class="absolute top-0 left-0 w-full h-1 bg-gradient-to-r from-transparent via-emerald-500/40 to-transparent"></div>
<div class="flex items-center justify-between mb-8">
<h3 class="text-[10px] font-black text-emerald-400 uppercase tracking-[0.2em] flex items-center">
<span class="w-6 h-6 bg-emerald-500/10 rounded flex items-center justify-center mr-3 font-mono text-emerald-500">03</span>
Grille Tarifaire & Caution
</h3>
{# Badge de rappel du type #}
<span class="text-[9px] font-bold text-slate-500 uppercase px-3 py-1 bg-white/5 rounded-lg border border-white/10 italic">
Tarification dégressive
</span>
</div>
<div class="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
{# Prix 1 Jour #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 1 Jour</label>
<div class="relative">
<input type="number" step="0.01" name="price[1j]" value="{{ formule.price1j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Prix 2 Jours #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 2 Jours</label>
<div class="relative">
<input type="number" step="0.01" name="price[2j]" value="{{ formule.price2j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Prix 5 Jours #}
<div class="space-y-2">
<label class="text-[10px] font-black text-slate-500 uppercase tracking-widest ml-1">Tarif 5 Jours</label>
<div class="relative">
<input type="number" step="0.01" name="price[5j]" value="{{ formule.price5j|default('0.00') }}"
class="w-full bg-white/5 border border-white/10 rounded-xl py-4 pl-5 pr-12 text-white font-bold focus:ring-2 focus:ring-emerald-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-slate-600">€</span>
</div>
</div>
{# Caution #}
<div class="space-y-2">
<label class="text-[10px] font-black text-rose-400 uppercase tracking-widest ml-1 italic">Garantie (Caution)</label>
<div class="relative">
<input type="number" step="1" name="price[caution]" value="{{ formule.caution|default('0') }}"
class="w-full bg-rose-500/5 border border-rose-500/20 rounded-xl py-4 pl-5 pr-12 text-rose-500 font-black focus:ring-2 focus:ring-rose-500/50 outline-none transition-all">
<span class="absolute right-5 top-1/2 -translate-y-1/2 text-[10px] font-black text-rose-500/50">€</span>
</div>
</div>
</div>
{# Bouton de sauvegarde spécifique aux prix #}
<div class="flex justify-end">
<button type="submit" class="group flex items-center space-x-3 px-8 py-4 bg-emerald-600 hover:bg-emerald-500 text-white rounded-2xl transition-all shadow-lg shadow-emerald-900/20 active:scale-95">
<span class="text-[10px] font-black uppercase tracking-widest">Mettre à jour la tarification</span>
<svg class="w-4 h-4 transform group-hover:rotate-12 transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</button>
</div>
</form>
{% include 'dashboard/formules/config-'~formule.type~".twig" %}
</div>

View File

@@ -99,6 +99,7 @@
{{ form_label(form.description, 'Description détaillée', {'label_attr': {'class': 'text-[10px] font-black text-slate-500 uppercase tracking-[0.2em] ml-1 mb-2 block'}}) }}
{{ form_widget(form.description, {
'attr': {
'is':'crm-editor',
'class': 'w-full bg-slate-900/50 border-white/5 rounded-2xl text-white focus:ring-blue-500/20 focus:border-blue-500 transition-all py-3.5 px-5 min-h-[150px]',
'placeholder': 'Décrivez les dimensions, la capacité, les points forts...'
}

View File

@@ -147,7 +147,16 @@
{# --- DESCRIPTION --- #}
<div class="prose prose-slate prose-lg max-w-none mb-12 text-slate-600 leading-relaxed text-center md:text-left">
{{ product.description|nl2br|raw }}
{% set desc = product.description %}
{# On vérifie si la chaîne contient l'un des marqueurs HTML de ton éditeur #}
{% if '<p' in desc or '<div' in desc or '<span' in desc or '<br' in desc %}
{# C'est du HTML (WYSIWYG) #}
{{ desc|raw }}
{% else %}
{# C'est du texte brut (Ancien système) #}
{{ desc|nl2br }}
{% endif %}
</div>
{# --- DIMENSIONS (Version Responsive) --- #}