Files
crm_ecosplay/templates/admin/clients/show.html.twig
Serreau Jovann 95d33a9a6d feat: gestion complete Devis + Avis de paiement + DocuSeal signature + mails
Devis :
- Entity DevisLine (pos, title, description, priceHt) liee a Devis (OneToMany cascade/orphanRemoval)
- Champs ajoutes sur Devis : customer (ManyToOne), submissionId, state machine (created/send/accepted/refused/cancel), raisonMessage, totaux HT/TVA/TTC, updatedAt, setUpdatedAt public
- Relation Devis <-> Advert changee de ManyToOne a OneToOne nullable
- Vich Attribute (migration Annotation -> Attribute) pour unsignedPdf/signedPdf/auditPdf
- DevisController CRUD complet : create (form repeater lignes + boutons rapides TarificationService), edit, cancel (libere OrderNumber), generate-pdf, send, resend, create-advert, events
- DevisPdf (FPDF/FPDI) : header legacy (logo, num, date, client), body lignes, summary totaux, footer SITECONSEIL + pagination, champ signature DocuSeal sur page devis + derniere page CGV
- OrderNumberService : preview() et generate() reutilisent les OrderNumber non utilises (isUsed=false) en priorite
- OrderNumber::markAsUnused() ajoute

DocuSeal integration devis :
- DocuSealService : sendDevisForSignature (avec completed_redirect_url), resendDevisSignature (archive ancienne submission), getSubmitterSlug, downloadSignedDevis (sauvegarde via Vich UploadedFile test=true)
- WebhookDocuSealController : dispatch par doc_type devis/attestation, handleDevisEvent (form.completed -> STATE_ACCEPTED + download PDF signe/audit, form.declined -> STATE_REFUSED + raison)
- DocusealEvent entity pour tracer form.viewed/started/completed/declined en temps reel
- Page evenements admin /admin/devis/{id}/events avec badges et payload JSON

Signature client :
- DevisProcessController : page publique /devis/process/{id}/{hmac} securisee par HMAC, boutons Signer (redirect DocuSeal) / Refuser (motif optionnel)
- Pages confirmation : signed.html.twig (merci + recap) et refused.html.twig (confirmation refus + motif)
- Nelmio whitelist : signature.esy-web.dev + signature.siteconseil.fr

Avis de paiement :
- Entity AdvertLine (pos, title, description, priceHt) liee a Advert
- Advert refactorise : customer, state, totaux, raisonMessage, submissionId, advertFile (Vich mapping advert_pdf), lines collection, updatedAt
- AdvertController : generate-pdf, send (mail + PJ + lien paiement), resend (rappel), cancel (delie devis, libere OrderNumber), search Meilisearch
- AdvertPdf (FPDF/FPDI) : QR code Endroid pointant vers /order/{numOrder}, texte "Scannez pour payer"
- OrderPaymentController : page publique /order/{numOrder} avec detail prestations, totaux, options paiement (placeholder)
- Creation auto depuis devis signe : copie client, totaux, lignes, meme OrderNumber

Meilisearch :
- Index customer_devis et customer_advert avec searchable (numOrder, customerName, customerEmail, state) et filterable (customerId, state)
- CRUD indexation sur chaque action (create, edit, send, cancel, create-advert)
- Recherche AJAX dans tabs Devis et Avis avec debounce + dropdown glassmorphism
- Sync admin : boutons syncDevis / syncAdverts + compteurs dans /admin/sync

Emails :
- MailerService : VCF auto (fiche contact SARL SITECONSEIL) en PJ sur tous les mails, bloc HTML pieces jointes injecte automatiquement (exclut .asc/.p7z/smime) avec icone trombone + taille fichier
- Templates : devis_to_sign, devis_signed_client/admin (PJ signed+audit), devis_refused_client/admin, advert_send (PJ + bouton paiement), ndd_expiration
- TestMailCommand : option --force-dsn pour envoyer via un DSN SMTP specifique (test prod depuis dev)

Commande NDD :
- app:ndd:check : verifie expiration domaines <= 30j, envoie mail groupe a monitor@siteconseil.fr
- Cron quotidien 8h (docker + ansible)

Divers :
- Titles templates : CRM SITECONSEIL -> SARL SITECONSEIL (52 fichiers)
- VAULT_URL dev = https://kms.esy-web.dev (comme prod)
- app.js : initDevisLines (repeater + drag & drop), initTabSearch, toggle refus devis
- app.scss : styles drag & drop
- setasign/fpdi-fpdf installe pour fusion PDF
- 5 migrations Doctrine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:44:35 +02:00

757 lines
55 KiB
Twig

