110 Commits
Author SHA1 Message Date
bachir 6e1c421e5b ecarts de 0.01€ visible dans le tableau 2026-09-09 21:42:17 +02:00
bachir 5eca3804bf Répartition assistée sur le formulaire + widget compacté
- ledger-form.js : nouveau behavior figliLedgerRepartition --
  pré-remplit les montants de répartition (1re ligne = HT entier,
  chaque ajout partage au centime avec report du reste sur les lignes
  suivantes), valeurs saisies manuellement ou chargées de la base
  « figées » (plus jamais déplacées), écart en direct dans la cellule
  de titre du widget (miroir exact du round(HT − somme, 2) et de la
  tolérance 0,01 du presave). Zéro changement PHP : #validate et
  node_presave() restent l'autorité. Les verrous vivent hors du DOM
  (survie aux re-rendus AJAX du widget) ; la détection manuel/assist
  repose sur le fait qu'une écriture programmatique ne déclenche pas
  d'événement input.
- ledger-form.css : compactage du widget -- titre « Répartition » par
  ligne, bouton Collapse et colonne Order masqués, paragraph-top en
  absolu (coin haut-droit, zéro hauteur), une seule ligne par
  répartition « Compte [input] Montant (€) [input] » où l'input du
  Compte est contraint en pourcentage de son wrapper claro-autocomplete
  (size=60 : il débordait sur le libellé Montant), header réordonné
  titre / écart / menu trois-points, inputs visibles au repos
  (--flform-bg, atténué en mode sombre), marqueur « figé ».
- install : update_8015 (features du widget vidées) puis update_8016
  (collapse_edit_all rétabli à la demande, duplicate reste off) +
  settings de l'install fraîche synchronisés.
2026-09-09 16:44:46 +02:00
bachir d2f3179f01 Import de relevé bancaire CSV en libre-service (/lignes/importer-releve)
Chaque transaction du relevé devient une ligne « à trier » : type autre,
répartition vide (liseré rouge existant via field_ecart), tag field_flag
« IMP AAMMJJ » (liseré ambre + filtre existants), client rapproché par
mots (ClientMatcher : séquence contiguë, sinon mot significatif unique --
jamais en sous-chaîne, jamais auto-créé).

- src/Import/ : CsvReleveParser (ISO-8859-1 confirmé, en-tête strict,
  fgetcsv avec escape '' explicite -- dépréciation PHP 8.4, rejets
  propres avec n° de ligne), ReleveImportBatch (Batch API par lots de
  25, dédoublonnage COMPTÉ par empreinte field_import_fitid -- max(0,
  k−m) importe les vrais doublons légitimes et dédoublonne à travers
  des fichiers qui se chevauchent --, totaux de contrôle au centime
  sur la page de résultat, résumé en tempstore privé).
- ReleveUploadForm : upload private://releves (fichier conservé +
  usage, hors de portée du cron), parse en validateForm(), batch,
  redirection vers la page de résultat.
- field_montant_releve : référence bancaire immuable, écrite une fois
  à l'import et jamais par presave ; affichée en TEXTE sous Montant
  TTC (widget remplacé par un #type item -- un item ne soumet rien et
  extractFormValues() saute le champ sans valeur soumise, la valeur
  survit donc à chaque save) ; masquée sur les lignes sans montant.
- SkipValidationContext : contournement du contrôle de répartition
  requête-scopé (ferme le trou de concurrence de l'ancien state
  global, AUDIT-2026-09-09 §2.2) -- presave honore le service (state
  gardé pour compat), updateType/updateField basculent dessus.
- Permissions (AUDIT §2.2 priorité 1) : access figli ledger sur toutes
  les routes du module + autocomplete (RouteSubscriber), import
  réservé Éditeur/Admin, access content retiré du rôle Authenticated
  (config/sync re-exportée pour les 4 rôles).
- Gin : hook_gin_ignore_sticky_form_actions() -- sans ça, le bouton
  Importer partait dans la barre sticky du chrome masqué.
- install : 8011 champs, 8012 index sur les empreintes, 8013/8014
  montant_releve sur le formulaire sous le TTC (poids renumérotés).
- /lignes : boutons + Ajouter / Importer / Historique dans le footer
  sticky (compacts), footer colspan dès la première colonne, badges de
  signalement qui reviennent à la ligne au lieu de déborder.
2026-09-09 14:58:36 +02:00
bachir 3688bddba8 Affiche les messages Drupal par-dessus les fenêtres modales
La région [data-drupal-messages] vit dans le layout Gin, où des stacking
contexts ancêtres neutralisaient son position:fixed + z-index : tout le
sous-arbre passait sous l'overlay de la modale jQuery UI (enfant direct
du <body>), rendant illisibles les messages insérés pendant l'édition
d'une ligne (MessageCommand -- erreur de répartition, création...).

admin-chrome.js déplace la région en enfant direct du <body> au
chargement et la re-vérifie dès qu'une modale entre dans le DOM ;
admin-chrome.css passe son z-index à 100000, hors d'atteinte du
_moveToTop de jQuery UI (qui ne remonte un dialog que au-dessus des
siblings .ui-front, ce que la région n'est pas).
2026-09-09 12:59:37 +02:00
bachir af36591d70 Ajoute le total en bas de chaque graph Charges structurelles par client
Ligne "Total" sous le graph principal et sous chacune des cartes par
année -- somme calculée à partir des items déjà tracés (sumItems()), pas
d'un champ stats séparé, pour rester cohérente par construction avec les
barres affichées au-dessus.
2026-09-08 15:38:17 +02:00
bachir 74d8eb2aa2 Ajoute "Charges structurelles par client" (+ par année) sur le dashboard SAS
Réutilise le node-level query déjà exécuté pour caParClient/totalParType
(aucune requête SQL supplémentaire) -- accumule abs(montant_ht) des
lignes type=charge par client (le vendeur/organisme : loyer, assurance,
URSSAF...). Trié par montant décroissant (pas alphabétique), pas de
plafond top-N vu le petit nombre de vendeurs distincts. Coloré en gris
"charge", même couleur que ce type dans "Répartition de l'activité par
type".
2026-09-08 15:30:31 +02:00
bachir 96bd9502d2 Les totaux du footer de /lignes suivent maintenant les filtres actifs
Remplace /lignes/api/totaux (LedgerStatsController::totauxAnnee(),
toujours non filtré) par une agrégation côté client des lignes renvoyées
par LedgerRowsController::index() (même endpoint que la fenêtre
glissante) avec annee=<année visible> + les filtres actifs. Reste une
requête serveur dédiée sur l'année entière (pas de LIMIT/range sur la
requête Entity), donc le total ne dépend jamais de ce qui est
effectivement chargé dans la fenêtre glissante à cet instant -- vérifié :
207 lignes non filtrées vs 13 avec le filtre "OVH", total du footer
identique au calcul indépendant dans les deux cas.

