Reports: an Accounts view, seller grouping, and an even header - #30
Merged
Conversation
…oggles Three asks, one seam. buildReport already collapsed transactions onto a subject and ranked them; all three of these are new subjects, so the `view === 'categories'` branch becomes a `grain` — transaction, seller, category, or account — and the rollup path is shared by the three that roll up. A transaction with nothing on the end being ranked is bucketed, never dropped, the same way an uncategorised one always was. - View gains Accounts: one bar per account of the user's own. A transfer touches two of them and is counted under both — it left one and arrived at the other, and dropping either end would under-report that account. Every other grain still sums exactly to the ledger; verified against the demo fixture in all four. - Group collapses the Transactions view onto the seller: the far end of the transaction, which Firefly models as an expense account on a withdrawal and a revenue account on a deposit, so the side flips with the direction. Both toggles now go inert where they mean nothing — Per month under a Month period (already one month; splitting it yields the identical single card), Group under a View that has already rolled up. The state is kept across the inert window and masked at the point of use, so switching back restores what the user had set rather than silently clearing it. That made the on/off read matter, and it did not: "on" was a bg-secondary class laid over the outline variant, which in dark mode took the button from bg-input/30 to /50 — the same colour, 20% more opaque, invisible in practice. Both now use the DS `secondary` variant solid vs a dashed outline, which is how FacetedFilter signals set/unset two controls to the right, so the whole header reads by one rule. DS compliance: PASS (gates 1-3 clean, tokens unforked at 0.7.0).
…only The facets were pushed to the far edge by `ml-auto`, which at this width made them wrap onto their own line and read as a second bar rather than part of the same group as View. Dropped it — View, the two toggles and the facets now flow as one run. Per month and Group lose their labels: they are switches, like Refresh beside them, and two words apiece bought nothing the icon and the tooltip do not already say. Group also loses the storefront icon for lucide's `group`, which is about the operation rather than about sellers.
The operator's grouping: Period and its navigator on the left (what window), everything from View rightward pushed right at a single 8px gap (what you are looking at, and how you are narrowing it). The separator that used to sit before View is gone — the `ml-auto` whitespace is the boundary now, and a rule inside the run would break the even spacing the group is built on. FilterBar renders a fragment, so its facets are direct children of the run and inherit that same gap rather than nesting their own.
The clear-filters button was always mounted and merely `invisible`, so an empty 32px slot plus its two gaps opened a 48px hole between Group and Type in a run whose every other gap is 8px. It is now rendered only when there is something to clear. The slot existed to keep the bar from shifting when a filter goes on or off. That trade is the wrong way round: the asymmetry is on screen every load, the shift happens once, at the moment you set the first filter, and you are inside the popover when it does. Both headers use this component, so Outstanding & Upcoming evens out too — and on Reports the 40px it gives back is exactly enough for the whole run to fit on one line at 1440px instead of wrapping.
…dule README gains the two new bar subjects and both toggles' inert conditions, and its category-that-both-spends note generalises to any bucket — which is now the visible case, since an account you are paid into and spend from shows as two bars. Screenshot reshot on the new header, at the same 1600px scale as the forecast one and palette-quantised back under 100 KB. Code: `Grain` stops being exported (nothing outside the module needs it), and ReportsNav hoists the two `x && !inert` masks it was recomputing four times each into `perMonthOn` / `groupOn`, which is also what the props now read as. Its docstring described a layout that no longer exists.
42piratas
added a commit
that referenced
this pull request
Aug 3, 2026
…#31) Mine, in #30. I symlink `web/node_modules` at a sibling checkout so a fresh worktree can typecheck without a second install, and on the last commit of that PR I did not remove it before `git add -A`. It went in as a mode 120000 blob pointing at `../../../ff3e-app/web/node_modules` — a path that exists on exactly one machine — so a fresh clone gets a dangling symlink that escapes the repo root. CI did not catch it: the Pages build runs its own install over the top and deployed fine. Both .gitignore files said `node_modules/`. The trailing slash means "a directory of that name", and a symlink is not a directory, so nothing matched it. Dropping the slash matches both — which is why this could happen at all, and why it now cannot: with the fix in place, re-creating the same symlink leaves `git status` clean.
42piratas
added a commit
that referenced
this pull request
Aug 3, 2026
The other half of #31, which I said was in that PR and was not: I edited both .gitignore files but only `git rm` had staged anything, so the commit carried the deleted symlink and neither ignore change. The symlink is gone from main; the rule that would have stopped it going in was never committed. `node_modules/` with a trailing slash means "a directory of that name". A symlink is not a directory, which is why nothing matched the one I committed in #30. Without the slash it matches both. Verified on this branch, not on an unstaged working tree: with the change staged, re-creating the same symlink leaves `git status` clean and `git check-ignore -v web/node_modules` reports `web/.gitignore:3`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Three asks on the Reports view, plus the header work they turned up.
View → Accounts. One bar per account of the user's own — a withdrawal's source, a deposit's destination. A transfer touches two of them and counts under both: it left one and arrived at the other, and dropping either end would under-report that account's movement.
Group (Transactions view only). Collapses the list onto the seller — the far end of the transaction, which Firefly models as an expense account on a withdrawal and a revenue account on a deposit, so the side follows the direction. On the demo fixture that is 195 rows down to 30.
Both toggles go inert where they mean nothing. Per month under a Month period (already one month; splitting it yields the identical single card), Group under a View that has already rolled up. The setting is kept across the inert window and masked at the point of use, so switching back restores what you had rather than silently clearing it.
How
buildReportalready collapsed transactions onto a subject and ranked them, so this is one seam, not three: theview === 'categories'branch becomes a grain — transaction, seller, category, account — and the three that roll up share a path. A transaction with nothing on the end being ranked is bucketed, never dropped, the same way an uncategorised one always was.No server change.
/api/transactionsalready carriedsource/destination.Header
The two toggles are icon-only now, and everything from View rightward is one right-aligned run at a uniform 8px gap. Two things had to change for that to be true:
bg-secondaryclass laid over theoutlinevariant, which took the button frombg-input/30to/50— the same colour, 20% more opaque. Both toggles now use the DSsecondaryvariant solid vs a dashed outline, which is howFacetedFilteralready signals set/unset two controls to the right.FilterBarreserved a slot for its clear-filters button, always mounted and merelyinvisible, leaving a 48px hole in a run whose every other gap is 8px. It now renders only when there is something to clear. The slot existed to avoid a shift when a filter goes on or off; that trade is the wrong way round — the asymmetry is on screen every load, the shift happens once, and you are inside the popover when it does. This component is shared, so Outstanding & Upcoming evens out too.Verification
tsc --noEmitclean ·npm run buildclean · 58/58 server tests pass (unchanged — no server code touched).Screenshot in
docs/reports.pngreshot on the new header.