{% extends 'admin/_layout.html.twig' %}
{% block title %}{{ customer.fullName }} - Client - SARL SITECONSEIL{% endblock %}
{% block admin_content %}
<div class="page-container">
<div class="flex items-center justify-between mb-6">
<div>
<h1 class="text-2xl font-bold heading-page">{{ customer.fullName }}</h1>
{% if customer.raisonSociale %}
<p class="text-xs text-gray-400 mt-1">{{ customer.raisonSociale }}{{ customer.codeComptable ?? 'N/A' }}</p>
{% endif %}
</div>
<div class="flex items-center gap-3">
{% if customer.state == 'active' %}
<span class="px-3 py-1 bg-green-500/20 text-green-700 font-bold uppercase text-xs rounded-lg">Actif</span>
{% elseif customer.state == 'pending_delete' %}
<span class="px-3 py-1 bg-red-500/20 text-red-700 font-bold uppercase text-xs rounded-lg animate-pulse">Suppression</span>
{% else %}
<span class="px-3 py-1 bg-yellow-100 text-yellow-800 font-bold uppercase text-xs rounded-lg">{{ customer.state }}</span>
{% endif %}
<a href="{{ path('app_admin_clients_index') }}" class="px-4 py-2 glass font-bold uppercase text-xs tracking-widest hover:bg-gray-900 hover:text-white transition-all">Retour</a>
</div>
</div>
{% for type, messages in app.flashes %}
{% for message in messages %}
<div class="mb-4 p-4 glass font-medium text-sm rounded-xl {{ type == 'success' ? 'border-green-300 text-green-800' : 'border-red-300 text-red-800' }}">
{{ message }}
</div>
{% endfor %}
{% endfor %}
{# Tabs #}
{% set tabs = {
'info': 'Information globale',
'contacts': 'Contacts',
'factures': 'Factures',
'avis': 'Avis de Paiement',
'devis': 'Devis',
'impayes': 'Impayes',
'echeancier': 'Echeancier',
'ndd': 'Noms de domaine',
'esyflex': 'EsyFlex',
'sites': 'Sites Internet',
'services': 'Services',
'securite': 'Securite'
} %}
<div class="flex flex-wrap gap-1 mb-6">
{% for key, label in tabs %}
<a href="{{ path('app_admin_clients_show', {id: customer.id, tab: key}) }}"
class="px-4 py-2 font-bold uppercase text-[10px] tracking-wider transition-all {{ tab == key ? 'glass-dark text-white' : 'glass text-gray-600 hover:bg-white/80' }}"
style="border-radius: 8px 8px 0 0;">
{{ label }}
</a>
{% endfor %}
</div>
{# Tab: Information globale #}
{% if tab == 'info' %}
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'info'}) }}" class="flex flex-col gap-6">
<section class="glass p-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Identite</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div>
<label for="firstName" class="block text-xs font-bold uppercase tracking-wider mb-2">Prenom *</label>
<input type="text" id="firstName" name="firstName" value="{{ customer.firstName }}" required class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="lastName" class="block text-xs font-bold uppercase tracking-wider mb-2">Nom *</label>
<input type="text" id="lastName" name="lastName" value="{{ customer.lastName }}" required class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="email" class="block text-xs font-bold uppercase tracking-wider mb-2">Email *</label>
<input type="email" id="email" name="email" value="{{ customer.email ?? customer.user.email }}" required class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4 mt-4">
<div>
<label for="phone" class="block text-xs font-bold uppercase tracking-wider mb-2">Telephone</label>
<input type="tel" id="phone" name="phone" value="{{ customer.phone }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="typeCompany" class="block text-xs font-bold uppercase tracking-wider mb-2">Type</label>
<select id="typeCompany" name="typeCompany" class="w-full px-4 py-3 glass text-sm font-bold">
<option value="">— Selectionner —</option>
{% for val in ['particulier','association','auto-entrepreneur','sas','sarl','eurl','sa','sci'] %}
<option value="{{ val }}" {{ customer.typeCompany == val ? 'selected' }}>{{ val|capitalize }}</option>
{% endfor %}
</select>
</div>
</div>
</section>
<section class="glass p-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Entreprise</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="raisonSociale" class="block text-xs font-bold uppercase tracking-wider mb-2">Raison sociale</label>
<input type="text" id="raisonSociale" name="raisonSociale" value="{{ customer.raisonSociale }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="siret" class="block text-xs font-bold uppercase tracking-wider mb-2">SIRET</label>
<input type="text" id="siret" name="siret" value="{{ customer.siret }}" maxlength="14" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="rcs" class="block text-xs font-bold uppercase tracking-wider mb-2">RCS</label>
<input type="text" id="rcs" name="rcs" value="{{ customer.rcs }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="numTva" class="block text-xs font-bold uppercase tracking-wider mb-2">N° TVA</label>
<input type="text" id="numTva" name="numTva" value="{{ customer.numTva }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="ape" class="block text-xs font-bold uppercase tracking-wider mb-2">Code APE / NAF</label>
<input type="text" id="ape" name="ape" value="{{ customer.ape }}" maxlength="10" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="rna" class="block text-xs font-bold uppercase tracking-wider mb-2">RNA</label>
<input type="text" id="rna" name="rna" value="{{ customer.rna }}" maxlength="20" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="revendeurCode" class="block text-xs font-bold uppercase tracking-wider mb-2">Code revendeur</label>
<input type="text" id="revendeurCode" name="revendeurCode" value="{{ customer.revendeurCode }}" maxlength="10" placeholder="EC-XXXX" class="w-full px-4 py-3 input-glass text-sm font-medium font-mono">
</div>
</div>
</section>
<section class="glass p-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Adresse</h2>
<div class="flex flex-col gap-4">
<div>
<label for="address" class="block text-xs font-bold uppercase tracking-wider mb-2">Adresse</label>
<input type="text" id="address" name="address" value="{{ customer.address }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="address2" class="block text-xs font-bold uppercase tracking-wider mb-2">Complement</label>
<input type="text" id="address2" name="address2" value="{{ customer.address2 }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label for="zipCode" class="block text-xs font-bold uppercase tracking-wider mb-2">Code postal</label>
<input type="text" id="zipCode" name="zipCode" value="{{ customer.zipCode }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
<div>
<label for="city" class="block text-xs font-bold uppercase tracking-wider mb-2">Ville</label>
<input type="text" id="city" name="city" value="{{ customer.city }}" class="w-full px-4 py-3 input-glass text-sm font-medium">
</div>
</div>
<input type="hidden" name="geoLat" value="{{ customer.geoLat }}">
<input type="hidden" name="geoLong" value="{{ customer.geoLong }}">
</div>
</section>
<section class="glass p-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Informations systeme</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Code comptable</span>
<span class="font-mono font-bold">{{ customer.codeComptable ?? '—' }}</span>
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Stripe ID</span>
<span class="font-mono font-bold">{{ customer.stripeCustomerId ?? '—' }}</span>
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Cree le</span>
<span class="font-bold">{{ customer.createdAt|date('d/m/Y H:i') }}</span>
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Modifie le</span>
<span class="font-bold">{{ customer.updatedAt ? customer.updatedAt|date('d/m/Y H:i') : '—' }}</span>
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Revendeur</span>
<span class="font-mono font-bold">{{ customer.revendeurCode ?? '—' }}</span>
</div>
</div>
{% if customer.user.hasTempPassword %}
<div class="mt-4 p-4 bg-indigo-50 border border-indigo-200 rounded-lg flex items-center justify-between">
<div>
<span class="text-xs font-bold uppercase text-indigo-700">Espace client</span>
<p class="text-[10px] text-indigo-500 mt-0.5">Le client n'a pas encore active son compte. Vous pouvez renvoyer l'email de bienvenue.</p>
</div>
<div class="flex gap-2"></div>
</div>
{% else %}
<div class="mt-4 p-4 bg-green-50 border border-green-200 rounded-lg">
<span class="text-xs font-bold uppercase text-green-700">Espace client active</span>
<p class="text-[10px] text-green-500 mt-0.5">Le client a active son compte et peut se connecter sur client.siteconseil.fr</p>
</div>
{% endif %}
</section>
<div class="flex items-center gap-3">
<button type="submit" class="px-6 py-3 btn-gold text-sm font-bold uppercase tracking-wider text-gray-900">Enregistrer</button>
{% if customer.user.hasTempPassword %}
</form>
<form method="post" action="{{ path('app_admin_clients_resend_welcome', {id: customer.id}) }}">
<button type="submit" class="px-6 py-3 glass text-indigo-600 text-sm font-bold uppercase tracking-wider hover:bg-indigo-600 hover:text-white transition-all">Renvoyer email bienvenue</button>
</form>
{% else %}
</form>
{% endif %}
</div>
{# Tab: Contacts #}
{% elseif tab == 'contacts' %}
<section class="glass p-6 mb-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Ajouter un contact</h2>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'contacts'}) }}" class="grid grid-cols-1 md:grid-cols-3 gap-4">
<input type="hidden" name="contact_action" value="create">
<div>
<label for="contact_firstName" class="block text-xs font-bold uppercase tracking-wider mb-2">Prenom *</label>
<input type="text" id="contact_firstName" name="contact_firstName" required class="w-full px-4 py-3 input-glass text-sm font-medium" placeholder="Prenom">
</div>
<div>
<label for="contact_lastName" class="block text-xs font-bold uppercase tracking-wider mb-2">Nom *</label>
<input type="text" id="contact_lastName" name="contact_lastName" required class="w-full px-4 py-3 input-glass text-sm font-medium" placeholder="Nom">
</div>
<div>
<label for="contact_email" class="block text-xs font-bold uppercase tracking-wider mb-2">Email</label>
<input type="email" id="contact_email" name="contact_email" class="w-full px-4 py-3 input-glass text-sm font-medium" placeholder="email@entreprise.fr">
</div>
<div>
<label for="contact_phone" class="block text-xs font-bold uppercase tracking-wider mb-2">Telephone</label>
<input type="tel" id="contact_phone" name="contact_phone" class="w-full px-4 py-3 input-glass text-sm font-medium" placeholder="06 12 34 56 78">
</div>
<div>
<label for="contact_role" class="block text-xs font-bold uppercase tracking-wider mb-2">Role</label>
<input type="text" id="contact_role" name="contact_role" class="w-full px-4 py-3 input-glass text-sm font-medium" placeholder="Gerant, Comptable...">
</div>
<div class="flex items-end gap-3">
<label for="contact_isBilling" class="flex items-center gap-2 cursor-pointer pb-3">
<input type="checkbox" id="contact_isBilling" name="contact_isBilling" value="1" class="accent-[#fabf04]">
<span class="text-xs font-bold">Email facturation</span>
</label>
<button type="submit" class="px-5 py-3 btn-gold text-xs font-bold uppercase tracking-wider text-gray-900 mb-0.5">Ajouter</button>
</div>
</form>
</section>
{% if contacts|length > 0 %}
<div class="glass overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Nom</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Email</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Telephone</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Role</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Facturation</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Actions</th>
</tr>
</thead>
<tbody>
{% for contact in contacts %}
<tr class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3 font-bold">{{ contact.fullName }}</td>
<td class="px-4 py-3 text-xs font-mono">{{ contact.email ?? '—' }}</td>
<td class="px-4 py-3 text-xs">{{ contact.phone ?? '—' }}</td>
<td class="px-4 py-3 text-xs">{{ contact.role ?? '—' }}</td>
<td class="px-4 py-3 text-center">
{% if contact.isBillingEmail %}
<span class="text-green-600 font-bold">&#10003;</span>
{% else %}
<span class="text-gray-300">&#10007;</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'contacts'}) }}" data-confirm="Supprimer le contact {{ contact.fullName }} ?">
<input type="hidden" name="contact_action" value="delete">
<input type="hidden" name="contact_id" value="{{ contact.id }}">
<button type="submit" class="px-2 py-1 border-2 border-red-600 bg-white text-red-600 font-bold uppercase text-[10px] tracking-widest hover:bg-red-600 hover:text-white transition-all">Supprimer</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ contacts|length }} contact(s)</p>
{% else %}
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun contact.</div>
{% endif %}
{# Tab: Noms de domaine #}
{% elseif tab == 'ndd' %}
<section class="glass p-6 mb-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Ajouter un nom de domaine</h2>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'ndd'}) }}" class="flex flex-wrap items-end gap-4">
<input type="hidden" name="domain_action" value="create">
<div class="flex-1 min-w-[200px]">
<label for="domain_fqdn" class="block text-xs font-bold uppercase tracking-wider mb-2">Nom de domaine *</label>
<input type="text" id="domain_fqdn" name="domain_fqdn" required placeholder="exemple.fr" class="w-full px-4 py-3 input-glass text-sm font-medium font-mono">
</div>
<div class="w-48">
<label for="domain_registrar" class="block text-xs font-bold uppercase tracking-wider mb-2">Registrar</label>
<select id="domain_registrar" name="domain_registrar" class="w-full px-4 py-3 glass text-sm font-bold">
<option value="">Auto-detection</option>
<option value="OVH">OVH</option>
<option value="Gandi">Gandi</option>
<option value="Cloudflare">Cloudflare</option>
<option value="Autre">Autre</option>
</select>
</div>
<button type="submit" class="px-5 py-3 btn-gold text-xs font-bold uppercase tracking-wider text-gray-900">Ajouter</button>
</form>
<p class="text-[10px] text-gray-400 mt-2">Le domaine sera automatiquement verifie sur OVH et Cloudflare. Si detecte : gestion et facturation actives, date d'expiration synchronisee.</p>
</section>
{% if domains|length > 0 %}
<div class="glass overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Domaine</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Registrar</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Cloudflare</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Gestion</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Facturation</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Expiration</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Actions</th>
</tr>
</thead>
<tbody>
{% for domain in domains %}
{% set dInfo = domainsInfo[domain.id] ?? {} %}
<tr class="hover:bg-white/50">
<td class="px-4 pt-3 pb-1 font-bold font-mono">{{ domain.fqdn }}</td>
<td class="px-4 pt-3 pb-1 text-xs">{{ domain.registrar ?? '—' }}</td>
<td class="px-4 pt-3 pb-1 text-center">
{% if domain.zoneIdCloudflare %}
<span class="px-2 py-0.5 bg-orange-500/20 text-orange-700 font-bold uppercase text-[10px] rounded">{{ domain.zoneCloudflare ?? 'Lie' }}</span>
{% else %}
<span class="text-gray-300">—</span>
{% endif %}
</td>
<td class="px-4 pt-3 pb-1 text-center">
{% if domain.isGestion %}<span class="text-green-600 font-bold">&#10003;</span>{% else %}<span class="text-gray-300">&#10007;</span>{% endif %}
</td>
<td class="px-4 pt-3 pb-1 text-center">
{% if domain.isBilling %}<span class="text-green-600 font-bold">&#10003;</span>{% else %}<span class="text-gray-300">&#10007;</span>{% endif %}
</td>
<td class="px-4 pt-3 pb-1 text-xs">
{% if domain.expiredAt %}
<span class="{{ domain.isExpired ? 'text-red-600 font-bold' : (domain.isExpiringSoon ? 'text-yellow-600 font-bold' : 'text-gray-500') }}">
{{ domain.expiredAt|date('d/m/Y') }}
</span>
{% else %}
<span class="text-gray-300">—</span>
{% endif %}
</td>
<td class="px-4 pt-3 pb-1 text-center">
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'ndd'}) }}" data-confirm="Supprimer le domaine {{ domain.fqdn }} ?">
<input type="hidden" name="domain_action" value="delete">
<input type="hidden" name="domain_id" value="{{ domain.id }}">
<button type="submit" class="px-2 py-1 border-2 border-red-600 bg-white text-red-600 font-bold uppercase text-[10px] tracking-widest hover:bg-red-600 hover:text-white transition-all">Supprimer</button>
</form>
</td>
</tr>
<tr class="border-b border-white/20">
<td colspan="7" class="px-4 pb-3 pt-0">
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-[10px]">
<span class="font-bold" title="Esy-Mail (boites mail)">
Esy-Mail
{% if dInfo.esyMail ?? false %}
<span class="text-green-600">&#10003;</span>
<span class="text-gray-400">({{ dInfo.emailCount ?? 0 }})</span>
{% else %}
<span class="text-red-500">&#10007;</span>
{% endif %}
</span>
<span class="text-gray-300">|</span>
<span class="font-bold" title="Esy-Mailer (newsletter/mailing)">
Esy-Mailer
{% if dInfo.esyMailer ?? false %}
<span class="text-green-600">&#10003;</span>
{% else %}
<span class="text-red-500">&#10007;</span>
{% endif %}
</span>
<span class="text-gray-300">|</span>
<span class="text-gray-300">|</span>
<span class="font-bold" title="Configuration DNS Esy-Mail (MX, SPF, DKIM, DMARC pour reception)">
Config Esy-Mail
{% if dInfo.configDnsEsyMail ?? false %}
<span class="px-1.5 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase rounded">OK</span>
{% else %}
<span class="px-1.5 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase rounded">KO</span>
{% endif %}
</span>
<span class="text-gray-300">|</span>
<span class="font-bold" title="Configuration DNS Esy-Mailer (SPF, DKIM SES, MAIL FROM pour envoi)">
Config Esy-Mailer
{% if dInfo.configDnsEsyMailer ?? false %}
<span class="px-1.5 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase rounded">OK</span>
{% else %}
<span class="px-1.5 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase rounded">KO</span>
{% endif %}
</span>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ domains|length }} domaine(s)</p>
{% else %}
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun nom de domaine.</div>
{% endif %}
{# Tab: Sites Internet #}
{% elseif tab == 'sites' %}
{% if websites|length > 0 %}
<div class="glass overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Nom</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">UUID</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Type</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Statut</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Cree le</th>
</tr>
</thead>
<tbody>
{% for site in websites %}
<tr class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3 font-bold">{{ site.name }}</td>
<td class="px-4 py-3 text-xs font-mono text-gray-500">{{ site.uuid }}</td>
<td class="px-4 py-3 text-center">
{% if site.type == 'ecommerce' %}
<span class="px-2 py-0.5 bg-purple-500/20 text-purple-700 font-bold uppercase text-[10px] rounded">E-Commerce</span>
{% else %}
<span class="px-2 py-0.5 bg-blue-500/20 text-blue-700 font-bold uppercase text-[10px] rounded">Vitrine</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
{% if site.state == 'open' %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">En ligne</span>
{% elseif site.state == 'install_progress' %}
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 font-bold uppercase text-[10px] rounded">Installation</span>
{% elseif site.state == 'suspended' %}
<span class="px-2 py-0.5 bg-orange-500/20 text-orange-700 font-bold uppercase text-[10px] rounded">Suspendu</span>
{% elseif site.state == 'closed' %}
<span class="px-2 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase text-[10px] rounded">Ferme</span>
{% else %}
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 font-bold uppercase text-[10px] rounded">Cree</span>
{% endif %}
</td>
<td class="px-4 py-3 text-xs text-gray-500">{{ site.createdAt|date('d/m/Y') }}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ websites|length }} site(s)</p>
{% else %}
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun site internet.</div>
{% endif %}
{# Tab: Avis de Paiement #}
{% elseif tab == 'avis' %}
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-bold uppercase tracking-wider">Avis de paiement</h2>
</div>
<div class="mb-4 relative">
<input type="text" id="search-adverts" placeholder="Rechercher un avis de paiement..." data-url="{{ path('app_admin_advert_search', {customerId: customer.id}) }}" data-tab="adverts" class="w-full px-4 py-3 input-glass text-sm font-medium">
<div id="search-adverts-results" class="hidden absolute left-0 right-0 glass-heavy mt-1 max-h-60 overflow-y-auto z-50"></div>
</div>
{% if advertsList|length > 0 %}
<div class="glass overflow-x-auto overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Numero</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Date</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Devis lie</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Lignes</th>
<th class="px-4 py-3 text-right font-bold uppercase text-xs tracking-widest">Total HT</th>
<th class="px-4 py-3 text-right font-bold uppercase text-xs tracking-widest">Total TTC</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Etat</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Actions</th>
</tr>
</thead>
<tbody>
{% for a in advertsList %}
<tr id="avis-{{ a.id }}" class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3 font-mono font-bold">{{ a.orderNumber.numOrder }}</td>
<td class="px-4 py-3 text-xs text-gray-500">{{ a.createdAt|date('d/m/Y') }}</td>
<td class="px-4 py-3 text-xs">
{% if a.devis %}
<a href="{{ path('app_admin_clients_show', {id: customer.id, tab: 'devis'}) }}" class="text-indigo-600 hover:underline font-mono font-bold">{{ a.devis.orderNumber.numOrder }}</a>
{% else %}
<span class="text-gray-400">—</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center text-xs">{{ a.lines|length }}</td>
<td class="px-4 py-3 text-right font-mono">{{ a.totalHt }} &euro;</td>
<td class="px-4 py-3 text-right font-mono font-bold">{{ a.totalTtc }} &euro;</td>
<td class="px-4 py-3 text-center">
{% if a.state == 'accepted' %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Accepte</span>
{% elseif a.state == 'refused' %}
<span class="px-2 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase text-[10px] rounded">Refuse</span>
{% elseif a.state == 'send' %}
<span class="px-2 py-0.5 bg-blue-500/20 text-blue-700 font-bold uppercase text-[10px] rounded">Envoye</span>
{% elseif a.state == 'cancel' %}
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 font-bold uppercase text-[10px] rounded">Annule</span>
{% else %}
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 font-bold uppercase text-[10px] rounded">Cree</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
{% if a.state != 'cancel' %}
<div class="flex items-center justify-center gap-2 flex-wrap">
{% if a.advertFile %}
<a href="{{ vich_uploader_asset(a, 'advertFileUpload') }}" target="_blank"
class="px-3 py-1 bg-gray-900 text-white hover:bg-[#fabf04] hover:text-gray-900 font-bold uppercase text-[10px] rounded transition-all">
Voir PDF
</a>
<form method="post" action="{{ path('app_admin_advert_generate_pdf', {id: a.id}) }}" class="inline" data-confirm="Regenerer le PDF ? Le fichier existant sera remplace.">
<button type="submit" class="px-3 py-1 bg-yellow-500/20 text-yellow-700 hover:bg-yellow-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Regenerer PDF</button>
</form>
{% else %}
<form method="post" action="{{ path('app_admin_advert_generate_pdf', {id: a.id}) }}" class="inline">
<button type="submit" class="px-3 py-1 bg-green-500/20 text-green-700 hover:bg-green-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Generer PDF</button>
</form>
{% endif %}
{% if a.advertFile and a.state == 'created' %}
<form method="post" action="{{ path('app_admin_advert_send', {id: a.id}) }}" class="inline" data-confirm="Envoyer l'avis de paiement {{ a.orderNumber.numOrder }} au client ?">
<button type="submit" class="px-3 py-1 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Envoyer</button>
</form>
{% endif %}
{% if a.state == 'send' %}
<form method="post" action="{{ path('app_admin_advert_resend', {id: a.id}) }}" class="inline" data-confirm="Renvoyer l'avis de paiement au client ?">
<button type="submit" class="px-3 py-1 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Renvoyer</button>
</form>
{% endif %}
<form method="post" action="{{ path('app_admin_advert_cancel', {id: a.id}) }}" class="inline" data-confirm="Annuler cet avis de paiement ? Le lien avec le devis sera supprime.">
<button type="submit" class="px-3 py-1 bg-red-500/20 text-red-700 hover:bg-red-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Annuler</button>
</form>
</div>
{% else %}
<span class="text-[10px] text-gray-400">—</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ advertsList|length }} avis de paiement</p>
{% else %}
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun avis de paiement.</div>
{% endif %}
{# Tab: Devis #}
{% elseif tab == 'devis' %}
<div class="flex items-center justify-between mb-4">
<h2 class="text-sm font-bold uppercase tracking-wider">Devis du client</h2>
<a href="{{ path('app_admin_devis_create', {customerId: customer.id}) }}"
class="px-4 py-2 glass-dark text-white font-bold uppercase text-xs tracking-widest hover:bg-[#fabf04] hover:text-gray-900 transition-all rounded-lg">
+ Creer un devis
</a>
</div>
<div class="mb-4 relative">
<input type="text" id="search-devis" placeholder="Rechercher un devis..." data-url="{{ path('app_admin_devis_search', {customerId: customer.id}) }}" data-tab="devis" class="w-full px-4 py-3 input-glass text-sm font-medium">
<div id="search-devis-results" class="hidden absolute left-0 right-0 glass-heavy mt-1 max-h-60 overflow-y-auto z-50"></div>
</div>
{% if devisList|length > 0 %}
<div class="glass overflow-x-auto overflow-hidden">
<table class="w-full text-sm">
<thead>
<tr class="glass-dark text-white">
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Numero</th>
<th class="px-4 py-3 text-left font-bold uppercase text-xs tracking-widest">Date</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Lignes</th>
<th class="px-4 py-3 text-right font-bold uppercase text-xs tracking-widest">Total HT</th>
<th class="px-4 py-3 text-right font-bold uppercase text-xs tracking-widest">Total TTC</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Etat</th>
<th class="px-4 py-3 text-center font-bold uppercase text-xs tracking-widest">Actions</th>
</tr>
</thead>
<tbody>
{% for d in devisList %}
<tr class="border-b border-white/20 hover:bg-white/50">
<td class="px-4 py-3 font-mono font-bold">{{ d.orderNumber.numOrder }}</td>
<td class="px-4 py-3 text-xs text-gray-500">{{ d.createdAt|date('d/m/Y') }}</td>
<td class="px-4 py-3 text-center text-xs">{{ d.lines|length }}</td>
<td class="px-4 py-3 text-right font-mono">{{ d.totalHt }} &euro;</td>
<td class="px-4 py-3 text-right font-mono font-bold">{{ d.totalTtc }} &euro;</td>
<td class="px-4 py-3 text-center">
{% if d.state == 'accepted' %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Accepte</span>
{% elseif d.state == 'refused' %}
<span class="px-2 py-0.5 bg-red-500/20 text-red-700 font-bold uppercase text-[10px] rounded">Refuse</span>
{% elseif d.state == 'send' %}
<span class="px-2 py-0.5 bg-blue-500/20 text-blue-700 font-bold uppercase text-[10px] rounded">Envoye</span>
{% elseif d.state == 'cancel' %}
<span class="px-2 py-0.5 bg-gray-100 text-gray-600 font-bold uppercase text-[10px] rounded">Annule</span>
{% else %}
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 font-bold uppercase text-[10px] rounded">Cree</span>
{% endif %}
</td>
<td class="px-4 py-3 text-center">
{% if d.state != 'cancel' %}
<div class="flex items-center justify-center gap-2 flex-wrap">
{% if d.unsignedPdf %}
<a href="{{ vich_uploader_asset(d, 'unsignedPdfFile') }}" target="_blank"
class="px-3 py-1 bg-gray-900 text-white hover:bg-[#fabf04] hover:text-gray-900 font-bold uppercase text-[10px] rounded transition-all">
Voir PDF
</a>
{% endif %}
{% if d.unsignedPdf %}
<form method="post" action="{{ path('app_admin_devis_generate_pdf', {id: d.id}) }}" class="inline" data-confirm="Regenerer le PDF ? Le fichier existant sera remplace.">
<button type="submit" class="px-3 py-1 bg-yellow-500/20 text-yellow-700 hover:bg-yellow-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Regenerer PDF</button>
</form>
{% else %}
<form method="post" action="{{ path('app_admin_devis_generate_pdf', {id: d.id}) }}" class="inline">
<button type="submit" class="px-3 py-1 bg-green-500/20 text-green-700 hover:bg-green-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Generer PDF</button>
</form>
{% endif %}
{% if d.unsignedPdf and d.state == 'created' %}
<form method="post" action="{{ path('app_admin_devis_send', {id: d.id}) }}" class="inline" data-confirm="Envoyer le devis {{ d.orderNumber.numOrder }} au client pour signature ?">
<button type="submit" class="px-3 py-1 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Envoyer</button>
</form>
{% endif %}
{% if d.state == 'send' %}
<form method="post" action="{{ path('app_admin_devis_resend', {id: d.id}) }}" class="inline" data-confirm="Renvoyer le lien de signature ? L'ancien lien sera annule.">
<button type="submit" class="px-3 py-1 bg-purple-500/20 text-purple-700 hover:bg-purple-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Renvoyer lien</button>
</form>
{% endif %}
{% if d.state == 'accepted' and d.advert is null %}
<form method="post" action="{{ path('app_admin_devis_create_advert', {id: d.id}) }}" class="inline" data-confirm="Creer l'avis de paiement a partir du devis {{ d.orderNumber.numOrder }} ?">
<button type="submit" class="px-3 py-1 bg-emerald-500/20 text-emerald-700 hover:bg-emerald-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Creer Avis</button>
</form>
{% endif %}
{% if d.advert %}
<a href="{{ path('app_admin_clients_show', {id: customer.id, tab: 'avis'}) }}#avis-{{ d.advert.id }}"
class="px-3 py-1 bg-emerald-500/10 text-emerald-600 hover:bg-emerald-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">
Avis {{ d.advert.orderNumber.numOrder }}
</a>
{% endif %}
{% if d.submissionId %}
<a href="{{ path('app_admin_devis_events', {id: d.id}) }}"
class="px-3 py-1 bg-indigo-500/20 text-indigo-700 hover:bg-indigo-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">
Evenements
</a>
{% endif %}
<a href="{{ path('app_admin_devis_edit', {id: d.id}) }}"
class="px-3 py-1 bg-blue-500/20 text-blue-700 hover:bg-blue-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">
Modifier
</a>
<form method="post" action="{{ path('app_admin_devis_cancel', {id: d.id}) }}" class="inline" data-confirm="Annuler ce devis ? Le numero {{ d.orderNumber.numOrder }} sera libere et pourra etre reutilise.">
<button type="submit" class="px-3 py-1 bg-red-500/20 text-red-700 hover:bg-red-500 hover:text-white font-bold uppercase text-[10px] rounded transition-all">Annuler</button>
</form>
</div>
{% else %}
<span class="text-[10px] text-gray-400">—</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
<p class="mt-3 text-xs text-gray-400">{{ devisList|length }} devis</p>
{% else %}
<div class="glass p-8 text-center text-gray-400 font-bold">Aucun devis.</div>
{% endif %}
{# Tab: Securite #}
{% elseif tab == 'securite' %}
{% set user = customer.user %}
{# Statut compte #}
<section class="glass p-6 mb-6">
<h2 class="text-sm font-bold uppercase tracking-wider mb-4">Statut du compte</h2>
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-xs">
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Email</span>
<span class="font-mono font-bold">{{ user.email }}</span>
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">Mot de passe</span>
{% if user.hasTempPassword %}
<span class="px-2 py-0.5 bg-yellow-100 text-yellow-800 font-bold uppercase text-[10px] rounded">Temporaire</span>
{% else %}
<span class="px-2 py-0.5 bg-green-500/20 text-green-700 font-bold uppercase text-[10px] rounded">Defini</span>
{% endif %}
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">2FA Email</span>
{% if user.isEmailAuthEnabled %}
<span class="text-green-600 font-bold">&#10003; Active</span>
{% else %}
<span class="text-gray-400">&#10007; Desactive</span>
{% endif %}
</div>
<div>
<span class="text-gray-400 font-bold uppercase text-[9px] block">2FA Google</span>
{% if user.isGoogleAuthenticatorEnabled %}
<span class="text-green-600 font-bold">&#10003; Active</span>
{% else %}
<span class="text-gray-400">&#10007; Desactive</span>
{% endif %}
</div>
</div>
</section>
{# Reinitialiser mot de passe #}
<section class="glass p-6 mb-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-sm font-bold uppercase tracking-wider">Reinitialiser le mot de passe</h2>
<p class="text-[10px] text-gray-400 mt-1">Genere un nouveau mot de passe temporaire et envoie un email au client avec un lien pour choisir son mot de passe.</p>
</div>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'securite'}) }}" data-confirm="Reinitialiser le mot de passe de {{ customer.fullName }} ? Un email sera envoye au client.">
<input type="hidden" name="security_action" value="send_reset_password">
<button type="submit" class="px-5 py-3 btn-gold text-xs font-bold uppercase tracking-wider text-gray-900">Envoyer un lien de reinitialisation</button>
</form>
</div>
</section>
{# Desactiver 2FA #}
{% if user.isEmailAuthEnabled or user.isGoogleAuthenticatorEnabled %}
<section class="glass p-6">
<div class="flex items-center justify-between">
<div>
<h2 class="text-sm font-bold uppercase tracking-wider">Desactiver l'authentification a deux facteurs</h2>
<p class="text-[10px] text-gray-400 mt-1">Desactive le 2FA par email et Google Authenticator. Le client pourra le reactiver depuis son profil.</p>
</div>
<form method="post" action="{{ path('app_admin_clients_show', {id: customer.id, tab: 'securite'}) }}" data-confirm="Desactiver la double authentification pour {{ customer.fullName }} ?">
<input type="hidden" name="security_action" value="disable_2fa">
<button type="submit" class="px-5 py-3 border-2 border-red-600 bg-white text-red-600 font-bold uppercase text-xs tracking-wider hover:bg-red-600 hover:text-white transition-all" style="border-radius: 6px;">Desactiver 2FA</button>
</form>
</div>
</section>
{% endif %}
{# Tabs placeholder #}
{% else %}
<div class="glass p-12 text-center">
<p class="text-gray-400 font-bold uppercase text-sm tracking-wider">{{ tabs[tab] ?? tab }}</p>
<p class="text-gray-300 text-xs mt-2">Cette section sera disponible prochainement.</p>
</div>
{% endif %}
</div>
{% endblock %}