onFilterChanged() déclenche maintenant systématiquement
loadCurrentYearTotals() (pas seulement quand detectCurrentYear() détecte
un changement d'année) -- sinon changer un filtre en restant sur la même
année laissait le footer afficher l'ancien total non filtré.

totauxAnnee() et sa route sont supprimés (plus aucun appelant après ce
changement, vérifié par recherche).
2026-09-08 15:25:18 +02:00
bachir cb2a0fcdaa Trie le graph versements par ordre alphabétique, Salaire/stage et Sous-traitant en dernier
Les 6 comptes sont désormais triés alphabétiquement plutôt que par
montant décroissant ; Salaire/stage et Sous-traitant ne participent pas à
ce tri, ils sont simplement ajoutés après coup, dans cet ordre fixe.
2026-09-08 15:10:49 +02:00
bachir 612269ec6d Colore les lignes du graph versements selon leur type
Les 6 comptes reprennent la couleur "versement" (orange, même que
"Versement freelance" dans Répartition par type) puisque c'est la même
somme, juste ventilée par bénéficiaire ; Salaire/stage et Sous-traitant
gardent leur propre couleur de type. Réutilise typeColor()/TYPE_COLORS
déjà en place, juste besoin d'un champ "type" sur chaque item pour que
colorFor puisse s'en servir.
2026-09-08 15:07:41 +02:00
bachir d17a3e2e33 Ajoute Salaire/stage et Sous-traitant au graphique des versements par compte
Deux lignes supplémentaires (pas ventilées par compte, un seul total
chacune) dans "Total des versements par compte" et sa version par année --
même source déjà utilisée par "Répartition de l'activité par type"
(total_par_type / total_par_type_par_annee), aucun changement backend.
2026-09-08 15:05:18 +02:00
bachir 692f7f3ea3 Ajoute "Total des versements par compte" (+ par année) sur le dashboard SAS
Réutilise total_par_type_par_compte / total_par_type_par_compte_par_annee,
déjà exposés par /dashboard/api/stats pour /dashboard/compte -- aucun
changement backend nécessaire, juste extrait la clé "versement" par
compte au lieu de la garder scindée par type.
2026-09-08 15:03:51 +02:00
bachir 8dfb4af98a Nouvelle page /dashboard/repartition, renomme "Dashboard" en "SAS" dans le menu
Nouvelle page "Répartition/Soldes" entre "SAS" (ex-"Dashboard") et "Par
compte" dans le menu : reprend "Solde par compte" et "Évolution du solde
par compte", retirés du dashboard général pour le recentrer sur
l'activité/CA/type/client. Mêmes données déjà exposées par
/dashboard/api/stats (solde_par_compte, solde_par_compte_par_annee),
aucun changement backend nécessaire pour cette page.

js/dashboard-repartition.js reprend le HBarChart/MiniTrend de
dashboard.js -- dupliqués plutôt que partagés, même convention que
dashboard-compte.js. Racine Vue volontairement le même id
#figli-dashboard-app que les deux autres pages dashboard (pas un id
dédié) : dashboard.css scope ses variables CSS (thème clair/sombre) sur
ce sélecteur, réutiliser le même id est comment les trois pages héritent
du même thème sans feuille de style séparée -- vérifié en dark mode.

Le lien de menu "Dashboard" devient "SAS" partout (les 3 templates Twig
+ le nav en render array de HistoryController, qui n'a pas de template
Twig propre).

dashboard.js : MiniTrend/soldeParCompteItems/comptesOrdonnes/
trendValues supprimés (code mort après le déplacement, plus rien ne les
utilise sur le dashboard général).
2026-09-08 15:00:22 +02:00
bachir 43817dcdce Retire la carte "Meilleur solde" du dashboard général 2026-09-08 14:38:23 +02:00
bachir 3552b5a79d Ajoute la répartition par type et le top clients par année sur /dashboard
Reprend exactement le pattern des petits multiples déjà en place sur
/dashboard/compte (grille figli-year-hbar-grid, variante compacte de
h-bar-chart) -- mêmes classes CSS, aucun nouveau style nécessaire.

Backend : DashboardStatsController::stats() calcule maintenant aussi
total_par_type_par_annee et top_clients_par_annee (top 8, contre 12 en
toutes années confondues) à partir des mêmes requêtes SQL déjà en place,
sans requête supplémentaire.

