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:
326
assets/app.js
326
assets/app.js
@@ -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, '"') + '"'
|
||||
+ ' data-siret="' + siret + '"'
|
||||
+ ' data-adresse="' + adresse.replace(/"/g, '"') + '"'
|
||||
+ ' data-cp="' + cp + '"'
|
||||
+ ' data-ville="' + ville.replace(/"/g, '"') + '">'
|
||||
+ '<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 */ }
|
||||
|
||||
197
assets/app.scss
197
assets/app.scss
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>`
|
||||
|
||||
Reference in New Issue
Block a user