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.
- 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).
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.
- 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.
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.
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é.
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>
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>
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>
- 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>
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>
Facture and libellé rendered their span with no fallback text when
empty, unlike Client's existing "|| '—'" -- an empty span has zero
width/height, so there was nothing there to click at all. Added the
same "—" placeholder to both.
Also switched .figli-editable-cell to display: block with a min-height,
so the clickable area fills the whole cell's content box instead of
just hugging the text -- a lone "—" character was still a tiny target
even with a placeholder.
New POST /lignes/{node}/champ endpoint (LedgerActionsController::updateField(),
whitelisted to client/facture/libelle -> field_client/field_numero_facture/
field_notes) mirrors updateType(): skips the répartition invariant check
for this save (client/facture/libellé never touch montant_ht or
field_repartition, so it can only ever leave a pre-existing historical
mismatch as it was, never introduce one), wrapped in the same
skip_validation state flag with a try/finally.
Client resolves the typed text against existing "Client" taxonomy terms
only (same known-names list the toolbar's Client filter already offers
via a datalist) -- a non-match is rejected with a clear error rather
than silently creating a new term from a typo.
Frontend mirrors the existing editingTypeId/startEditType/saveType
pattern exactly, generalized to any of the three fields via a single
{id, field} editingCell state.
Verified live: editing all three fields on a row with a known
répartition écart succeeds (bypass confirmed), an unknown client name
is rejected with a visible error and the display value stays unchanged,
and the database was confirmed clean of test artifacts afterward.
A native <select multiple> can't collapse to a single-line trigger and
open as an overlay on hover -- it's either an always-open multi-row
list box or a single-value dropdown, not both. Replaced with a custom
dropdown: a trigger showing the current selection ("Tous" / "Maud,
Ouidade"), and a checkbox list shown on hover via a plain CSS :hover
rule (no open/close state needed). Checkboxes instead of ctrl/shift+click,
but same result -- several comptes/types at once -- with a more
discoverable interaction (no keyboard modifier to know about).
Also added a "✕ Effacer ce filtre" button next to Compte/Client/Type/
Année, shown only once that filter is active. Directly addresses the
Client filter specifically being painful to clear (select the text,
delete, press enter) -- one click now.
Compte and Type were single-value <select> elements. Converted both to
<select multiple> -- Vue binds those to an array natively, so
ctrl/cmd+click and shift+click just work with no custom JS. Matching
semantics are OR within a filter (any of the selected comptes/types) and
AND across filters, same as the existing client/year/écarts filters.
The URL hash (compte=Maud,Bachir&type=versement,achat) now carries
comma-separated lists instead of a single value, so the shareable/
reloadable filtered view still works with multiple selections.
900px was way too narrow for 16 columns forced to nowrap. Widened the
modal to 96vw, let cells wrap instead of forcing nowrap, and gave
columns proportional widths (table-layout: fixed otherwise spread all
16 evenly, squeezing the libellé text into the same width as an empty
compte column).
Replacing the main table's rows with the filtered group made the browser
clamp scrollTop to 0 the moment the drill-down shrank the visible content,
so closing it never returned to where the user had been scrolled. A modal
overlay leaves the main table (and its scroll position) untouched entirely.
Three related changes to the /lignes table:
1. The "Client" filter is now a text input with a <datalist> instead
of a <select> -- 50+ clients made the dropdown unwieldy. v-model.lazy
(not the default per-keystroke binding) since a change here triggers
ensureScrollable() and a hash rewrite, which shouldn't fire on every
character typed.
2. Clicking a "versement freelance" row's own status badge (Non liée /
Reste à verser / Sur-versé) now drills down the same way an entrée's
"N sorties liées" badge already did, instead of only being clickable
from the entrée side.
3. field_entree_liee is now multi-value (cardinality unlimited) --
sometimes one payment covers several client invoices at once.
LinkEntreeForm uses #tags => TRUE (a single comma-separated
autocomplete field, Drupal's field-API-native multi-value shape on
submit, no manual tag parsing needed). This is the deeper change and
touches most of the reconciliation logic in home.js:
- buildRows() reads field_entree_liee as an array
(entreeLieeIds/entreeLieeLabels) -- JSON:API always returns a list
for a multi-cardinality relationship now, even with 0 or 1 items.
- sortiesByEntree indexes a sortie under every entrée it links to.
- reconciliationByEntree splits a multi-linked sortie's répartition
equally across its linked entrées -- there's no per-link amount to
divide by, so equal split is the least-wrong assumption available
rather than counting the sortie's full amount against every linked
entrée (which would double-count the same money).
- versementStatus() sums residuals across all of a versement's linked
entrées for its own compte(s), skipping any not in the currently
loaded window (same accepted trade-off reconciliationByEntree
already had).
- The drill-down (filterEntreeId) is now filterEntreeGroup, a
transitive closure over shared entrée<->sortie links -- clicking
one entrée (or, per #2, one versement) surfaces every other entrée
it's connected to through a shared sortie, and every sortie linked
to any of them, not just the originally-clicked one's direct links.
Verified end-to-end: linked a real unlinked versement to two entrées
for the same client via the actual form submission (no manual DB
edit), confirmed both persisted, confirmed the link button's tooltip
lists both, and confirmed clicking either the versement's or an
entrée's badge produces the same 3-row connected group with correct
drill-down footer totals. Reverted the test link afterward.
Too much visual noise -- the badge alone already says everything
needed. Removes the now-unused --figli-warning custom property along
with the outline rule.
For "versement freelance" rows specifically: an amber outline plus a
badge when the versement either isn't linked to any entrée client at
all ("Non liée"), or is linked but reconciliationByEntree still shows
a residual on that entrée ("Reste à verser" / "Sur-versé", with the
amount in the tooltip). The residual belongs to the entrée as a whole,
not to any one sortie -- when several versements share an entrée, each
shows the same aggregate figure, since there's no way to attribute the
shortfall to one specific payment. No highlight when the linked entrée
isn't in the currently loaded window (can't tell either way -- same
accepted trade-off as reconciliationByEntree itself).
Verified: filtering to "Versement freelance" shows 21 of 25 loaded
rows flagged (17 unlinked, 4 with a reste-à-verser residual), the
remaining 4 fully-reconciled rows correctly unflagged, and the amber
outline resolves correctly in dark mode.
Three new field_type_ligne values (config/sync +
figli_compta_ledger.install for fresh-install parity), wired through
every place that enumerates the type list: LedgerActionsController's
inline type-change endpoint, home.js's type dropdown/badge, dashboard.js's
per-type chart, home.css's badge colors.
Sous-traitant is linkable to field_entree_liee (pays out against a
client's work, like versement/achat); salaire/stage and charges local
pro are not (structural costs, like charge). Turns out 13 lines
already carried "salaire_stage" and 1 "sous_traitant" as raw field
values from the historical migration -- list_string doesn't enforce
allowed_values at the storage level, so they saved fine but had no
label and weren't selectable in the UI until now.
Also fixes a real bug this surfaced: filtering /lignes by a sparse
type (e.g. "Autre") silently broke the sliding window -- so few rows
matched that the table no longer overflowed, so it never fired another
'scroll' event, so loadOlder()/loadNewer() never ran again ("les lignes
antérieures ne chargent plus"). Added ensureScrollable(), which keeps
extending the window in both directions whenever a thinning filter
(compte/client/type/écarts) leaves too little to scroll, and widened
the trim cap while filtering (MAX_LOADED_MONTHS_FILTERED) since the
normal 30-month cap actively fights a sparse filter -- extending one
end and immediately trimming the other nets out to nearly the same
slice every round. Bounded by a round counter rather than "did the
window stop moving": addMonths() uses Date#setMonth(), which isn't
invertible for month-end dates, so the window can drift indefinitely
in tiny steps without ever exactly repeating.
Verified: filtering by "Autre" now finds 24 matches (was stuck at 1)
and the view becomes scrollable within ~17s, settling cleanly rather
than hanging.
Clicking a row's type badge swaps it for a native <select> in place;
picking a new value POSTs to a new endpoint
(LedgerActionsController::updateType) instead of opening the full
edit modal for this one field.
The endpoint goes through the normal node save() lifecycle, so
figli_compta_ledger_node_presave() still forces a proper revision and
still enforces the répartition invariant -- nothing here bypasses
that. It also clears a stale field_entree_liee when the new type is
no longer linkable (versement/achat/hébergement), mirroring the full
form's #states visibility rule. CSRF-protected via core's own
/session/token, scoped to CsrfRequestHeaderAccessCheck::TOKEN_KEY to
match what that endpoint actually generates. Verified end-to-end via
the real click flow: correct revision (user + timestamp), correct
optimistic UI update, correct field_entree_liee clearing, and 400/403
on invalid type / missing CSRF respectively.
The generic totals-row rule (tr.figli-totals-row td) has higher
specificity than the lone td.figli-solde-crediteur/-debiteur rules, so
its neutral color always won in the footer even though soldeClass()
was already applying the right class there.
With 5+ years of migrated history, loading every ligne up front took
~40s. The table now only ever holds a date-range window (~18 months
around today by default), extended by 6 months when scrolling near
either edge and trimmed from the far end past a 30-month cap. The
totals footer and "Année" filter can't be answered from a partial
window, so they're backed by two new small endpoints
(LedgerStatsController) instead: per-compte totals for whichever year
is currently scrolled into view, and the distinct list of years with
data.
Single anchor per row -- Montant TTC and the 8 compte columns stay
neutral, so the table doesn't turn into a red/green garland. No 0.5€
threshold like soldeClass (used for aggregate totals): individual
lines are often small (a -1.07€ OVH renewal shouldn't read as
"neutral"), any nonzero sign gets colored.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
tfoot had position: sticky but no explicit z-index (auto/0), while the
sticky month/year group-row headers use z-index: 1 -- so a group row
scrolling past the bottom painted on top of the totals footer instead
of behind it. z-index: 3 (above thead's 2 and the group-row's 1).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Link and edit icons get one dedicated column each instead of sharing
one text-align: center cell -- with a shared cell, a row with only the
edit icon (non-linkable types) centered differently than a row with
both icons, so pencils never lined up vertically across rows.
Separate columns line up by construction. Verified: identical left
offset (141px) for the edit icon across 15 consecutive rows.
Also: tighter cell padding (was using the table's default 0.6rem
horizontal padding, way more than an icon needs) and no column-hover
crosshair on these technical columns -- nothing meaningful to compare
across rows there.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
display: flex on td.actions-col was fighting the table's own
column-width calculation -- a ~55px gap opened up between the actions
column and Date, and the buttons rendered outside their cell's actual
boundary. Buttons go back to sitting inline (already inline-flex
individually) inside a plain table-cell, spaced with a margin on the
first one instead of a flex gap on the container.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
New field_entree_liee (entity reference, node -> node) on
ligne_comptable, restricted to entrée-type nodes via a custom
EntreeClientSelection plugin -- narrows further to the same client as
the sortie being linked when one is already known, using the
referencing-entity context Drupal's selection handler API passes
through (getSelectionHandler($field, $entity)).
Quick-link UI (per the associates' explicit ask: no need to open the
full ligne_comptable form just for this):
- A chain-link icon next to the edit pencil on versement/achat/
hébergement rows (the only types that pay out against a client
invoice) opens LinkEntreeForm, a one-field AJAX modal, reusing the
same modal/close-on-save plumbing as the edit form. Filled/colored
when already linked, with the linked entrée's label on hover.
Also present (states-hidden unless one of those three types is
selected) on the full node form for whoever's already there anyway.
- Entrée rows get a reconciliation badge once at least one sortie
links back to them, clickable to drill the table down to just that
entrée and its linked sorties.
Conformity check assumes multi-compte répartition on both sides (an
entrée's répartition and each linked sortie's répartition can each
split across several comptes -- confirmed this is the real shape of
"hébergement" sorties, e.g. OVH/HETZNER renewals split across all 8
comptes, even though versement/achat lines happen to always be
single-compte in the current data). Per compte, compares the entrée's
répartition share (positive, owed) against the summed répartition of
every linked sortie (negative, paid) -- a residual near zero means
settled, positive means still owed ("reste à verser"), negative means
overpaid ("sur-versé", flagged for a closer look).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Pencil icon opens the existing node edit form in the same AJAX modal
as "+ Ajouter une ligne" -- no new form logic, reuses the form_alter
validation/close-on-save already in place for the add form.
Column highlight pairs with the existing row hover (from Gin's global
table CSS) to form a crosshair. Column position is computed logically
(accounting for colspan) rather than via DOM cellIndex, since the
totals row's label cell spans 4 columns and would otherwise misalign
every column after it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
27 lines (18 previously "charge", 9 "versement") that touch the EXT.WEB
compte in their répartition are now typed "hebergement" instead -- makes
Bachir's separate hosting activity filterable/visible as its own category
rather than blended into general charges/versements. Amounts/répartition
untouched, only the classification changed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- tfoot row: sum per compte (créditeur/débiteur colored) for the
currently filtered rows, plus HT/TTC/écart totals
- Fixed a real bug: fetchAllLignes() paginated without a unique sort key
(field_date_ligne alone, many ties), which let Drupal's JSON:API return
the same row on two pages -- silently inflating totals (Bachir showed
-3115,38€ instead of -3013,56€). Now sorts by
field_date_ligne,drupal_internal__nid (home) / drupal_internal__nid
(dashboard), plus a defensive client-side de-dup by node id either way.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Gin dark mode (html.gin--dark-mode) is on by default and the previous CSS
relied on Gin custom properties that didn't resolve here, causing
black-on-black/white-on-white text. Now uses self-contained explicit
colors with a dark-mode override block. Error rows get a thin red border
instead of a background fill, as requested.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- New /lignes route (set as site front page): full line-by-line table of
every ligne comptable, Vue app with filters (compte/client/type/année)
and month/year grouping, columns matching the original spreadsheet
(one per compte). Rows with répartition ≠ montant HT are visibly
flagged (red row + écart column), not hidden or auto-corrected.
- /dashboard now only holds the aggregate solde-par-compte/par-client view
- Migrated all 199 real 2026 transaction lines + 9 opening balances (from
REPORT CLOTURE 2025) via a drush import script, preserving raw source
data (known répartition mismatches included) -- validated with a new
state-flag bypass of the presave check, used only for historical import
- Added "Autre" as an allowed field_type_ligne value for edge-case rows
- Client taxonomy grew from 15 seeded terms to the full unified list
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>