feat: comptabilite + prestataires + rapport financier + stats dynamiques

Comptabilite (Super Admin) :
- ComptabiliteController avec 7 exports CSV/JSON compatibles SAGE
  (journal ventes, grand livre, FEC, balance agee, reglements,
  commissions Stripe 1.5%+0.25E, couts services)
- Export PDF via ComptaPdf (FPDF) avec bloc legal pre-rempli,
  tableau pagine, champ signature DocuSeal
- Signature electronique DocuSeal + callback + envoi email signe
  avec template dedie (compta_export_signed.html.twig)
- Rapport financier public (RapportFinancierPdf) : recettes par
  service, depenses (Stripe, infra, prestataires), bilan excedent/deficit
- Codes comptables clients EC-XXXX (plus de 411xxx)

Prestataires (Super Admin) :
- Entite Prestataire (raisonSociale, siret, email, phone, adresse)
- Entite FacturePrestataire (numFacture, montantHt, montantTtc,
  year, month, isPaid, PDF via Vich)
- CRUD complet avec recherche SIRET via proxy API data.gouv.fr
- Commande cron app:reminder:factures-prestataire (5 du mois)
- Factures prestataires integrees dans export couts services
- Sidebar Super Admin : entree Prestataires + Comptabilite

Stats (/admin/stats) :
- Cout prestataire dynamique depuis FacturePrestataire
- Fusion Infra + Prestataire en "Cout de fonctionnement"
- Commission Stripe corrigee (1.5% + 0.25E par transaction)

Divers :
- DocuSealService::sendComptaForSignature() + getApi()
- Customer::generateCodeComptable() format EC-XXXX-XXXXX
- Protection double prefixe EC- a la creation client
- Bouton regenerer PDF cache quand advert state=accepted
- Modals sans script inline (data-modal-open/close dans app.js)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Serreau Jovann
2026-04-07 23:39:31 +02:00
parent 95d33a9a6d
commit 8b35e2b6d2
215 changed files with 11539 additions and 1402 deletions

View File

