Migrates the Transaction Bulk Code modal (a single-step form wearing a full
wizard costume) to a plain Selmer form, cold-applying the ssr-form-migration
skill. Almost entirely reuse of the Phase-2 work: the whole `sc/*` Selmer
component library, `account-typeahead*` / `location-select*`, and the
`edit-modal` / `transitioner` chrome are imported wholesale.
What changed
- Wizard removed: deleted `BulkCodeWizard` / `AccountsStep` records,
`MultiStepFormState`, the `step-params[...]` prefix, and all `mm/*`
middleware. Replaced with a plain handler + flat `wrap-bulk-state` (decode
straight into `bulk-code-schema`, no snapshot round-trip).
- Selection round-trip: the non-editable transaction selection is resolved to
a concrete not-locked id vector at open and ridden back in hidden `ids[]`
fields (the bulk analog of edit's single `db/id`) — no EDN snapshot, no
filter re-query, and more correct (codes exactly the rows the user saw).
- 100% Selmer render path (only the shared terminal `com/success-modal` keeps
Hiccup — heuristic-9 exception). New shared component `sc/select`
(`location-select.html` generalized) for the status dropdown.
- Routes 4 -> 3: GET `bulk-code` (open), POST `bulk-code-submit`, POST
`bulk-code-form-changed` (one whole-form op dispatcher folding the old
`new-account` + `vendor-changed` routes). Location swap moved off `find *`
onto explicit `#account-location-<index>` + `hx-select`.
- Fixed a latent correctness bug surfaced by the migration: the vendor
typeahead needs `:id` (value-keyed `:key`) or its value-bound hidden goes
stale across a whole-form swap and posts blank.
Scorecard delta (transaction/bulk_code.clj): mm coupling 19->0, snapshot
merges 4->0, wizard records 3->0, step-params 10->0, routes 4->3, OOB 0,
Hiccup-in-render ->0 (bar success-modal). LOC 420->506 (documented exception:
the wizard was a thin shell over mm/* defaults, so explicitness moves shared
plumbing into the file). Cookbook: reused the entire Phase-2 sc/* lib + chrome,
added sc/select.
Verification: bulk-code-transactions.spec.ts 13/13; full Playwright suite
39/39; cljfmt clean.
Skill fed: scorecard row + narrative + LOC exception; gotchas (value-bound
typeahead keying, selection-as-ids round-trip); cookbook (sc/select).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Squashed Phase-2 SSR work: migrate the Transaction Edit modal's render path
entirely to Selmer templates (zero Hiccup in the render path), rip out the
multi-step wizard abstraction (EditWizard/LinksStep records, MultiStepFormState,
step-params[...] field names, mm/* middleware) in favor of a plain form with
flat derived state, and promote shared UI components to reusable Selmer partials
under resources/templates/components/. Adds the Selmer interop bridge, the
auto-ap.ssr.components.selmer (sc) wrapper library, and the ssr-form-migration
skill capturing the learnings.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replicate the master CLJS "delete external ledger" feature on the SSR
external ledger page: an admin-only bulk delete that retracts the
selected journal entries, skipping any in a client's locked period and
capping at 1000 per request.
Return the result via modal-response (retargets the persistent
#modal-content shell) and target #modal-content from the button so the
request never relies on the outerHTML swap inherited from the data-grid
card, which previously replaced #modal-holder and broke the next click.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Shows a "Clear filters" button in the transactions action bar whenever a
non-date filter is active. It's a boosted link back to the transactions
page that preserves the date range (and any implied status), so the
sidebar filters and table both reset.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Disable Code, Delete, and Suppress until at least one row is checked or
"select all" is active, matching the existing selection-aware UI.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Bulk coding left the checked items selected after the table refreshed.
Add a dedicated reset-selection event that the grid's Alpine state
listens for, and fire it alongside refreshTable on bulk-code submit.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The unresolved/potential-duplicates query-param decoders fell through to
(boolean %) for unrecognized strings. A round-tripped "false" (pushed into the
URL, re-read via HX-Current-URL) decoded to true since any non-nil string is
truthy, so navigating pages silently turned on the "Unresolved only" filter.
Handle "false" and already-boolean values symmetrically.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Account coding lived in the always-applied base map of edit-form-schema, so
every action (including the link/apply-rule/unlink actions) required a valid
transaction-account/account. The edit modal always submits the Manual tab's
(usually blank) account row, so link submits failed validation before reaching
their save-handler and silently no-op'd. Move account validation into the
:manual branch of the action :multi so link actions validate without it.
Also surface whole-form validation errors in the wizard footer error bar:
default-step-footer only handled top-level/sequential error shapes, so nested
field-error maps (e.g. a hidden tab's account error) produced an empty bar and
a silent failure. Add flatten-form-errors to flatten the humanized error tree.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bulk-code "Requires Feedback" option submitted "requires_feedback"
(underscore), which decoded to an enum keyword not present in the
schema (idents use a hyphen), so selecting it failed validation. Use
the hyphenated value and relabel the option, the reconciliation report
header to "Client Review" to unify with the sidebar terminology.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The profit-and-loss report always passed :warning as a [:div ...] hiccup
vector, which is truthy even when empty. The shared report table renders
its red warning box with (when warning ...), so a clean report with no
warning and no unresolved entries still showed an empty red error box.
Only build the warning div when there is actual warning text or sample
links, matching how the balance-sheet and cash-flows reports pass nil.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The save-handler re-rendered the edited row via row* without passing
:request, so the Client column's :hide? predicate received a nil request
and never hid the column. Pass :request request like table* does.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The bank-account filter rendered "Please select a client" even when a
client was set on the rule. Two causes:
- Inside (fc/with-field :transaction-rule/bank-account ...) the cursor is
rebound to the bank-account field, so (:transaction-rule/client
(fc/field-value)) read the nil bank-account value and the server
rendered the placeholder. The clientId watcher only fires on change, so
when editing (client preset, unchanged) the htmx swap never corrected
it. Read the client from the form root before entering the field.
- The clientId-change swap used innerHTML, nesting a fresh typeahead
inside the stale one and breaking its Alpine refs. Use outerHTML so the
typeahead is replaced in place.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The top bar grew vertically on narrower viewports when the environment
badge and company-selector labels wrapped, pushing content under the
fixed navbar (which the layout offsets with a fixed pt-16).
Rework the navbar into a fixed h-16 row with a priority-based responsive
layout:
- search fills the middle (flex-1) and shrinks first when space is tight
- company selector holds its size and truncates long names
- environment badge degrades full pill -> compact letter badge -> hidden
- harmonize control heights (40px controls, 32px badge/avatar accents) so
the search no longer renders as a cramped thin strip
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The error-details <pre> lived inside a <span x-data="{e:false}"> that was
itself inside a <p>. Since <pre> is block content, the HTML parser closed
the <p> and reparented the <pre> out of the span, so Alpine evaluated
x-show="e" with e no longer in scope ("e is not defined"). Use a <div>
wrapper instead of <p> so the pre stays within the e scope.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Location field hx-target "find *" resolved to the <label> (first child),
so changing an account swapped the reloaded <select> over the label and
left a duplicate dropdown. Target "find select" instead (simple + advanced).
- edit-wizard-toggle-mode-handler read mode only from step-params, but the
hidden "mode" field is a top-level form param, so current-mode always
defaulted to "simple" and the toggle could never return from advanced.
Read it from form-params too, matching edit-vendor-changed-handler.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The EditModal step body wrapped all rule fields in a nested
<form id="my-form"> inside the wizard's own #wizard-form. By HTML
form-ownership rules those fields belonged to the inner form, so when
htmx serialized #wizard-form on Next, none of the step-params fields
were sent. The server saw an empty rule, reported "required" for
description/accounts, and re-rendered a blank wizard (losing input).
Replace the nested <form> with a plain <div>; the wizard form already
owns submission, so the inner form and its htmx attributes were
redundant.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Most grid pages auto-submitted their date-range filter on every change
event, which fired mid-typing and re-rendered the date inputs, breaking
manual date entry. Invoices and ledgers already gated date submission
behind an explicit Apply button; this brings the other ten pages in line.
- date-range component: stop `change` from the date inputs bubbling to
the form (@change.stop) and always render the Apply button, so typed or
picked dates submit only via the Apply button's `datesApplied` event.
The All/Week/Month/Year presets and all other filters are unaffected.
- payments, invoice import, transactions, import batches, sales
summaries, expected deposits, cash drawer shifts, refunds, tenders,
sales orders: add `datesApplied` to the form hx-trigger.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
content-card used `overflow-scroll`, which renders scrollbar tracks even
when the content fits — visible as superfluous bars around the admin chart
cards. Switch to `overflow-auto` so scrollbars only appear when content
genuinely overflows (e.g. wide data tables still scroll).
Also add a gitignored ./tmp/ scratch directory (tracked via .gitkeep) and
document in AGENTS.md that temp files belong there.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The admin page was the only consumer of Chartist while the dashboard and
expense report already use Chart.js. Convert the admin "Growth in clients"
(bar) and "Changes by hour" (line) charts to Chart.js using the same
Alpine x-data/x-init canvas pattern as the dashboard, and drop the global
Chartist CSS/JS includes from the base page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Alphabetize the import.clj :require block (AGENTS.md Import Formatting).
- Remove unused imports (digest, strip) flagged by clj-kondo.
- Make the client-not-found classify-table test independent: it previously
reused the bank-account-not-found input and added zero marginal coverage;
now seeds an orphan bank account so only the client error fires.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implement the SSR/alpine/htmx manual transaction import, wiring the
already-declared but unhandled ::external-import-page/parse/import routes.
Mirrors the SSR ledger import: paste the exact master-branch Yodlee
positional-column TSV, review parsed rows in an editable grid with per-row
error/warning badges, and import. Every master validation is preserved and
the existing import.transactions engine is reused unchanged
(via import.manual/import-batch), so core components are untouched.
- New ns auto-ap.ssr.transaction.import (page, paste/parse, editable grid,
two-tier validation, import handler) + admin-only transactions Import nav.
- Two-tier validation: fixable problems (bad date/amount, unknown client or
bank-account code, missing fields) are hard errors that block the whole
batch; inherent skip-conditions (non-POSTED, before start-date/locked,
already-imported) are warnings computed from the engine's own
categorize-transaction so the grid preview matches the import result.
- Tests: failing-first Playwright e2e (e2e/transaction-import.spec.ts) plus
unit/integration coverage (ssr/transaction/import_test.clj, 10 tests).
- Deterministic bank-account code in the e2e seed.
Plan: docs/plans/2026-06-01-001-feat-manual-transaction-import-ssr-plan.md
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
When a transaction is pre-coded, the snapshot stores :transaction-account/account
as a Datomic ref map {:db/id N} rather than a bare integer. simple-mode-fields*
and the simpleAccountId Alpine initializer both need the integer id, not the map,
to correctly populate the account typeahead value and the x-hx-val binding.