{% extends 'base.html.twig' %} {% block title %}Mes transactions — Bubul{% endblock %} {# Le sous-tableau des lignes d'un mois. Six colonnes, pas huit. Le CA brut et les frais Stripe sortent : c'est la BASE COMMISSIONNÉE — le net après frais Stripe — qui est réellement multipliée par le taux, et elle suffit à rendre le calcul vérifiable. Le détail complet reste dans l'export CSV. Le même tableau sert aux lignes réglées et aux lignes tardives : c'est la même information, seul son sort diffère. #} {% macro lignes(lines, plansBySlug, tardives = false) %}
{# Les opérateurs vivent dans l'en-tête, pas dans les cellules : une contre-passation porte des montants négatifs, et « × -1,03 € » ne se lit pas. Lue en travers, la ligne d'en-tête EST le calcul. #} {% for line in lines %} {% endfor %}
Date Filleul OffreBase commissionnée × Taux = Commission
{{ line.paidAt|date('d/m/Y') }} {{ line.user ? line.user.fullName : '—' }} {{ line.user ? line.user.email : '' }} {% set p = plansBySlug[line.planSlug] ?? null %} {% include '_atoms/plan-badge/plan-badge.html.twig' with { name: p ? p.name : line.planSlug, gradient: p ? p.gradientCss : null, isFree: p ? p.isFree() : (line.planSlug is null), isAnnual: line.billingCycle == 'annual' } only %} {{ line.amountNetAfterStripe|number_format(2, ',', ' ') }} € {{ (line.appliedRate * 100)|number_format(2, ',', ' ') }} % {{ line.commissionAmount|number_format(2, ',', ' ') }} € {% if line.isReversal %} {% include '_atoms/badge/badge.html.twig' with { label: 'Contre-passation', variant: 'error', size: 'xs' } only %} {% endif %}
{% endmacro %} {% block body %}
{% include '_molecules/page-header/page-header.html.twig' with { title: 'Mes transactions', subtitle: 'Votre relevé, mois par mois. Dépliez un mois pour voir ses paiements.', icon: 'wallet', backHref: path('app_commercial_dashboard'), backLabel: 'Retour au tableau de bord' } only %}
{# L'export suit le filtre : sans quoi le fichier téléchargé contredirait l'écran depuis lequel on l'a demandé. #} {% include '_atoms/button/button.html.twig' with { label: 'Exporter en CSV', variant: 'primary', size: 'md', icon: 'download', href: path('app_commercial_transactions_export', range.isActive() ? { du: range.startInput(), au: range.endInput() } : {}) } only %}
{% include '_molecules/month-range-filter/month-range-filter.html.twig' with { action: path('app_commercial_transactions'), months: moisDisponibles, start: range.startMonthInput(), end: range.endMonthInput() } only %}
{# ================================================================ LE RELEVÉ — une ligne par mois, le détail au dépliage ================================================================ #}
{% if periods is empty %}
{% if range.isActive() %} {% include '_molecules/empty-state/empty-state.html.twig' with { title: 'Aucune transaction sur cette période', description: 'Élargissez la période, ou affichez tout votre relevé.', icon: 'wallet', variant: 'minimal', actionText: 'Tout voir', actionHref: path('app_commercial_transactions') } only %} {% else %} {% include '_molecules/empty-state/empty-state.html.twig' with { title: 'Aucune transaction pour l\'instant', description: 'Dès qu\'un filleul souscrit une offre payante, son paiement apparaît ici et vous rapporte une commission. Partagez votre code depuis la page Parrainage.', icon: 'wallet', variant: 'minimal', actionText: 'Voir mon code de parrainage', actionHref: path('app_settings_referral') } only %} {% endif %}
{% else %}
{# La colonne du chevron : sans libellé, elle n'annonce rien d'autre que le fait que la ligne se déplie. #} {# Un par mois : c'est le seul élément qui puisse porter le contrôleur au-dessus de la ligne sommaire ET de sa ligne de détail. Le détail est rendu VISIBLE et replié par le contrôleur à connect() : sans JavaScript la page montre tout, ce qui est dégradé mais juste. #} {% for period in periods %} {% set estMoisCourant = period.period == moisCourant %} {# C'est la LIGNE ENTIÈRE qui déplie, pas le seul libellé du mois : la cible de clic est ce que l'œil désigne. Le porte donc le rôle, l'état et le clavier ; le chevron n'est plus qu'un indicateur, posé tout à droite au bout de la ligne. Même montage que templates/commercial/paiements.html.twig. #} : shared--collapsible pose `display: block`, ce qui détruirait une ligne de tableau. #}
{% if period.isPaid or period.carryOverIn < 0 or period.carryOverOut < 0 %}
{% if period.isPaid %} {% include '_atoms/icon/icon.html.twig' with { name: 'check-circle', size: 'xs', color: 'success' } only %} Viré le {{ period.paidAt ? period.paidAt|date('d/m/Y') : '—' }}{% if period.reference %} · référence {{ period.reference }}{% endif %} {% endif %} {% if period.carryOverIn < 0 %} Report du mois précédent : {{ period.carryOverIn|number_format(2, ',', ' ') }} € {% endif %} {% if period.carryOverOut < 0 %} Reste {{ period.carryOverOut|number_format(2, ',', ' ') }} € reporté au mois suivant {% endif %}
{% endif %} {{ _self.lignes(period.lines, plansBySlug) }} {# ── Ce qui est arrivé APRÈS le virement de ce mois ── Un virement figé n'est JAMAIS recalculé : ces lignes ne sont ni comprises dans le versement, ni payables, ni reportées au mois suivant. Les taire est l'anomalie la plus coûteuse de cet écran — l'encart reste en clair. #} {% if period.hasLateLines() %}
{% include '_molecules/inline-alert/inline-alert.html.twig' with { variant: 'warning', icon: 'warning', message: '' ~ period.lateLines|length ~ ' transaction' ~ (period.lateLines|length > 1 ? 's' : '') ~ ' arrivée' ~ (period.lateLines|length > 1 ? 's' : '') ~ ' après le virement — ' ~ (period.lateCommissionAmount|number_format(2, ',', ' ')) ~ ' €' ~ 'Le virement de ce mois est définitif : il n\'est jamais recalculé. Ces commissions ne sont donc ni comprises dans ce versement, ni reportées au mois suivant. Si ce montant vous semble vous revenir, signalez-le à l\'administration.' } only %}
{{ _self.lignes(period.lateLines, plansBySlug, true) }} {% endif %}
{% endfor %}
Mois Paiements Commission acquise Montant viré StatutDétail
{{ period.label() }} {{ period.lines|length }} {{ period.commissionAmount|number_format(2, ',', ' ') }} € {% if period.hasLateLines() %} + {{ period.lateCommissionAmount|number_format(2, ',', ' ') }} € hors relevé {% endif %} {{ period.status == 'paye' ? (period.amountPayable|number_format(2, ',', ' ')) ~ ' €' : '—' }} {% include '_atoms/badge/badge.html.twig' with { label: period.status == 'paye' ? 'Versé' : (period.status == 'en_cours' ? 'Mois en cours' : 'À verser'), variant: period.status == 'paye' ? 'success' : (period.status == 'en_cours' ? 'info' : 'warning'), size: 'sm' } only %} {% include '_atoms/icon/icon.html.twig' with { name: 'chevron-down', size: 'xs' } only %}
{# La cible est ce
, jamais le
{% endif %}
{% endblock %}