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>
This commit is contained in:
2026-09-06 15:47:34 +02:00
co-authored by Claude Sonnet 5
parent fa28783bcc
commit 3f8767b4ef
3 changed files with 177 additions and 42 deletions
@@ -219,18 +219,7 @@
</template>
</td>
<td class="figli-flag-cell">
<input
v-if="isEditingCell(item, 'flag')"
v-focus
type="text"
class="figli-inline-input"
list="figli-flag-datalist"
:value="item.flags.join(', ')"
@change="saveCell(item, 'flag', $event)"
@blur="editingCell = null"
@keyup.enter="$event.target.blur()"
/>
<span v-else class="figli-editable-cell" title="Cliquer pour signaler un problème" @click="startEditCell(item, 'flag')">
<span class="figli-editable-cell" title="Cliquer pour gérer le signalement" @click="openFlagModal(item)">
<template v-if="item.flags.length"><span v-for="f in item.flags" :key="f" class="figli-flag-badge">{{ f }}</span></template>
<template v-else>—</template>
</span>
@@ -331,5 +320,32 @@
</div>
</div>
</div>
<div v-if="flagModalItem" class="figli-modal-backdrop" @click.self="closeFlagModal">
<div class="figli-modal figli-flag-modal">
<div class="figli-modal-header">
<h3>Signalement -- {{ formatDate(flagModalItem.date) }}{{ flagModalItem.client ? ' · ' + flagModalItem.client : '' }}</h3>
<button type="button" class="figli-modal-close" @click="closeFlagModal">✕ Fermer</button>
</div>
<div class="figli-flag-modal-body">
<p v-if="!flagModalItem.flags.length" class="figli-note">Aucun signalement pour cette ligne.</p>
<div v-for="f in flagModalItem.flags" :key="f" class="figli-flag-modal-row">
<span class="figli-flag-badge">{{ f }}</span>
<button type="button" class="figli-flag-remove" title="Retirer ce signalement" @click="removeFlagTag(f)">✕</button>
</div>
<div class="figli-flag-modal-add">
<input
v-focus
type="text"
list="figli-flag-datalist"
placeholder="Ajouter un signalement…"
v-model="flagModalNewTag"
@keyup.enter="addFlagTag"
/>
<button type="button" class="button" @click="addFlagTag">Ajouter</button>
</div>
</div>
</div>
</div>
</div>
{% endverbatim %}