@@ -3,8 +3,8 @@ import { initEntrepriseSearch } from "./modules/entreprise-search.js"
// Membre / Super Admin : mutuellement exclusif
document.addEventListener('DOMContentLoaded', () => {
const memberCheckbox = document.querySelector('input[value="siteconseil_member"]');
const adminCheckbox = document.querySelector('input[value="siteconseil_admin"]');
const memberCheckbox = document.querySelector('input[value="gp_member"]');
const adminCheckbox = document.querySelector('input[value="superadmin"]');
const otherGroupCheckboxes = () =>
[...document.querySelectorAll('input[name="groups[]"]')].filter(cb => cb !== memberCheckbox);
@@ -35,8 +35,8 @@ document.addEventListener('DOMContentLoaded', () => {
confirmModal.className = 'hidden fixed inset-0 z-[100] flex items-center justify-center';
confirmModal.innerHTML = `
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" id="confirm-overlay"></div>
<div class="relative glass-heavy w-full max-w-md mx-4 overflow-hidden" style="border-radius: 16px;">
<div class="glass-dark text-white px-6 py-4 flex items-center gap-3" style="border-radius: 16px 16px 0 0;">
<div class="relative glass-heavy w-full max-w-md mx-4 overflow-hidden">
<div class="glass-dark text-white px-6 py-4 flex items-center gap-3">
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-red-400" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
@@ -46,7 +46,7 @@ document.addEventListener('DOMContentLoaded', () => {
<p id="confirm-message" class="text-sm font-medium text-gray-700 leading-relaxed"></p>
<div class="flex justify-end gap-3 mt-6">
<button type="button" id="confirm-cancel" class="px-5 py-2 glass font-bold uppercase text-xs tracking-wider text-gray-700 hover:bg-gray-900 hover:text-white transition-all">Annuler</button>
<button type="button" id="confirm-ok" class="px-5 py-2 bg-red-600 text-white font-bold uppercase text-xs tracking-wider hover:bg-red-700 transition-all" style="border-radius: 6px;">Confirmer</button>
<button type="button" id="confirm-ok" class="px-5 py-2 bg-red-600 text-white font-bold uppercase text-xs tracking-wider hover:bg-red-700 transition-all">Confirmer</button>
</div>
</div>
</div>`;
@@ -274,9 +274,13 @@ document.addEventListener('DOMContentLoaded', () => {
});
}
// ──────── Stripe Payment Modal ────────
initStripePayment();
// ──────── Tab search devis / avis ────────
initTabSearch('search-devis', 'search-devis-results');
initTabSearch('search-adverts', 'search-adverts-results');
initTabSearch('search-factures', 'search-factures-results');
// ──────── Devis lines repeater + drag & drop ────────
initDevisLines();
@@ -287,8 +291,224 @@ document.addEventListener('DOMContentLoaded', () => {
if (refuseBtn && refuseForm) {
refuseBtn.addEventListener('click', () => refuseForm.classList.toggle('hidden'));
}
// ──────── Modal open/close generique (data-modal-open / data-modal-close) ────────
document.querySelectorAll('[data-modal-open]').forEach(btn => {
btn.addEventListener('click', () => {
const modal = document.getElementById(btn.dataset.modalOpen);
if (modal) modal.classList.remove('hidden');
});
});
document.querySelectorAll('[data-modal-close]').forEach(btn => {
btn.addEventListener('click', () => {
const modal = document.getElementById(btn.dataset.modalClose);
if (modal) modal.classList.add('hidden');
});
});
// ──────── Prestataire : recherche SIRET via API entreprise.data.gouv.fr ────────
const siretSearchBtn = document.getElementById('siret-search-btn');
const siretInput = document.getElementById('siret-search-input');
const siretResults = document.getElementById('siret-search-results');
if (siretSearchBtn && siretInput && siretResults) {
siretSearchBtn.addEventListener('click', () => {
const q = siretInput.value.trim();
if (q.length < 3) { siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Saisissez au moins 3 caracteres.</p>'; siretResults.classList.remove('hidden'); return; }
siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Recherche...</p>';
siretResults.classList.remove('hidden');
fetch('/admin/prestataires/entreprise-search?q=' + encodeURIComponent(q))
.then(r => r.json())
.then(data => {
const results = data.results || [];
if (results.length === 0) {
siretResults.innerHTML = '<p class="text-xs text-gray-400 p-2">Aucun resultat.</p>';
return;
}
siretResults.innerHTML = results.map(r => {
const siege = r.siege || {};
const siret = siege.siret || '';
const nom = r.nom_complet || r.nom_raison_sociale || '';
const adresse = siege.adresse || '';
const cp = siege.code_postal || '';
const ville = siege.libelle_commune || '';
return '<button type="button" class="siret-result-item block w-full text-left px-3 py-2 hover:bg-white/70 border-b border-white/20 transition-all"'
+ ' data-nom="' + nom.replace(/"/g, '&quot;') + '"'
+ ' data-siret="' + siret + '"'
+ ' data-adresse="' + adresse.replace(/"/g, '&quot;') + '"'
+ ' data-cp="' + cp + '"'
+ ' data-ville="' + ville.replace(/"/g, '&quot;') + '">'
+ '<span class="font-bold text-xs">' + nom + '</span>'
+ '<span class="text-[10px] text-gray-400 ml-2">' + siret + '</span>'
+ '<br><span class="text-[10px] text-gray-400">' + adresse + ' ' + cp + ' ' + ville + '</span>'
+ '</button>';
}).join('');
siretResults.querySelectorAll('.siret-result-item').forEach(item => {
item.addEventListener('click', () => {
const form = siretSearchBtn.closest('form');
if (!form) return;
const set = (name, val) => { const el = form.querySelector('[name="' + name + '"]'); if (el) el.value = val; };
set('raisonSociale', item.dataset.nom);
set('siret', item.dataset.siret);
set('address', item.dataset.adresse);
set('zipCode', item.dataset.cp);
set('city', item.dataset.ville);
siretResults.classList.add('hidden');
siretInput.value = '';
});
});
})
.catch(() => {
siretResults.innerHTML = '<p class="text-xs text-red-500 p-2">Erreur lors de la recherche.</p>';
});
});
siretInput.addEventListener('keydown', (e) => { if (e.key === 'Enter') { e.preventDefault(); siretSearchBtn.click(); } });
document.addEventListener('click', (e) => { if (!siretResults.contains(e.target) && e.target !== siretInput && e.target !== siretSearchBtn) siretResults.classList.add('hidden'); });
}
});
function initStripePayment() {
const btnStripe = document.getElementById('btn-stripe');
const modal = document.getElementById('stripe-modal');
const overlay = document.getElementById('stripe-modal-overlay');
const closeBtn = document.getElementById('stripe-modal-close');
const payBtn = document.getElementById('stripe-pay-btn');
const errorsEl = document.getElementById('stripe-errors');
const paymentEl = document.getElementById('stripe-payment-element');
if (!btnStripe || !modal || !paymentEl) return;
const pk = btnStripe.dataset.pk;
const intentUrl = btnStripe.dataset.intentUrl;
const successUrl = btnStripe.dataset.successUrl;
const amount = btnStripe.dataset.amount;
if (!pk) return;
const stripe = Stripe(pk);
let elements = null;
let clientSecret = null;
let isProcessing = false;
const showModal = () => modal.classList.remove('hidden');
const hideModal = () => { if (!isProcessing) modal.classList.add('hidden'); };
closeBtn.addEventListener('click', hideModal);
overlay.addEventListener('click', hideModal);
document.addEventListener('keydown', (e) => { if (e.key === 'Escape') hideModal(); });
let currentMethod = 'card';
function openStripeModal(method) {
currentMethod = method;
showModal();
payBtn.disabled = true;
payBtn.textContent = 'Chargement...';
errorsEl.classList.add('hidden');
const bodyPayload = method === 'sepa' ? { method: 'sepa' } : {};
fetch(intentUrl, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(bodyPayload),
})
.then(r => r.json())
.then(data => {
if (data.error) {
errorsEl.textContent = data.error;
errorsEl.classList.remove('hidden');
payBtn.textContent = method === 'sepa' ? 'Prelever ' + amount + ' €' : method === 'paypal' ? 'Payer via PayPal ' + amount + ' €' : 'Payer ' + amount + ' €';
return;
}
clientSecret = data.clientSecret;
const elementsOptions = {
clientSecret: clientSecret,
locale: 'fr',
appearance: {
theme: 'stripe',
variables: {
fontFamily: 'Arial, Helvetica, sans-serif',
colorPrimary: method === 'sepa' ? '#2563eb' : '#4f46e5',
borderRadius: '8px',
},
},
};
elements = stripe.elements(elementsOptions);
const paymentElement = elements.create('payment');
paymentEl.innerHTML = '';
paymentElement.mount('#stripe-payment-element');
paymentElement.on('ready', () => {
payBtn.disabled = false;
payBtn.textContent = method === 'sepa' ? 'Prelever ' + amount + ' €' : method === 'paypal' ? 'Payer via PayPal ' + amount + ' €' : 'Payer ' + amount + ' €';
});
paymentElement.on('change', (event) => {
if (event.error) {
errorsEl.textContent = event.error.message;
errorsEl.classList.remove('hidden');
} else {
errorsEl.classList.add('hidden');
}
});
})
.catch(() => {
errorsEl.textContent = 'Erreur de connexion au serveur de paiement.';
errorsEl.classList.remove('hidden');
payBtn.textContent = method === 'sepa' ? 'Prelever ' + amount + ' €' : method === 'paypal' ? 'Payer via PayPal ' + amount + ' €' : 'Payer ' + amount + ' €';
});
}
btnStripe.addEventListener('click', () => openStripeModal('card'));
// Bouton SEPA
const btnSepa = document.getElementById('btn-sepa');
if (btnSepa) {
btnSepa.addEventListener('click', () => openStripeModal('sepa'));
}
payBtn.addEventListener('click', async () => {
if (!clientSecret || !elements || isProcessing) return;
isProcessing = true;
payBtn.disabled = true;
payBtn.textContent = 'Traitement en cours...';
errorsEl.classList.add('hidden');
// confirmPayment gere automatiquement le 3D Secure (3DS)
// Stripe ouvre la modal 3DS dans une iframe si necessaire
const { error } = await stripe.confirmPayment({
elements: elements,
confirmParams: {
return_url: window.location.origin + successUrl + '?method=' + currentMethod,
},
});
// Si on arrive ici, c'est qu'il y a eu une erreur
// (en cas de succes, le navigateur est redirige vers return_url)
if (error) {
if (error.type === 'card_error' || error.type === 'validation_error') {
errorsEl.textContent = error.message;
} else {
errorsEl.textContent = 'Une erreur inattendue est survenue. Veuillez reessayer.';
}
errorsEl.classList.remove('hidden');
}
isProcessing = false;
payBtn.disabled = false;
payBtn.textContent = currentMethod === 'sepa' ? 'Prelever ' + amount + ' €' : 'Payer ' + amount + ' €';
});
}
function initTabSearch(inputId, resultsId) {
const input = document.getElementById(inputId);
const results = document.getElementById(resultsId);
@@ -394,7 +614,62 @@ function initDevisLines() {
addBtn.addEventListener('click', () => addLine());
// Boutons prestations rapides : ajoute une ligne pre-remplie
// Validation : empeche l'envoi si un type est selectionne mais pas le service
const form = document.getElementById('devis-form');
if (form) {
form.addEventListener('submit', (e) => {
const rows = container.querySelectorAll('.line-row');
for (const row of rows) {
const typeSelect = row.querySelector('.line-type');
const serviceSelect = row.querySelector('.line-service-id');
if (!typeSelect || !serviceSelect) continue;
const type = typeSelect.value;
if (!type || type === 'hosting' || type === 'maintenance' || type === 'other' || type === 'ndd' || type === 'website') continue;
// Type avec service obligatoire (esymail) mais pas selectionne — ndd et website autorisent le vide
if (!serviceSelect.value && !serviceSelect.disabled && serviceSelect.options.length > 1) {
e.preventDefault();
serviceSelect.focus();
serviceSelect.classList.add('border-red-500', 'ring-2', 'ring-red-300');
const pos = row.querySelector('.line-pos')?.textContent || '';
alert('Ligne ' + pos + ' : veuillez selectionner le service pour le type "' + typeSelect.options[typeSelect.selectedIndex].text + '".');
return;
}
}
});
}
// Chargement dynamique des services par type
container.addEventListener('change', async (e) => {
if (!e.target.classList.contains('line-type')) return;
const select = e.target;
const row = select.closest('.line-row');
const serviceSelect = row.querySelector('.line-service-id');
const type = select.value;
serviceSelect.innerHTML = '<option value="">— Selectionner le service —</option>';
serviceSelect.disabled = true;
if (!type || type === 'hosting' || type === 'maintenance' || type === 'other') return;
const url = select.dataset.servicesUrl.replace('__TYPE__', type);
try {
const resp = await fetch(url);
const items = await resp.json();
if (items.length > 0) {
items.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.textContent = item.label;
serviceSelect.appendChild(opt);
});
serviceSelect.disabled = false;
}
} catch (err) { /* silencieux */ }
});
// Boutons prestations rapides : ajoute une ligne pre-remplie avec type auto
document.querySelectorAll('.quick-price-btn').forEach(btn => {
btn.addEventListener('click', () => {
addLine();
@@ -404,6 +679,18 @@ function initDevisLines() {
lastRow.querySelector('textarea[name$="[description]"]').value = btn.dataset.description || '';
const priceInput = lastRow.querySelector('.line-price');
priceInput.value = btn.dataset.price || '0.00';
// Auto-set le type de service
const lineType = btn.dataset.lineType || '';
if (lineType) {
const typeSelect = lastRow.querySelector('.line-type');
if (typeSelect) {
typeSelect.value = lineType;
// Trigger change pour charger les services du client
typeSelect.dispatchEvent(new Event('change', { bubbles: true }));
}
}
recalc();
});
});
@@ -457,13 +744,38 @@ function initDevisLines() {
try {
const arr = JSON.parse(initial);
arr.sort((a, b) => (a.pos || 0) - (b.pos || 0));
arr.forEach(l => {
arr.forEach(async (l) => {
addLine();
const row = container.querySelector('.line-row:last-child');
if (!row) return;
row.querySelector('input[name$="[title]"]').value = l.title || '';
row.querySelector('textarea[name$="[description]"]').value = l.description || '';
row.querySelector('.line-price').value = l.priceHt || '0.00';
// Pre-select type
const typeSelect = row.querySelector('.line-type');
if (l.type && typeSelect) {
typeSelect.value = l.type;
// Charge les services si type avec serviceId
if (l.serviceId && l.type !== 'hosting' && l.type !== 'maintenance' && l.type !== 'other') {
const serviceSelect = row.querySelector('.line-service-id');
const url = typeSelect.dataset.servicesUrl.replace('__TYPE__', l.type);
try {
const resp = await fetch(url);
const items = await resp.json();
serviceSelect.innerHTML = '<option value="">— Selectionner le service —</option>';
items.forEach(item => {
const opt = document.createElement('option');
opt.value = item.id;
opt.textContent = item.label;
if (item.id === l.serviceId) opt.selected = true;
serviceSelect.appendChild(opt);
});
serviceSelect.disabled = false;
} catch (err) { /* ignore */ }
}
}
});
recalc();
} catch (e) { /* ignore */ }

View File

@@ -1,147 +1,143 @@
@import "tailwindcss";
/* ─── Glass Design System ─── */
/* ─── Neo Brutalist Design System ─── */
:root {
--glass-bg: rgba(255, 255, 255, 0.65);
--glass-bg-heavy: rgba(255, 255, 255, 0.85);
--glass-border: rgba(255, 255, 255, 0.3);
--glass-border-strong: rgba(255, 255, 255, 0.5);
--glass-dark: rgba(17, 24, 39, 0.85);
--glass-dark-heavy: rgba(17, 24, 39, 0.92);
--glass-blur: 16px;
--glass-blur-heavy: 24px;
--brutal-bg: #ffffff;
--brutal-border: 3px solid #111827;
--brutal-border-thin: 2px solid #111827;
--brutal-shadow: 4px 4px 0px #111827;
--brutal-shadow-hover: 6px 6px 0px #111827;
--brutal-shadow-active: 2px 2px 0px #111827;
--brutal-shadow-gold: 4px 4px 0px #b8860b;
--gold: #fabf04;
--gold-light: rgba(250, 191, 4, 0.15);
--gold-glow: rgba(250, 191, 4, 0.4);
--radius: 16px;
--radius-sm: 10px;
--radius-xs: 6px;
--shadow-glass: 0 8px 32px rgba(0, 0, 0, 0.08), 0 2px 8px rgba(0, 0, 0, 0.04);
--shadow-glass-hover: 0 12px 40px rgba(0, 0, 0, 0.12), 0 4px 12px rgba(0, 0, 0, 0.06);
--shadow-gold: 0 4px 24px rgba(250, 191, 4, 0.25);
--dark: #111827;
--radius: 0px;
--radius-sm: 0px;
--radius-xs: 0px;
}
/* ─── Animated background ─── */
/* ─── Background ─── */
body.glass-bg {
background: #f0f0f5;
background: #f5f5f0;
background-image:
radial-gradient(ellipse at 20% 50%, rgba(250, 191, 4, 0.08) 0%, transparent 50%),
radial-gradient(ellipse at 80% 20%, rgba(99, 102, 241, 0.06) 0%, transparent 50%),
radial-gradient(ellipse at 50% 80%, rgba(250, 191, 4, 0.05) 0%, transparent 50%);
background-attachment: fixed;
repeating-linear-gradient(0deg, transparent, transparent 49px, rgba(0,0,0,0.03) 49px, rgba(0,0,0,0.03) 50px),
repeating-linear-gradient(90deg, transparent, transparent 49px, rgba(0,0,0,0.03) 49px, rgba(0,0,0,0.03) 50px);
}
/* ─── Glass panel ─── */
/* ─── Brutalist panels ─── */
.glass {
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid var(--glass-border);
background: var(--brutal-bg);
border: var(--brutal-border);
border-radius: var(--radius);
box-shadow: var(--shadow-glass);
box-shadow: var(--brutal-shadow);
}
.glass-heavy {
background: var(--glass-bg-heavy);
backdrop-filter: blur(var(--glass-blur-heavy));
-webkit-backdrop-filter: blur(var(--glass-blur-heavy));
border: 1px solid var(--glass-border-strong);
background: var(--brutal-bg);
border: var(--brutal-border);
border-radius: var(--radius);
box-shadow: var(--shadow-glass);
box-shadow: var(--brutal-shadow);
}
.glass-dark {
background: var(--glass-dark);
backdrop-filter: blur(var(--glass-blur-heavy));
-webkit-backdrop-filter: blur(var(--glass-blur-heavy));
border: 1px solid rgba(255, 255, 255, 0.08);
background: var(--dark);
border: var(--brutal-border);
border-radius: var(--radius);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
box-shadow: var(--brutal-shadow);
color: white;
}
.glass-dark-heavy {
background: var(--glass-dark-heavy);
backdrop-filter: blur(var(--glass-blur-heavy));
-webkit-backdrop-filter: blur(var(--glass-blur-heavy));
border: 1px solid rgba(255, 255, 255, 0.06);
background: var(--dark);
border: 3px solid #000;
color: white;
}
.glass-gold {
background: rgba(250, 191, 4, 0.12);
backdrop-filter: blur(var(--glass-blur));
-webkit-backdrop-filter: blur(var(--glass-blur));
border: 1px solid rgba(250, 191, 4, 0.3);
background: var(--gold);
border: var(--brutal-border);
border-radius: var(--radius);
box-shadow: var(--shadow-gold);
box-shadow: var(--brutal-shadow-gold);
}
/* ─── Glass buttons ─── */
/* ─── Brutalist buttons ─── */
.btn-glass {
background: var(--glass-bg);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid var(--glass-border);
border-radius: var(--radius-xs);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
background: var(--brutal-bg);
border: var(--brutal-border-thin);
border-radius: var(--radius);
box-shadow: var(--brutal-shadow);
transition: all 0.1s ease;
cursor: pointer;
&:hover {
background: var(--glass-bg-heavy);
box-shadow: var(--shadow-glass-hover);
transform: translateY(-1px);
box-shadow: var(--brutal-shadow-hover);
transform: translate(-2px, -2px);
}
&:active {
box-shadow: var(--brutal-shadow-active);
transform: translate(2px, 2px);
}
}
.btn-gold {
background: var(--gold);
border: 1px solid rgba(250, 191, 4, 0.6);
border-radius: var(--radius-xs);
box-shadow: var(--shadow-gold);
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border: var(--brutal-border-thin);
border-radius: var(--radius);
box-shadow: var(--brutal-shadow);
transition: all 0.1s ease;
cursor: pointer;
&:hover {
box-shadow: 0 6px 28px rgba(250, 191, 4, 0.4);
transform: translateY(-1px);
box-shadow: var(--brutal-shadow-hover);
transform: translate(-2px, -2px);
}
&:active {
box-shadow: var(--brutal-shadow-active);
transform: translate(2px, 2px);
}
}
.btn-dark {
background: var(--glass-dark-heavy);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--radius-xs);
background: var(--dark);
border: var(--brutal-border-thin);
border-radius: var(--radius);
color: white;
transition: all 0.25s cubic-bezier(0.4, 0, 0.2, 1);
box-shadow: var(--brutal-shadow);
transition: all 0.1s ease;
cursor: pointer;
&:hover {
background: rgba(99, 102, 241, 0.85);
border-color: rgba(99, 102, 241, 0.4);
transform: translateY(-1px);
background: var(--gold);
color: var(--dark);
box-shadow: var(--brutal-shadow-hover);
transform: translate(-2px, -2px);
}
&:active {
box-shadow: var(--brutal-shadow-active);
transform: translate(2px, 2px);
}
}
/* ─── Glass input ─── */
/* ─── Brutalist input ─── */
.input-glass {
background: rgba(255, 255, 255, 0.5);
backdrop-filter: blur(8px);
-webkit-backdrop-filter: blur(8px);
border: 1px solid rgba(0, 0, 0, 0.1);
border-radius: var(--radius-xs);
transition: all 0.2s ease;
background: #ffffff;
border: var(--brutal-border-thin);
border-radius: var(--radius);
transition: all 0.1s ease;
&:focus {
outline: none;
background: rgba(255, 255, 255, 0.8);
border-color: var(--gold);
box-shadow: 0 0 0 3px var(--gold-light), var(--shadow-glass);
box-shadow: 3px 3px 0px var(--gold);
}
}
@@ -160,7 +156,7 @@ body.glass-bg {
}
.heading-page {
border-bottom: 2px solid var(--gold);
border-bottom: 4px solid var(--gold);
display: inline-block;
padding-bottom: 0.5rem;
}
@@ -201,15 +197,14 @@ body.glass-bg {
top: 0;
bottom: 0;
z-index: 50;
transition: left 0.3s cubic-bezier(0.4, 0, 0.2, 1);
transition: left 0.2s ease;
}
.admin-sidebar.open { left: 0; }
.admin-overlay {
display: none;
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
background: rgba(0, 0, 0, 0.5);
z-index: 40;
}
.admin-sidebar.open + .admin-overlay { display: block; }
@@ -223,42 +218,45 @@ body.glass-bg {
gap: 0.75rem;
padding: 0.625rem 0.875rem;
font-size: 0.7rem;
font-weight: 700;
font-weight: 800;
text-transform: uppercase;
letter-spacing: 0.08em;
border-radius: var(--radius-sm);
transition: all 0.2s ease;
letter-spacing: 0.1em;
border-radius: 0;
transition: all 0.1s ease;
color: rgba(255, 255, 255, 0.75);
border-left: 4px solid transparent;
&:hover {
background: rgba(30, 41, 59, 0.9);
background: rgba(255, 255, 255, 0.1);
color: white;
border-left-color: var(--gold);
}
&.active {
background: var(--gold);
color: #111827;
box-shadow: 0 2px 12px rgba(250, 191, 4, 0.3);
border-left-color: #111827;
font-weight: 900;
}
&.active-danger {
background: rgba(220, 38, 38, 0.8);
background: #dc2626;
color: white;
box-shadow: 0 2px 12px rgba(220, 38, 38, 0.3);
border-left-color: #111827;
}
}
/* ─── Scrollbar styling ─── */
.admin-sidebar::-webkit-scrollbar {
width: 4px;
width: 6px;
}
.admin-sidebar::-webkit-scrollbar-track {
background: transparent;
}
.admin-sidebar::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.15);
border-radius: 4px;
background: rgba(255, 255, 255, 0.2);
border-radius: 0;
}
/* ─── Smooth transitions ─── */
@@ -269,4 +267,9 @@ body.glass-bg {
/* ─── Devis lines drag & drop ─── */
.line-row.dragging { opacity: 0.4; }
.line-row.drag-over { border-top: 2px solid #fabf04; }
.line-row.drag-over { border-top: 4px solid var(--gold); }
/* ─── Brutalist overrides for Tailwind rounded ─── */
.rounded, .rounded-lg, .rounded-xl, .rounded-md, .rounded-sm, .rounded-full {
border-radius: 0 !important;
}

View File

@@ -63,9 +63,9 @@ const renderResult = (e, onSelect) => {
</div>
<div class="text-xs text-gray-400 mt-1">${s.geo_adresse || s.adresse || ''}</div>
${d.nom ? '<div class="text-xs text-gray-400 mt-1">Dirigeant : ' + (d.prenoms || '') + ' ' + d.nom + '</div>' : ''}
${isAsso ? '<div class="text-xs mt-1"><span class="px-1.5 py-0.5 bg-indigo-100 text-indigo-800 font-bold text-[10px] uppercase" style="border-radius:4px;">Association</span></div>' : ''}
${isAsso ? '<div class="text-xs mt-1"><span class="px-1.5 py-0.5 bg-indigo-100 text-indigo-800 font-bold text-[10px] uppercase" >Association</span></div>' : ''}
</div>
<span class="shrink-0 px-2 py-1 text-[10px] font-bold uppercase ${actif ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}" style="border-radius: 6px;">
<span class="shrink-0 px-2 py-1 text-[10px] font-bold uppercase ${actif ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800'}" >
${actif ? 'Actif' : 'Ferme'}
</span>
</div>`