Files
drupal-figli-compta/web/modules/custom/figli_compta_ledger/css/home.css
T
bachir 0314625593 Client autocomplete, drill down from versements, multi-entrée linking
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.
2026-09-05 11:35:18 +02:00

365 lines
10 KiB
CSS

/* Self-contained colors (not relying on Gin CSS custom properties, which
don't reliably resolve on this route) + explicit dark-mode overrides,
since Gin's dark mode (html.gin--dark-mode) is enabled by default here. */
#figli-home-app {
--figli-bg: #ffffff;
--figli-bg-alt: #f5f6f8;
--figli-text: #1a1a1a;
--figli-text-light: #6b7280;
--figli-border: #dcdee2;
--figli-error: #c9312b;
--figli-positive: #1a7f37;
--figli-col-hover: rgba(15, 23, 42, 0.05);
font-family: Inter, -apple-system, sans-serif;
margin: 1rem 0;
color: var(--figli-text);
}
html.gin--dark-mode #figli-home-app {
--figli-bg: #232426;
--figli-bg-alt: #2c2d30;
--figli-text: #e8e9ea;
--figli-text-light: #a1a5ab;
--figli-border: #3d3e42;
--figli-error: #ff6b6b;
--figli-positive: #4ade80;
--figli-col-hover: rgba(255, 255, 255, 0.07);
}
#figli-home-app .figli-toolbar {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.75rem 1.25rem;
margin-bottom: 1rem;
padding: 0.75rem 1rem;
background: var(--figli-bg-alt);
border: 1px solid var(--figli-border);
border-radius: 6px;
color: var(--figli-text);
}
#figli-home-app .figli-toolbar label {
display: flex;
flex-direction: column;
font-size: 0.75rem;
font-weight: 600;
color: var(--figli-text-light);
gap: 0.15rem;
}
#figli-home-app .figli-toolbar select {
font-size: 0.85rem;
padding: 0.2rem 0.4rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
}
#figli-home-app .figli-client-input {
font-size: 0.85rem;
padding: 0.2rem 0.4rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
width: 12rem;
}
#figli-home-app .figli-checkbox {
flex-direction: row !important;
align-items: center;
gap: 0.35rem !important;
}
#figli-home-app .figli-count {
margin-left: auto;
font-size: 0.8rem;
color: var(--figli-text-light);
white-space: nowrap;
}
#figli-home-app .figli-table-wrap {
overflow-x: auto;
border: 1px solid var(--figli-border);
border-radius: 6px;
max-height: 75vh;
overflow-y: auto;
}
#figli-home-app table {
width: 100%;
border-collapse: collapse;
background: var(--figli-bg);
color: var(--figli-text);
font-size: 0.8rem;
white-space: nowrap;
}
#figli-home-app th,
#figli-home-app td {
text-align: left;
padding: 0.3rem 0.6rem;
border-bottom: 1px solid var(--figli-border);
color: var(--figli-text);
}
#figli-home-app thead th {
position: sticky;
top: 0;
background: var(--figli-bg-alt);
color: var(--figli-text);
font-weight: 600;
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.02em;
z-index: 2;
}
#figli-home-app td.amount,
#figli-home-app th.amount {
text-align: right;
font-variant-numeric: tabular-nums;
}
#figli-home-app td.figli-libelle {
white-space: normal;
min-width: 220px;
max-width: 340px;
}
#figli-home-app tr.figli-group-row td {
background: var(--figli-bg-alt);
color: var(--figli-text);
font-weight: 700;
position: sticky;
top: 2rem;
z-index: 1;
}
#figli-home-app tfoot {
position: sticky;
bottom: 0;
/* Higher than thead (2) and the sticky group-row headers (1) -- without
this, tfoot had no explicit z-index (auto), so a group-row header
scrolling underneath it painted on top instead of behind. */
z-index: 3;
}
#figli-home-app tr.figli-totals-row td {
background: var(--figli-bg-alt);
color: var(--figli-text);
font-weight: 700;
border-top: 2px solid var(--figli-border);
border-bottom: none;
}
#figli-home-app td.figli-solde-crediteur {
color: var(--figli-positive);
}
#figli-home-app td.figli-solde-debiteur {
color: var(--figli-error);
}
/* Footer totals row sets its own `color` (higher specificity: tr.class +
td beats a lone td.class) which otherwise overrides the solde colors
above -- repeat them scoped to the row so red/green still wins there. */
#figli-home-app tr.figli-totals-row td.figli-solde-crediteur {
color: var(--figli-positive);
}
#figli-home-app tr.figli-totals-row td.figli-solde-debiteur {
color: var(--figli-error);
}
/* Single colored + bold anchor per row (Montant HT only) so entrées vs
sorties are scannable at a glance -- everything else in the row (TTC,
the 8 compte columns) stays neutral, not a red/green garland. */
#figli-home-app td.figli-montant-positif {
color: var(--figli-positive);
font-weight: 700;
}
#figli-home-app td.figli-montant-negatif {
color: var(--figli-error);
font-weight: 700;
}
/* Error rows: a thin red outline around the row, not a background fill --
easier to read, doesn't fight with dark mode. */
#figli-home-app tr.figli-error-row td {
border-top: 1px solid var(--figli-error);
border-bottom: 1px solid var(--figli-error);
}
#figli-home-app tr.figli-error-row td:first-child {
border-left: 1px solid var(--figli-error);
}
#figli-home-app tr.figli-error-row td:last-child {
border-right: 1px solid var(--figli-error);
}
#figli-home-app td.figli-ecart {
color: var(--figli-error);
font-weight: 700;
}
/* Column highlight to pair with the row hover, forming a crosshair over
the hovered cell. box-shadow (not background) so it layers on top of
whatever the cell already has -- sticky header/footer backgrounds,
error-row outlines -- instead of overwriting them. */
#figli-home-app td.figli-col-hover,
#figli-home-app th.figli-col-hover {
box-shadow: inset 0 0 0 9999px var(--figli-col-hover);
}
#figli-home-app .figli-badge {
display: inline-block;
padding: 0.1rem 0.45rem;
border-radius: 10px;
font-size: 0.68rem;
font-weight: 600;
white-space: nowrap;
}
#figli-home-app .type-entree { background: #1a7f371a; color: var(--figli-positive); }
#figli-home-app .type-charge { background: #6b72801a; color: var(--figli-text-light); }
#figli-home-app .type-versement { background: #d97a0a1a; color: #d97a0a; }
#figli-home-app .type-achat { background: #1d4ed81a; color: #3b6fe0; }
#figli-home-app .type-hebergement { background: #0e91821a; color: #0e9182; }
#figli-home-app .type-ouverture { background: #7c3aed1a; color: #9061f0; }
#figli-home-app .type-sous_traitant { background: #c9312b1a; color: #c9312b; }
#figli-home-app .type-salaire_stage { background: #0891b21a; color: #0891b2; }
#figli-home-app .type-charges_local_pro { background: #65a30d1a; color: #65a30d; }
#figli-home-app .type-autre { background: #6b72801a; color: var(--figli-text-light); }
/* Click-to-edit type badge -- swaps for a native <select> in place
(figli-type-select below), no modal needed for this one field. */
#figli-home-app .figli-badge-editable {
cursor: pointer;
border: 1px solid transparent;
}
#figli-home-app .figli-badge-editable:hover {
border-color: currentColor;
}
#figli-home-app .figli-type-select {
font-size: 0.72rem;
padding: 0.05rem 0.2rem;
background: var(--figli-bg);
color: var(--figli-text);
border: 1px solid var(--figli-border);
border-radius: 4px;
}
#figli-home-app .figli-note {
color: var(--figli-text-light);
font-weight: 400;
font-size: 0.75rem;
}
/* One column per icon (link, edit) rather than one shared cell -- with a
shared cell, text-align: center puts a lone button (rows with no link
icon) in a different spot than a row with both, so the edit pencil
never lined up from row to row. Separate columns line up by
construction, and get a tight, icon-sized width instead of the
table's default 0.6rem horizontal cell padding. */
#figli-home-app td.actions-col,
#figli-home-app th.actions-col {
width: 1%;
padding: 0.2rem 0.1rem;
text-align: center;
white-space: nowrap;
}
/* Buttons stay inline (not flex) inside the <td> -- display: flex on a
table cell breaks the table's own column-width calculation (observed:
a ~55px gap opening up between this column and the next, and the
buttons rendering outside their cell's actual boundary). */
#figli-home-app .figli-edit-btn,
#figli-home-app .figli-link-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.4rem;
height: 1.4rem;
padding: 0;
background: transparent;
border: 1px solid transparent;
border-radius: 4px;
color: var(--figli-text-light);
cursor: pointer;
vertical-align: middle;
}
#figli-home-app .figli-edit-btn:hover,
#figli-home-app .figli-link-btn:hover {
background: var(--figli-bg-alt);
border-color: var(--figli-border);
color: var(--figli-text);
}
#figli-home-app .figli-link-btn.is-linked {
color: #3b6fe0;
}
/* Reconciliation badge on entrée rows: répartition (money owed) vs the
combined répartition of every linked sortie (money actually paid out). */
#figli-home-app .figli-recon-badge {
display: inline-block;
margin-left: 0.4rem;
padding: 0.05rem 0.4rem;
border-radius: 10px;
font-size: 0.68rem;
font-weight: 600;
white-space: nowrap;
background: #1a7f371a;
color: var(--figli-positive);
}
/* Not every badge using this class does something on click (the
ouverture/clôture écart badge is purely informational) -- only show
the pointer cursor where a click actually goes somewhere. */
#figli-home-app .figli-recon-badge.is-clickable {
cursor: pointer;
}
#figli-home-app .figli-recon-badge.is-reste {
background: #d97a0a1a;
color: #d97a0a;
}
#figli-home-app .figli-recon-badge.is-anomalie {
background: #c9312b1a;
color: var(--figli-error);
}
#figli-home-app .figli-clear-drilldown {
font-size: 0.8rem;
padding: 0.2rem 0.6rem;
}
/* Sliding-window edge markers (IntersectionObserver targets) -- kept
short so they don't add visible dead space when idle, tall enough
(min-height) to reliably intersect the observer's root margin. */
#figli-home-app tr.figli-sentinel-row td {
padding: 0.3rem 0.6rem;
min-height: 1.5rem;
text-align: center;
color: var(--figli-text-light);
font-size: 0.75rem;
font-style: italic;
}
#figli-home-app .figli-error {
background: #fde8e8;
border: 1px solid #f4a3a3;
color: #7a1a1a;
padding: 0.75rem 1rem;
border-radius: 6px;
}
/* Dismissible, doesn't replace the table like the top-level fetch error
does -- a failed inline type change is a small hiccup, not a reason to
hide everything that's already loaded. */
#figli-home-app .figli-inline-error {
margin-bottom: 1rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 0.75rem;
}