feat: services inclus dans les contrats avec quantites et prix
Entite Contrat: - Champ JSON services (service, quantity, priceHt) - Catalogue SERVICE_CATALOG avec 11 services et prix - getTotalHt() calcule le total Controller: - Formulaire accepte service_key[] et service_qty[] en repeater - Associe les prix depuis le catalogue Template index (modal creation): - Repeater dynamique : select service + champ quantite + bouton X - Bouton "+ Ajouter un service" - JS avec nonce CSP Template show: - Tableau services inclus (service, quantite, prix unitaire, sous-total) - Total HT en pied de tableau PDF ContratMigrationSiteconseilPdf: - Section "SERVICES INCLUS DANS LE CONTRAT" avec tableau (service, qte, prix unitaire HT, sous-total HT) - Total HT en bas Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -62,7 +62,7 @@
|
||||
|
||||
{# Modal creation #}
|
||||
<div id="modal-contrat" class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/50">
|
||||
<div class="glass-heavy p-6 w-full max-w-lg">
|
||||
<div class="glass-heavy p-6 w-full max-w-2xl max-h-[90vh] overflow-y-auto">
|
||||
<h2 class="text-lg font-bold uppercase mb-4">Nouveau contrat</h2>
|
||||
<form method="post" action="{{ path('app_admin_contrats_create') }}">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-3 mb-4">
|
||||
@@ -82,6 +82,33 @@
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Services #}
|
||||
<div class="mb-4">
|
||||
<label class="block text-[9px] font-bold uppercase tracking-wider text-gray-400 mb-2">Services inclus dans le contrat</label>
|
||||
<div id="services-container" class="space-y-2">
|
||||
<div class="flex gap-2 items-center">
|
||||
<select name="service_key[]" class="input-glass flex-1 px-3 py-2 text-xs font-bold">
|
||||
<option value="">— Selectionner un service —</option>
|
||||
<option value="esite_vitrine">E-Site Vitrine (100 EUR/mois)</option>
|
||||
<option value="esite_ecommerce">E-Site E-Commerce (150 EUR/mois)</option>
|
||||
<option value="esite_hors_cms">Site hors CMS Esy-Web (100 EUR/mois)</option>
|
||||
<option value="email_3go">E-Mail 3 Go (1 EUR/mois)</option>
|
||||
<option value="email_50go">E-Mail 50 Go (5 EUR/mois)</option>
|
||||
<option value="ndd_gestion">Nom de domaine - Gestion (30 EUR/an)</option>
|
||||
<option value="ndd_renouvellement">Nom de domaine - Renouvellement (20 EUR/an)</option>
|
||||
<option value="eprotect">E-Protect Pro (60 EUR/trimestre)</option>
|
||||
<option value="ecalendar">E-Calendar (30 EUR/mois)</option>
|
||||
<option value="echat">E-Chat (15 EUR/mois)</option>
|
||||
<option value="emailer">E-Mailer (30 EUR/mois)</option>
|
||||
</select>
|
||||
<input type="number" name="service_qty[]" value="1" min="1" class="input-glass w-16 px-2 py-2 text-xs font-bold text-center">
|
||||
<button type="button" class="remove-service px-2 py-1 bg-red-500/20 text-red-700 font-bold text-xs hidden">X</button>
|
||||
</div>
|
||||
</div>
|
||||
<button type="button" id="add-service-btn" class="mt-2 px-3 py-1 glass text-xs font-bold uppercase tracking-wider hover:bg-gray-900 hover:text-white transition-all">+ Ajouter un service</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end gap-2">
|
||||
<button type="button" data-modal-close="modal-contrat" class="px-4 py-2 glass font-bold uppercase text-[10px] tracking-widest">Annuler</button>
|
||||
<button type="submit" class="btn-gold px-4 py-2 font-bold uppercase text-[10px] tracking-wider text-gray-900">Creer</button>
|
||||
@@ -90,4 +117,37 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script nonce="{{ csp_nonce('script') }}">
|
||||
(function() {
|
||||
var container = document.getElementById('services-container');
|
||||
var addBtn = document.getElementById('add-service-btn');
|
||||
if (!container || !addBtn) return;
|
||||
|
||||
var optionsHtml = container.querySelector('select').innerHTML;
|
||||
|
||||
function updateRemove() {
|
||||
var rows = container.querySelectorAll('.flex');
|
||||
rows.forEach(function(r) {
|
||||
var btn = r.querySelector('.remove-service');
|
||||
if (btn) btn.classList.toggle('hidden', rows.length <= 1);
|
||||
});
|
||||
}
|
||||
|
||||
addBtn.addEventListener('click', function() {
|
||||
var div = document.createElement('div');
|
||||
div.className = 'flex gap-2 items-center';
|
||||
div.innerHTML = '<select name="service_key[]" class="input-glass flex-1 px-3 py-2 text-xs font-bold">' + optionsHtml + '</select>'
|
||||
+ '<input type="number" name="service_qty[]" value="1" min="1" class="input-glass w-16 px-2 py-2 text-xs font-bold text-center">'
|
||||
+ '<button type="button" class="remove-service px-2 py-1 bg-red-500/20 text-red-700 font-bold text-xs">X</button>';
|
||||
container.appendChild(div);
|
||||
div.querySelector('.remove-service').addEventListener('click', function() { div.remove(); updateRemove(); });
|
||||
updateRemove();
|
||||
});
|
||||
|
||||
container.addEventListener('click', function(e) {
|
||||
if (e.target.classList.contains('remove-service')) { e.target.parentElement.remove(); updateRemove(); }
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -54,6 +54,54 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{# Services #}
|
||||
{% if contrat.services|length > 0 %}
|
||||
<div class="glass p-5 mb-6">
|
||||
<h2 class="text-sm font-bold uppercase tracking-wider mb-3">Services inclus</h2>
|
||||
<div class="glass overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead>
|
||||
<tr class="glass-dark text-white">
|
||||
<th class="px-4 py-2 text-left font-bold uppercase text-[10px] tracking-widest">Service</th>
|
||||
<th class="px-4 py-2 text-center font-bold uppercase text-[10px] tracking-widest">Quantite</th>
|
||||
<th class="px-4 py-2 text-right font-bold uppercase text-[10px] tracking-widest">Prix unitaire HT</th>
|
||||
<th class="px-4 py-2 text-right font-bold uppercase text-[10px] tracking-widest">Sous-total HT</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% set catalogLabels = {
|
||||
'esite_vitrine': 'E-Site Vitrine',
|
||||
'esite_ecommerce': 'E-Site E-Commerce',
|
||||
'esite_hors_cms': 'Site hors CMS Esy-Web',
|
||||
'email_3go': 'E-Mail 3 Go',
|
||||
'email_50go': 'E-Mail 50 Go',
|
||||
'ndd_gestion': 'Nom de domaine - Gestion',
|
||||
'ndd_renouvellement': 'Nom de domaine - Renouvellement',
|
||||
'eprotect': 'E-Protect Pro',
|
||||
'ecalendar': 'E-Calendar',
|
||||
'echat': 'E-Chat',
|
||||
'emailer': 'E-Mailer'
|
||||
} %}
|
||||
{% for s in contrat.services %}
|
||||
<tr class="border-b border-white/20 {{ loop.index is odd ? 'bg-white/30' : '' }}">
|
||||
<td class="px-4 py-2 font-bold text-xs">{{ catalogLabels[s.service] ?? s.service }}</td>
|
||||
<td class="px-4 py-2 text-center text-xs">{{ s.quantity }}</td>
|
||||
<td class="px-4 py-2 text-right text-xs font-mono">{{ s.priceHt|number_format(2, ',', ' ') }} €</td>
|
||||
<td class="px-4 py-2 text-right text-xs font-mono font-bold">{{ (s.priceHt * s.quantity)|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr class="glass-dark text-white">
|
||||
<td colspan="3" class="px-4 py-2 text-right font-bold uppercase text-[10px] tracking-widest">Total HT</td>
|
||||
<td class="px-4 py-2 text-right font-mono font-bold">{{ contrat.totalHt|number_format(2, ',', ' ') }} €</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{# Actions #}
|
||||
<div class="flex flex-wrap gap-2 mb-6">
|
||||
{% if contrat.state == 'draft' %}
|
||||
|
||||
Reference in New Issue
Block a user