Frontend : dashboard.js n'a jamais de données ligne par ligne (contraire-
ment à dashboard-compte.js qui filtre côté client) -- l'agrégation par
année doit donc venir du serveur. Ajout du prop "compact" au HBarChart de
dashboard.js (jusqu'ici absent, seule la copie de dashboard-compte.js
l'avait).
2026-09-08 14:34:04 +02:00
bachir c927771795 Corrige un deadlock dans onFilterChanged() qui gelait le tableau
ensureScrollable() (appelé par onFilterChanged() après chaque changement
de filtre) appelle lui-même loadOlder()/loadNewer(), qui passent par le
même _queueWindowOp -- en le chaînant *à l'intérieur* de l'opération déjà
mise en file par onFilterChanged(), la file d'attente se retrouvait à
attendre sa propre continuation dès qu'un filtre laissait trop peu de
lignes pour remplir l'écran, gelant purement et simplement le tableau
(recherche qui ne charge plus les lignes précédentes en scrollant, et
même effacer le filtre ensuite ne faisait plus rien -- tout attendait
derrière l'opération bloquée).

Corrigé en chaînant ensureScrollable() après la résolution de l'opération
mise en file, pas dedans -- ses propres appels à loadOlder()/loadNewer()
s'empilent alors normalement sur la file, sans dépendance circulaire.

Reproduit et vérifié en conditions réelles : recherche "Assurance local"
(47 correspondances de 2023 à 2026) qui chargeait bien 2026 mais bloquait
en scrollant vers le haut -- après correctif, chaque scroll vers le haut
déclenche bien un nouveau chargement (vérifié sur 2 scrolls successifs),
et effacer le filtre recharge immédiatement la vue complète.
2026-09-08 14:22:39 +02:00
bachir f7e7ae3265 Phase 2 : filtrage server-side de /lignes (compte, client, type, signalement, écarts, recherche libre)
Remplace le "charge tout puis filtre côté client" (filteredRows()) par un
nouvel endpoint /lignes/api/lignes (LedgerRowsController), qui pousse tous
les filtres de la barre d'outils dans une seule requête Entity/Field Query
API (pas de SQL brut) -- y compris à travers la relation
field_repartition -> field_compte (paragraph -> taxonomie), confirmé
fonctionner empiriquement avant d'écrire le contrôleur. `rows` ne contient
donc plus que ce qui est à la fois dans la fenêtre de dates ET dans les
filtres actifs ; la fenêtre glissante elle-même (loadOlder/loadNewer/
ensureScrollable) est inchangée, seul ce qui la peuple change.

Ajouts :
- Nouveau filtre "Libellé / Détails" (recherche plein texte sur
  field_notes ou le titre), débouncé côté client (350ms).
- filterYear (l'"Année" dédiée) passe par le même endpoint via son
  paramètre "annee".
- mergeChangedRows() (le merge du polling) tient maintenant compte des
  filtres actifs : une ligne qui ne correspond plus après un changement
  est retirée de `rows`, une ligne qui correspond nouvellement est ajoutée
  -- polling lui-même reste global/non filtré, seul le merge est
  filter-aware.

JSON:API reste utilisé pour ce que l'endroit filtré ne couvre pas : le
groupe entrée/sorties liées (fetchLignesByNids) et le polling
(fetchChangedSince), tous deux indépendants d'une plage de dates+filtres.

Testé en local : chaque filtre individuellement et combiné (compte+q,
client+type), widening de fenêtre sous filtre restrictif, restauration
combinée depuis le hash au reload, modale d'édition + reloadWindow après
fermeture, polling sans erreur.
2026-09-08 14:04:35 +02:00
bachir 7bcf65d25b Ajoute field_ecart (Montant HT - somme répartition), stocké à la sauvegarde
Phase 1 du chantier filtrage serveur de /lignes : l'écart devient une
vraie valeur stockée plutôt que recalculée à la volée en résolvant les
paragraphes de répartition, pour que le futur filtre "Écarts uniquement"
côté serveur puisse filtrer directement dessus.

Contrairement au Montant TTC (protégé par un garde-fou pour ne jamais
altérer un TTC historique), l'écart n'a pas de valeur passée à
protéger -- il reflète l'état *actuel* de la répartition, donc
recalculé sans condition à chaque sauvegarde. figli_compta_ledger_
node_presave() calculait déjà cette somme pour la validation
répartition == HT ; le stockage était quasi gratuit à ajouter au même
endroit.

Migration (update hooks 8009/8010) : rétro-calcul mécanique pour les
1556 lignes existantes (tous types, contrairement aux migrations TVA/
cotisation qui ne concernaient que les entrées client) -- aucune
ambiguïté à arbitrer, juste HT moins répartition. Vérifié
indépendamment ligne par ligne après coup : 0 écart entre la valeur
recalculée à la main et celle stockée par la migration.

Nouveau flag d'état 'figli_compta_ledger.skip_revision', utilisé
uniquement par cette migration : backfiller un champ purement calculé
sur 1556 lignes déjà migrées n'est pas un changement éditorial qui
justifie 1556 nouvelles révisions -- vérifié qu'aucune révision
supplémentaire n'a été créée. Reste indépendant de skip_validation
(déjà utilisé par tous les autres scripts de migration de ce module,
et qui doit continuer à créer une révision).

Restructuration de figli_compta_ledger_node_presave() : le calcul de
l'écart et son stockage se font maintenant même sous skip_validation
(seul le lancement de l'exception reste conditionnel) -- vérifié que
l'exception se déclenche toujours normalement sur une vraie
répartition incohérente, et que Montant TTC/Cotisation restent
inchangés sur une sauvegarde qui ne touche ni HT ni TVA.
2026-09-08 13:37:39 +02:00
bachir 00956825fa Réduit les marges du tableau (côtés et dessous) aux 2/3
- Gin's .layout-container margin-left/right (48px) réduit à 16px, sur
  les routes de ce module (hide_admin_chrome, déjà route-scopé).
- #figli-home-app margin-bottom réduit de 1rem à 0,33rem.
- .figli-table-wrap : max-height 75vh remplacé par calc(100vh - 16rem)
  -- le pourcentage de viewport ne tenait plus compte du titre/menu +
  barre d'outils au-dessus (hauteur à peu près fixe, ~16rem), qui a
  grandi au fil des filtres ajoutés depuis le dernier fix du double
  scroll -- avait fini par redépasser ce que 75vh laissait de marge,
  réintroduisant un scroll de page en plus de celui du tableau (84px
  d'écart mesuré avant ce correctif, ~8px après -- l'essentiel de
  l'écart restant venant justement de la marge du dessous réduite
  ci-dessus, pas d'un nouveau débordement).
2026-09-07 14:54:06 +02:00
bachir 74ee8ab3b9 Badge TVA non officielle plus visible : gros liseret rouge sous le montant
Le point ambre discret n'était pas assez visible. Remplacé par un
border-bottom épais (3px, rouge) directement sur la cellule TVA --
lisible d'un coup d'œil sans avoir besoin de place horizontale
supplémentaire, contrairement à un badge texte/icône dans une colonne
de 3,5% de large.
2026-09-07 14:48:09 +02:00
bachir 6576b35c07 Badge sur les taux TVA non officiels, écart positif en vert
- Un point ambre discret (pas de texte -- la colonne TVA ne fait que
  3,5% de large, à peine assez pour "12,64 %" lui-même) marque tout
  taux TVA qui ne tombe sur aucun des 5 taux officiels français (0,
  2,1, 5,5, 10, 20 %, ±0,05 point comme dans les scripts de migration).
  Infobulle pour le détail.
- Écart positif (répartition > Montant HT) passe au vert
  (--figli-positive), au lieu du rouge uniforme précédent -- même
  logique que la colonne Montant HT, qui distingue déjà positif/négatif.
2026-09-07 14:45:06 +02:00
bachir 0baa03855b Le champ/colonne "1,1%" affiche le delta seul, pas HT + 1,1%
field_cotisation_urssaf stockait round(HT * 1.011, 2) (le montant
augmenté) -- "1,1%" comme libellé de champ/colonne désigne la
cotisation elle-même, pas HT + cotisation. Change pour
round(HT * 0.011, 2). La TVA continue de s'appliquer sur la base
augmentée (HT + ce champ) : Montant TTC est inchangé par cette
correction, seule la valeur affichée/stockée dans "1,1%" change.

Migration (update hook 8008) : recalcule field_cotisation_urssaf pour
les 154 lignes que figli_compta_ledger_update_8007() avait marquées
cotisation active, en soustrayant simplement le HT déjà correct --
contrairement à cette dernière, ce n'est pas un arbitrage sur des
données historiques ambiguës, juste un bug dans du code écrit plus tôt
le même jour, donc recalculé sans condition ni prudence particulière.

Ajoute aussi le total "1,1%" au pied du tableau (solde de l'année),
absent jusqu'ici : LedgerStatsController::totauxAnnee() somme
maintenant field_cotisation_urssaf comme il le fait déjà pour HT/TTC.
2026-09-07 14:38:10 +02:00
bachir 6c33bc0098 Cotisation diffuseur URSSAF (1,1%) : nouveau champ, TTC calculé en cascade
Architecture métier corrigée : la SAS devise HT, ajoute (quand
applicable -- pas systématique) 1,1% de cotisation diffuseur URSSAF
pour l'usage de freelances, puis applique la TVA sur ce montant
augmenté -- pas sur le HT brut. La répartition entre comptes reste
basée sur le HT seul, inchangée.

- field_cotisation_active (case à cocher, "Entrée client" uniquement,
  cochée par défaut sur une nouvelle ligne -- optionnelle puisque tous
  les devis ne l'incluent pas historiquement).
- field_cotisation_urssaf ("1,1%", montant calculé HT × 1,011, jamais
  éditable).
- Montant TTC = round(cotisation × (1 + TVA/100), 2) quand la
  cotisation s'applique, round(HT × (1 + TVA/100), 2) sinon --
  inchangé pour tous les types hors "Entrée client".

Migration (update hooks 8006/8007) : toutes les lignes "Entrée client"
de field_tva rétro-calculées par figli_compta_ledger_update_8005()
étaient fausses dès que la cotisation s'appliquait (taux mixte HT->TTC,
pas le vrai taux de TVA) -- corrigées avec la même prudence que la
migration précédente : 2021 laissée de côté (pratique non confirmée
sur cette année), et par ligne, un taux déjà "propre" (0/2,1/5,5/10/20
%) signifie qu'aucune cotisation n'a été appliquée (laissé tel quel) ;
sinon le nouveau taux recalculé n'est retenu que s'il retombe lui-même
sur un taux officiel, sinon la ligne reste inchangée plutôt que de
deviner. Montant TTC historique jamais réécrit, comme pour toute
migration de ce module. Résultat : 154 lignes corrigées (cotisation
active), 90 déjà correctes (cotisation inactive), 40 ambiguës laissées
telles quelles, 43 lignes 2021 ignorées.

Formulaire réorganisé en grille à 4 colonnes (HT | 1,1% | TVA | TTC),
case à cocher masquée hors "Entrée client" via #states réel (fiable
ici, contrairement au select TVA -- watch sur field_type_ligne, un
vrai champ, pas un élément ajouté à la main). Colonne "1,1%" ajoutée
au grand livre entre HT et TVA.
2026-09-07 14:33:20 +02:00
bachir 134c97ac9d Supprime le double scroll : menu à hauteur du titre, marge Gin retirée
Le menu Grand livre/Dashboard/Par compte passe en position fixe,
aligné avec le <h1> de la page plutôt que sur sa propre ligne en
dessous -- les deux viennent de régions Drupal différentes (le titre
du bloc sticky top-bar de Gin, le menu du contenu de la page) sans
conteneur flex/grid commun pour les aligner autrement.

Ça ne suffisait pas à éliminer le scroll de page en plus de celui du
tableau (max-height: 75vh sur .figli-table-wrap) : Gin applique un
margin-bottom: 80px sur <main class="page-content">, pensé pour une
page d'admin classique, pas pour ce layout à hauteur de viewport fixe.
Neutralisé sur les routes du module (hide_admin_chrome, déjà route-
scopé).

Au passage, corrige un oubli : figli_compta_ledger.dashboard_compte
n'était jamais dans la liste hide_admin_chrome, donc la barre d'admin
Gin restait visible sur /dashboard/compte (et donc le titre plus bas
que sur les 3 autres pages) -- ajouté.

Et sur demande complémentaire en cours de route : le même menu manquait
purement et simplement sur /lignes/historique (pas de template Twig
propre, juste un tableau brut) -- ajouté en tableau de rendu directement
dans HistoryController, mêmes classes CSS que le <nav> des autres pages.
2026-09-07 12:46:36 +02:00
bachir 897d3c208e htaccess 2026-09-07 12:26:48 +02:00
bachir e117089be5 Renomme HT/TTC, réduit leur largeur, ajoute une colonne TVA au grand livre
HT/TTC passent de "Montant HT"/"Montant TTC" à "HT"/"TTC" (5% de large
au lieu de 6,2%), avec une nouvelle colonne TVA entre les deux (3,5%,
affichée en %, arrondie à 2 décimales -- les lignes migrées portent
parfois un taux rétro-calculé à 4 décimales, voir
figli_compta_ledger_update_8005()). field_tva était déjà exposé sans
changement de requête JSON:API (pas de sparse fieldset ici).

Piège de spécificité CSS : .amount:not(.compte-col) (déjà en place
pour Écart) compte comme 2 classes à cause de :not(), donc un simple
.figli-ht-col à une classe perdait systématiquement contre elle --
recombiné en .amount.figli-ht-col pour égaler/dépasser sa spécificité.
2026-09-07 12:18:58 +02:00
bachir f0927bac88 TVA par défaut à 10 %, corrige le Montant TTC illisible
- TVA par défaut passe de 0 % à 10 % (taux intermédiaire) pour une
  nouvelle ligne -- la plupart des lignes saisies portent effectivement
  de la TVA.
- Montant TTC (désactivé) était illisible : Chromium affiche le texte
  d'un champ disabled via -webkit-text-fill-color plutôt que color,
  qui restait sur son gris par défaut du navigateur -- quasi invisible
  sur le fond gris de --flform-bg-subtle en mode sombre. Fixé
  explicitement sur les deux thèmes.
2026-09-07 12:01:33 +02:00
bachir 5aa00b2ffd TVA en sélection des taux officiels français, TTC non éditable
Le champ TVA devient un select (0 %, 2,1 %, 5,5 %, 10 %, 20 %) plutôt
qu'une saisie libre, avec une option "Autre (préciser)" qui révèle le
champ décimal existant -- indispensable pour les lignes migrées, dont
le taux rétro-calculé (figli_compta_ledger_update_8005()) est souvent
un taux effectif non standard qu'il ne faut surtout pas forcer à
s'aligner sur l'une des 5 valeurs officielles.

Montant TTC passe de readonly à #disabled : non focusable, non
éditable même via les flèches d'un input number, et Form API rejette
toute valeur soumise malgré tout au profit de #default_value.

Le select est un élément de formulaire à part (pas un widget de champ)
pour éviter que WidgetBase::extractFormValues() ne s'étouffe sur une
clé étrangère mêlée aux valeurs de field_tva -- un #validate callback
recopie la valeur choisie dans field_tva au moment opportun (après
tous les #validate, avant la reconstruction de l'entité au submit).

L'affichage/masquage du champ "Autre" repose sur du JS simple plutôt
que sur #states : #states pose bien l'attribut data-drupal-states mais
ne bascule jamais la visibilité dans cette modale AJAX précise, pour
une raison non identifiée (un #states pourtant fonctionnel existe
juste au-dessus, sur field_entree_liee, qui surveille un vrai champ
Field API plutôt qu'un select ajouté à la main).
2026-09-07 11:56:08 +02:00
bachir b7490eb76a Ajoute la TVA (%) et calcule automatiquement le Montant TTC
Architecture : Montant HT reste saisi à la main, un nouveau champ TVA
(%) le complète, et Montant TTC = round(HT * (1 + TVA/100), 2) devient
une valeur calculée plutôt que saisie -- champ readonly dans le
formulaire (aperçu live en JS), calcul faisant foi côté serveur dans
figli_compta_ledger_node_presave().

Migration (update hooks 8004/8005) : Montant TTC n'est **jamais**
modifié pour les données historiques -- seul un TVA effectif est
rétro-calculé depuis HT/TTC existants et ajouté en tant que nouvelle
métadonnée (1498 lignes renseignées, 58 laissées vides faute de TTC
source). Précision du champ TVA fixée à 4 décimales après vérification
empirique sur les 1556 lignes existantes : reconstruire TTC = HT * (1 +
TVA/100) avec un taux arrondi à 4 décimales ne s'écarte du TTC réel que
pour 9 lignes (probables factures multi-taux), contre 74 à 2 décimales.

Garde-fou supplémentaire dans le presave : le recalcul du TTC ne se
déclenche que si HT ou TVA ont réellement changé par rapport à la
révision précédente (comparaison à $node->original) -- sans ça,
rouvrir une ancienne ligne migrée pour corriger un simple libellé
aurait silencieusement dérivé son TTC historique de ±0,01€ à cause de
l'arrondi du taux rétro-calculé, ce qu'interdit la règle du projet de
ne jamais corriger les données historiques.

Réorganisation du formulaire : N° Facture rejoint Client sur une même
ligne, Montant HT/TVA/Montant TTC forment la ligne suivante -- les
poids de champs doivent rester des entiers (Drupal tronque silencieusement
tout poids fractionnaire lors de la sauvegarde de l'affichage).
2026-09-07 11:36:53 +02:00
bachir f00f680143 Corrige trois régressions du restyling du formulaire de ligne comptable
- Le select "Type de ligne" affichait un chevron géant répété (le
  shorthand `background: transparent` sur input/select/textarea
  réinitialisait aussi position/repeat/size de Claro, transformant sa
  flèche unique alignée à droite en motif carrelé). Les select gardent
  maintenant leur background Claro intact.
- Les menus "Toggle Actions" (⋮) de la Répartition étaient ouverts en
  permanence : mon display:flex écrasait le display:none par défaut de
  Claro, cassant le toggle piloté par paragraphs.actions.js. Rescopé à
  `.paragraphs-dropdown.open .paragraphs-dropdown-actions`.
- Marges resserrées : le `.form-item { margin-block: 1.5rem }` de Claro
  s'ajoutait à notre propre grid-gap. Neutralisé (`margin-block: 0`), et
  gap/paddings réduits.
2026-09-07 11:14:24 +02:00
bachir 4ce1b12099 Modernise l'UI du formulaire modal d'ajout/édition de ligne comptable
Le formulaire natif Drupal (Menu settings, URL alias, Authoring
information, tabledrag/drag-handle pour des tableaux de 1-3 lignes,
"Title" affiché tel quel) était pensé pour un éditeur de contenu
générique, pas pour la saisie numérique quotidienne d'une ligne
comptable. Sans toucher au Form API (validation, structure des champs
inchangées) :

- Grille compacte à 3 colonnes (Date+Type, Client, Montant HT/Facture/
  TTC en ligne), au lieu de l'empilement vertical par défaut.
- Menu settings / URL alias / Authoring information / Published
  masqués via #access -- jamais utilisés pour une ligne_comptable.
- "Title" relabellisé en "Libellé court" avec une description qui
  explicite son rôle de repli quand "Notes / détail" est vide (déjà le
  comportement de home.js/dashboard*.js, jusque-là invisible côté
  formulaire).
- Répartition / Entrée liée : poignée de glisser-déposer et bascule
  "Show row weights" masquées (l'ordre n'affecte jamais la somme ni
  l'affichage), boutons Ajouter/Retirer/Dupliquer restylés.

Classe CSS partagée `figli-ledger-form` ajoutée par form_alter plutôt
que de cibler la classe générée par Drupal, différente entre le
formulaire d'ajout (node-ligne-comptable-form) et celui d'édition
(node-ligne-comptable-edit-form).
2026-09-07 09:57:37 +02:00
bachir 9234050a68 Ajoute un filtre par tag de signalement sur /lignes et /dashboard/compte
Même modèle multiselect/OR que les filtres Compte et Type déjà en place,
en complément (pas en remplacement) du simple on/off "Signalées
uniquement" du grand livre. Sur /dashboard/compte, la liste d'options se
limite aux tags réellement présents pour le compte sélectionné, et se
réinitialise au changement de compte.
2026-09-06 21:59:32 +02:00
bachirandClaude Sonnet 5 11250bea77 Add solde footers + per-année small multiples to /dashboard/compte
1. "Entrées sur-versées" and "Versements sans entrée liée" tables get a
   solde footer, same convention as "Reste à verser" above them --
   reuses the totalSurVerse/totalNonLies computeds already backing the
   summary cards, no new computation.

2. "Répartition de l'activité par type" and "Top clients" keep their
   all-time chart, now followed by a small-multiples grid of the same
   chart per année. The type breakdown reuses
   total_par_type_par_compte_par_annee, a new field on
   DashboardStatsController::stats() built from the same répartition-
   level rows already fetched for total_par_type_par_compte (no extra
   query, just one more level of grouping in the PHP aggregation). Top
   clients per année is computed client-side from the same
   entreesDuCompte already used for the all-time version, capped to top
   5 (not 10) to keep the grid readable.

HBarChart gains a `compact` prop (narrower fixed columns, smaller text)
for use inside the small-multiples cards -- the all-time charts above
keep the full-width layout unchanged.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 20:41:11 +02:00
bachirandClaude Sonnet 5 ec2af0ea7e Add "Répartition de l'activité par type" chart to /dashboard/compte
Extends DashboardStatsController::stats()'s répartition-level SQL query
to also group by type (not just année/compte), producing a new
total_par_type_par_compte breakdown alongside the existing global one.
Unlike the reconciliation tables on this page (deliberately entrée/
versement only), this chart covers every type touching the selected
compte's répartition, matching what the general /dashboard already
shows for the whole ledger.

Fixed a latent bug the query change would otherwise have introduced:
solde_par_compte_par_annee[année][compte] used to be a 1:1 assignment
because each (année, compte) pair was unique in the old query -- adding
type to the GROUP BY means several rows can now share that same pair,
so it has to accumulate instead of overwrite (verified the accumulated
totals exactly match a query without the type split, so this preserves
existing behavior for the fields already in use).

HBarChart in dashboard-compte.js gained the same colorFor prop
dashboard.js's version already has (existing Top clients usage keeps
its default green, unaffected).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 20:35:10 +02:00
bachirandClaude Sonnet 5 bd251d0a5e Fix diverging year-bars chart: bars were pinned to the top, not the zero line
Root cause: .figli-year-bar was position: relative with an explicit
height (set inline by barStyle()), which opts it out of the flex
container's default stretch alignment -- so it rendered flex-start
(top) aligned first, and the top/bottom: 50% from barStyle() only
*offset* that already-top position instead of anchoring an edge to the
middle. Visibly: green (positive) bars piling up near the top instead
of growing upward from the zero line.

Fixed by wrapping each bar in a .figli-year-bar-slot that stays in the
normal flex flow (so horizontal side-by-side layout for two bars/year
still works) and giving the bar itself position: absolute, anchored
against the slot's full-height box -- that's what makes a 50% top/
bottom offset actually mean "the zero line" instead of "50% further
down/up from wherever flex already put it."

Verified geometrically in-browser: bars now straddle the track's
midpoint and extend outward in the correct direction (positive up,
negative down) on both "Entrées vs versements par année" and
"Évolution du solde".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 18:14:00 +02:00
bachirandClaude Sonnet 5 22f57379e5 Add solde footer to the "Lignes signalées" table on /dashboard/compte
Sum of the Montant column across every flagged line for the selected
compte, styled red when negative -- same convention as the other
reconciliation tables' footers on this page.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 18:10:25 +02:00
bachirandClaude Sonnet 5 c7c143e038 Revert: force jj/mm/aaaa on the ligne_comptable date field
Back to the native <input type="date"> widget, per request.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 18:07:52 +02:00
bachirandClaude Sonnet 5 463a7f343c Force jj/mm/aaaa on the ligne_comptable date field, regardless of browser locale
The native <input type="date"> widget's displayed digit order follows
the browser's own locale, not Drupal's site language -- an English-
locale browser was showing 07/26/2024. Switches the date sub-element to
'text' with an explicit PHP date format so Drupal parses/displays it
consistently for everyone.

Verified end-to-end (not just cosmetically): submitted 05/03/2024 through
the real add form and confirmed it stored as 2024-03-05 (5 March), not
2024-05-03 -- genuinely parsed as day/month, not silently reinterpreted
as month/day.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 18:03:18 +02:00
bachirandClaude Sonnet 5 775ee0955f Display /lignes dates as aa/mm/jj instead of jj/mm/aa
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 17:56:57 +02:00
bachirandClaude Sonnet 5 78661628fc Make the "Reste à verser" badge dark green instead of orange
Distinguishable from the default badge green (used for "Lié") while
staying green rather than orange -- it's an amount owed, not a warning.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:51:22 +02:00
bachirandClaude Sonnet 5 9ce5d87ace Surface signalement (flag) tags on the per-compte dashboard
Fetches field_flag alongside the existing entrée/versement data (same
JSON:API include list, same buildRows() shape as home.js's flags/
hasFlag). Two changes:

- Rows already shown in the reste-à-verser/sur-versé/non-liés tables
  get the same amber left-edge accent + tag badges as /lignes when
  they're also flagged.
- New "Lignes signalées" section lists every flagged entrée/versement
  for the selected compte, including ones that don't appear in any of
  the other tables -- a fully-settled line can still carry a flag for
  an unrelated reason (e.g. "client injoignable"), which none of the
  reconciliation-based tables would otherwise surface.

Verified live against an isolated test node plus two real flagged lines
already present in production data (client had already started using
the /lignes signalement feature) -- both the section and the inline
badges render correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 16:43:44 +02:00
bachirandClaude Sonnet 5 3f8767b4ef Narrow edit columns for wider amounts, modal-based signalement, fix scroll storm
Three related /lignes fixes:

1. Column widths: shrink Date/Facture/Libellé/Signalement (the four
   editable text columns) to free up room for the amount columns
   (Montant HT/TTC, the 8 compte columns, Écart), which were cramped.
   Date/Facture stay nowrap (already short: jj/mm/aa, Fxxxxxxx); Libellé/
   Signalement keep wrapping.

2. Signalement editing: replaced the comma-separated inline text input
   with a small modal -- one tag per line, each with its own remove
   button, plus an add field at the bottom. Clearer than parsing/
   retyping a whole comma list to drop one tag. Backend endpoint is
   unchanged (still takes a comma-joined value); only the front-end
   interaction model changed.

3. Scroll storm: a continuous scroll gesture fires many native 'scroll'
   events, and checkEdges() ran on every one of them -- each qualifying
   event queued its own loadOlder()/loadNewer() call (queuing, not
   dropping, was the previous session's fix for a *different* bug), and
   every queued call did a real fetch + scroll compensation regardless
   of whether an earlier one already moved the window away from the
   edge. That pileup is what looked like the same request firing over
   and over and dragged the scroll position around unpredictably.
   Fixed by guarding checkEdges() with the existing loadingOlder/
   loadingNewer flags so it stops queuing once one's already in flight.
   (A first attempt at this suppressed the compensation write's own
   resulting scroll event via a flag cleared on requestAnimationFrame --
   reproduced, live, the exact "stuck forever" failure already fixed
   once this session for the old rAF-based scroll throttle, because rAF
   doesn't reliably fire in this environment. Removed: turns out no
   suppression is needed at all, since that event finds loadingOlder
   already true and the checkEdges() guard blocks it on its own.)
   Also added a re-entrancy guard to pollForChanges(), which had no
   protection against a slow response overlapping with the next
   setInterval tick.

Verified with scripted scroll stress tests (dense bursts of 40-100
events, and repeated attempts to scroll back from the very top): exactly
one fetch per genuine edge crossing, no duplicate requests, scroll
position stays correctly anchored, never snaps back down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:47:34 +02:00
bachirandClaude Sonnet 5 fa28783bcc Fix table jitter, horizontal scroll, and vertical scroll snapping on /lignes
Root cause: the main table used auto layout, so every loadOlder()/
loadNewer() reflowed every column's width based on whatever was
currently loaded -- visibly shifting the table, spilling past the
viewport into a horizontal scrollbar, and corrupting loadOlder()'s
scroll-position compensation (which assumes the scrollHeight delta
after prepending rows is *only* the new rows' own height -- not true
once existing rows also reflow). That's what made scrolling up feel
like it kept snapping back down.

- table-layout: fixed with explicit per-column widths (percentages
  throughout, not mixed with rem -- mixing meant the rem columns' width
  was added on top of the percentage budget instead of coming out of
  it), plus box-sizing: border-box so padding doesn't inflate columns
  beyond their declared width.
- overflow-x: hidden instead of auto on the scroll container: with both
  x and y auto on the same element, the browser has to guess whether a
  vertical scrollbar will appear before laying out width: 100%, and a
  wrong guess understates available width by a scrollbar's worth --
  exactly enough to tip a tightly-fitting table into needing horizontal
  scroll too.
- Fixed a footer-row column count bug found along the way: it still had
  two actions-col cells and a colspan=5 label from before the link
  button was removed, leaving the label 6 columns short of Signalement
  and misaligning every footer cell after it.

Verified with a 25-round scripted scroll-up stress test spanning 3
loadOlder() triggers: scroll position stays correctly anchored near
where the user was looking, never jumps toward the bottom.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:16:06 +02:00
bachirandClaude Sonnet 5 eda79e75a3 Simplify /lignes row indicators: drop redundant link button and écart outline
- Remove the left-side link button (actions-col) -- the link status
  badge already does the same thing (view linked entrées, or open the
  link form when unlinked), so the button was pure duplication.
- Remove the red row outline on écart lines -- the Écart column itself
  (bold red text) already flags it, no need for a second row-level cue.
- Hide the link status badge specifically on "Hébergement" lines (both
  the main table and the drill-down modal) -- reconciliation math is
  untouched (hébergement sorties still count toward other rows' totals),
  this only suppresses the badge on hébergement's own row.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 15:00:29 +02:00
bachirandClaude Sonnet 5 34b1082991 Add signalement (flag) tags for problem lines on /lignes
New field_flag: free-tagging taxonomy reference (multi-value,
auto-create) on ligne_comptable, for marking a line with an
unstructured problem description (e.g. "client impayé") that can't be
detected automatically the way the répartition écart already is.

- New "Signalement" column, inline-editable like Client/Facture/Libellé
  (comma-separated tags, datalist autocomplete, server-side auto-create
  of unknown tags -- same pattern LedgerActionsController already used
  for Client, now shared via findOrCreateTerm()).
- New "Signalées uniquement" filter, mirroring "Écarts uniquement".
- Flagged rows get a distinct amber left-edge accent (box-shadow, not
  border) so a row that's both in écart and signalée shows both
  indicators without one overwriting the other.
- Native node add/edit form gets the field for free via core's
  entity_reference_autocomplete_tags widget.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:55:46 +02:00
bachirandClaude Sonnet 5 ceaf301bf0 Add per-compte dashboard: reste à verser between entrées and versements
New /dashboard/compte page, one compte associé (freelance) at a time via
a selector. Focused on entree/versement only (not achat/hébergement/
sous-traitant): reuses the same entrée<->versement reconciliation
algorithm as the /lignes badges (a versement can settle several entrées
at once, split equally), aggregated per compte to surface outstanding
"reste à verser", over-paid entrées, and versements with no linked
entrée at all -- plus solde/entrées-vs-versements/top-clients charts for
context.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 14:29:27 +02:00
bachirandClaude Sonnet 5 3404ee14c3 Fix sliding window silently stalling under multi-filter URL hashes
Restoring several filters at once from the URL hash (client + type)
fired several concurrent loadOlder()/loadNewer()/ensureScrollable()
chains. They raced on the loadingWindow guard, which silently dropped
a call arriving mid-flight instead of queuing it -- indistinguishable
from "nothing more to load", so the sliding window gave up expanding
after one round even when the table was still far from scrollable,
with no scroll events left to ever retry it.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-06 13:06:02 +02:00
bachir 71fde0f3c0 Add live polling and optimistic locking for concurrent inline edits
Two-part fix so multiple people can edit /lignes at once without
clobbering each other, and see each other's changes without a manual
reload -- no Socket.io/websocket infra, just what fits the existing
fetch-based architecture:

1. Polling (POLL_INTERVAL_MS = 8s): pollForChanges() asks JSON:API for
   any ligne_comptable changed since the last check (filtering on the
   `changed` field -- confirmed live that JSON:API only accepts a raw
   Unix timestamp for this, not the ISO string it returns in responses,
   silently matching everything otherwise) and mergeChangedRows()
   patches matching rows in place via Object.assign (not a `rows`
   reassignment, so it doesn't re-trigger the reconciliation-resolution
   watcher for routine polls). A row currently being edited is skipped
   entirely rather than overwritten out from under an in-progress
   keystroke.

2. Optimistic locking: every row now carries its `changed` timestamp,
   sent back on every inline edit (updateType/updateField). A new
   checkConflict() compares it against the node's actual changed time
   before saving and rejects with 409 if they differ -- someone else
   saved this exact line in between. On a 409, refreshSingleRow()
   re-fetches just that node and patches it in place so the view
   self-corrects instead of staying stuck on the stale state that
   caused the rejection.

Verified live end-to-end against an isolated temporary test node (not
real data): an external edit correctly appeared in the browser within
one poll cycle with no reload; a save using a stale `changed` value was
rejected with the conflict error, confirmed via direct DB query that it
left the node's data completely untouched, and the view auto-corrected
to show the other edit. Test node and its paragraph fully cleaned up
afterward.
2026-09-06 12:49:56 +02:00
bachir 0c893b1b24 Show the exact solde amount directly in the versement badge
The reste-à-verser/sur-versé badge only showed the word, with the exact
figure available in the hover title -- had to open the drill-down modal
just to see how much. linkStatus() now exposes the computed amount
(montant) alongside kind/detail, and linkStatusLabel() takes the whole
status object to format it inline: "Reste à verser : 440,00 €",
"Sur-versé : 710,00 €", "Lié : 0,00 €" for a fully settled one.

Verified live: all three cases render with the exact figure in both the
main table and the drill-down modal.
2026-09-06 12:32:41 +02:00
bachir fc89ae6232 Resolve versement reste-à-verser/sur-versé from the server proactively
The badge on a versement freelance row previously only got the correct
status once someone clicked it -- before that it could show "inconnu"
(or, worse, silently default to "ok") whenever its linked entrée wasn't
in the currently loaded sliding window, which is common: the entrée
that got paid can be dated years before or after the versement itself.

Also fixed a latent bug in the click path itself: toggleEntreeFilter()
passed the *entrée's* id to loadEntreeGroup(), which needs an
already-loaded row to find a starting nid for the server-side query --
if that entrée wasn't loaded (the exact case this is all about), the
lookup silently found nothing and did nothing. Split the fetch+merge
logic into fetchGroupFromNid() and let loadEntreeGroup() accept a
fallbackNid (the versement's own, always loaded) to start the
traversal from when the entrée itself isn't available locally --
LedgerStatsController::groupeEntree() finds the same connected
component either way.

New ensureLinkedReconciliationResolved(), triggered by a `rows` watcher
after every load/loadOlder/loadNewer, calls the same server-backed
resolution for every visible linkable row instead of waiting for a
click, deduplicated via resolvedLinkGroups so rows sharing an entrée
don't each trigger their own fetch.

Verified live: the LA MINE versement (own date 2022, linked entrées
dated 2023 and 2025) now shows "⚠ Sur-versé" immediately on page load
with zero clicks, where before it required manually opening the badge
first. Drill-down modal and the "Non liée" -> link-form click path both
still work; no console errors on repeated fresh loads.
2026-09-06 12:21:53 +02:00
bachir 18cb4cc3c8 Auto-focus click-to-edit fields instead of requiring a second click
Type/Client/Facture/Libellé all swap a span for an <input>/<select> via
v-if -- Vue doesn't focus a newly created element on its own, so the
first click only revealed the field and a second click was needed to
actually type into it. Added a v-focus directive (mounted() calls
el.focus() + el.select(), firing exactly once per v-if true-flip) to
all four.

Verified live: a single click on each of the four editable cells now
moves document.activeElement into the field immediately.
2026-09-06 11:14:02 +02:00