refactor(ssr): Phase 5 — full Selmer migration of Invoice Bulk Edit; remove the wizard; implement live totals
Migrates the Invoice Bulk Edit modal off the wizard to a plain Selmer form, building on the parity gate. Structurally Phase 3's bulk-code applied to invoices (selected entities -> expense-account rows), so near-pure reuse of bulk-code's flat-state plumbing + edit's account-totals-tbody. What changed - Wizard removed: deleted BulkEditWizard/AccountsStep records, MultiStepFormState, the step-params[...] prefix, the EDN snapshot, and all mm/* for this modal. Replaced with a plain handler + flat wrap-bulk-state (decode straight into bulk-edit-schema, no snapshot). - Selection-as-ids round-trip: the non-editable invoice 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 filter re-query. - De-cursored bulk-edit-account-row* to Selmer (sc/*), explicit-id location swap (#account-location-<index>, replacing the old find * swap), reusing tx-edit/location-select*. - 100% Selmer modal render path; the surgical edit was done with the text-based Edit tool (the clojure-mcp structural tools reformat the whole 1812-line file), so the diff is contained to the requires + the bulk-edit region. - Routes 5 -> 3: GET bulk-edit, PUT bulk-edit-submit, POST bulk-edit-form-changed (one whole-form op dispatcher folding the old new-account route). Implemented the dead totals - The wizard's TOTAL/BALANCE percentage rows were commented out (#_(...)) with a duplicate id="total". Implemented as a #expense-totals sibling-<tbody> refreshed by a Rule-4 percentage-keyup swap (the new-account + total + balance routes all fold into form-changed / the sibling-tbody). Scorecard delta (bulk-edit modal): wizard records 2->0, bulk-edit routes 5->3, step-params/fc-cursor (modal) ->0, location swap find *-> explicit-id, totals dead->implemented. Verification: invoice-bulk-edit spec 5/5 (incl. add-row, save, validation, the implemented totals); full Playwright suite 50/50; cljfmt clean; diff confined to the modal region. Skill fed: scorecard row + settled repeated-row target-selector convention; gotcha (structural tools reformat large files -> use text Edit). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -253,6 +253,18 @@ A money/text input wired `hx-trigger="keyup changed delay:300ms"` does **not** f
|
|||||||
swap actually triggers. (A `change`-triggered control is the opposite — `dispatchEvent('change')`
|
swap actually triggers. (A `change`-triggered control is the opposite — `dispatchEvent('change')`
|
||||||
is fine there.)
|
is fine there.)
|
||||||
|
|
||||||
|
## clojure-mcp structural edits reformat the whole file — use text Edit in big shared files
|
||||||
|
|
||||||
|
`clojure_edit` / `clojure_edit_replace_sexp` re-emit the **entire file** through the formatter.
|
||||||
|
In a small single-modal file that's fine (cljfmt-clean output). In a **large multi-modal file**
|
||||||
|
(Phase 5: `invoices.clj`, 1812 lines) a one-line require addition produced a **650-line spurious
|
||||||
|
whitespace diff** that buries the real change and makes review impossible. For a surgical
|
||||||
|
migration inside a big shared file, use the **text-based Edit tool** (exact-string match — no
|
||||||
|
reformat); this is the AGENTS.md "edit Clojure with file tools only when absolutely necessary"
|
||||||
|
carve-out. Verify with `load-file` (compile) + `lein cljfmt check`, not by eyeballing. Confirm the
|
||||||
|
diff is contained with `git diff -U0 <file> | grep '^@@'` — the hunks should cluster only where you
|
||||||
|
edited (requires + the modal region), nothing else.
|
||||||
|
|
||||||
## Scorecard exceptions (ratchet violations with a reason)
|
## Scorecard exceptions (ratchet violations with a reason)
|
||||||
|
|
||||||
**Heuristic 4 (LOC net ↓) — exception (Phase 3, Transaction Bulk Code: 420→506).** When the
|
**Heuristic 4 (LOC net ↓) — exception (Phase 3, Transaction Bulk Code: 420→506).** When the
|
||||||
|
|||||||
@@ -133,3 +133,31 @@ Each migration appends one row (after-numbers), referencing the before in the di
|
|||||||
> proper `#summary-totals` block shows running Total + Balanced/Unbalanced (a Rule-4 targeted swap
|
> proper `#summary-totals` block shows running Total + Balanced/Unbalanced (a Rule-4 targeted swap
|
||||||
> on manual amount edits). The spec was updated to assert the fixed behavior. New cookbook entries:
|
> on manual amount edits). The spec was updated to assert the fixed behavior. New cookbook entries:
|
||||||
> the **inline click-to-edit cell** and the **db/id-keyed item merge** for partially-posted rows.
|
> the **inline click-to-edit cell** and the **db/id-keyed item merge** for partially-posted rows.
|
||||||
|
|
||||||
|
> **Phase 5 — Invoice Bulk Edit (cold apply in a 1812-line shared file).** Structurally
|
||||||
|
> Phase 3's bulk-code applied to invoices (selected entities → expense-account rows:
|
||||||
|
> account/location/percentage), so it was near-pure reuse: bulk-code's flat-state plumbing
|
||||||
|
> (ids round-trip, `wrap-bulk-state`, schema/decode) + edit's `account-totals-tbody` for the
|
||||||
|
> live totals. `BulkEditWizard`/`AccountsStep` + `MultiStepFormState` deleted, `step-params`
|
||||||
|
> dropped, the rows de-cursored to Selmer with the explicit-id location swap, bulk-edit
|
||||||
|
> routes 5→**3** (the `new-account` + `total` + `balance` routes folded into one
|
||||||
|
> `form-changed` op dispatcher + the sibling-`<tbody>` totals swap). **Implemented the dead
|
||||||
|
> TOTAL/BALANCE display** (the wizard had them commented out with a duplicate `id="total"`)
|
||||||
|
> as a `#expense-totals` sibling-`<tbody>` refreshed by a Rule-4 percentage-keyup swap.
|
||||||
|
> Parity held: invoice-bulk-edit spec 5/5, full suite 50/50.
|
||||||
|
>
|
||||||
|
> **Editing a wizard buried in a large shared file:** the clojure-mcp structural tools
|
||||||
|
> (`clojure_edit` / `replace_sexp`) **reformat the whole file** — here that was a spurious
|
||||||
|
> 650-line whitespace diff that would bury the real change. For a surgical migration inside a
|
||||||
|
> big multi-modal file, use the **text-based Edit tool** instead (the AGENTS.md "absolutely
|
||||||
|
> necessary" carve-out), then `load-file` + `cljfmt` to verify. The resulting diff was fully
|
||||||
|
> contained to the requires + the bulk-edit region.
|
||||||
|
>
|
||||||
|
> **Repeated-row target-selector convention — settled (the Phase 5 exit criterion).** Across
|
||||||
|
> edit / bulk-code / sales-summary / invoice-bulk-edit the convention converged on: **explicit
|
||||||
|
> per-row ids** (`#account-location-<index>`, `#account-row-<index>`) for a cell-local swap
|
||||||
|
> (Rule 2), and a **single stable-id sibling-`<tbody>`** (`#account-totals` / `#expense-totals`)
|
||||||
|
> for running totals (Rule 4) — *not* data-attribute selectors or a `form-path→selector`
|
||||||
|
> helper. Per-row ids are generated from the row index the form already uses for field names
|
||||||
|
> (`path->name2`), so server and markup agree by construction. Whole-form swap (Rule 3) covers
|
||||||
|
> structural changes (add/remove row). This is now the cookbook default; see `swap-doctrine.md`.
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ async function addNewAccount(page: any) {
|
|||||||
// one (Solr-backed typeahead is unavailable in tests), then dispatching the location
|
// one (Solr-backed typeahead is unavailable in tests), then dispatching the location
|
||||||
// reload -- the same approach the bulk-code spec uses.
|
// reload -- the same approach the bulk-code spec uses.
|
||||||
async function setRowAccount(page: any, rowIndex: number, accountId: string) {
|
async function setRowAccount(page: any, rowIndex: number, accountId: string) {
|
||||||
const rows = page.locator('#wizard-form tbody tr');
|
const rows = page.locator('#bulk-edit-form tbody tr');
|
||||||
const row = rows.nth(rowIndex);
|
const row = rows.nth(rowIndex);
|
||||||
const hidden = row.locator('input[type="hidden"][name*="[account]"]').first();
|
const hidden = row.locator('input[type="hidden"][name*="[account]"]').first();
|
||||||
await hidden.evaluate((el: HTMLInputElement, value: string) => {
|
await hidden.evaluate((el: HTMLInputElement, value: string) => {
|
||||||
@@ -53,7 +53,7 @@ async function setRowAccount(page: any, rowIndex: number, accountId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setRowPercentage(page: any, rowIndex: number, pct: string) {
|
async function setRowPercentage(page: any, rowIndex: number, pct: string) {
|
||||||
const row = page.locator('#wizard-form tbody tr').nth(rowIndex);
|
const row = page.locator('#bulk-edit-form tbody tr').nth(rowIndex);
|
||||||
const input = row.locator('input.amount-field, input[name*="percentage"]').first();
|
const input = row.locator('input.amount-field, input[name*="percentage"]').first();
|
||||||
await input.fill(pct);
|
await input.fill(pct);
|
||||||
await input.dispatchEvent('change');
|
await input.dispatchEvent('change');
|
||||||
@@ -61,7 +61,7 @@ async function setRowPercentage(page: any, rowIndex: number, pct: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function submitForm(page: any) {
|
async function submitForm(page: any) {
|
||||||
await page.locator('#wizard-form').evaluate((f: HTMLFormElement) =>
|
await page.locator('#bulk-edit-form').evaluate((f: HTMLFormElement) =>
|
||||||
f.dispatchEvent(new Event('submit', { bubbles: true })));
|
f.dispatchEvent(new Event('submit', { bubbles: true })));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ test.describe('Invoice Bulk Edit (characterization)', () => {
|
|||||||
await expect(modal).toContainText('TOTAL');
|
await expect(modal).toContainText('TOTAL');
|
||||||
await expect(modal).toContainText('BALANCE');
|
await expect(modal).toContainText('BALANCE');
|
||||||
// a default expense-account row is present, plus the New account button
|
// a default expense-account row is present, plus the New account button
|
||||||
expect(await modal.locator('input[name*="[expense-accounts]"][name*="[account]"]').count()).toBeGreaterThanOrEqual(1);
|
expect(await modal.locator('input[name*="expense-accounts"][name*="[account]"]').count()).toBeGreaterThanOrEqual(1);
|
||||||
await expect(modal.locator('a:has-text("New account")')).toBeVisible();
|
await expect(modal.locator('a:has-text("New account")')).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -89,7 +89,7 @@ test.describe('Invoice Bulk Edit (characterization)', () => {
|
|||||||
await selectFirstInvoice(page);
|
await selectFirstInvoice(page);
|
||||||
await openBulkEditModal(page);
|
await openBulkEditModal(page);
|
||||||
|
|
||||||
const accountRows = () => page.locator('#wizard-form input[name*="[expense-accounts]"][name*="[account]"]');
|
const accountRows = () => page.locator('#bulk-edit-form input[name*="expense-accounts"][name*="[account]"]');
|
||||||
const before = await accountRows().count();
|
const before = await accountRows().count();
|
||||||
await addNewAccount(page);
|
await addNewAccount(page);
|
||||||
expect(await accountRows().count()).toBe(before + 1);
|
expect(await accountRows().count()).toBe(before + 1);
|
||||||
@@ -110,6 +110,23 @@ test.describe('Invoice Bulk Edit (characterization)', () => {
|
|||||||
await expect(page.locator('#wizardmodal')).toBeHidden({ timeout: 8000 });
|
await expect(page.locator('#wizardmodal')).toBeHidden({ timeout: 8000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// The TOTAL/BALANCE percentage rows were dead code in the wizard (commented out, with a
|
||||||
|
// duplicate id="total"); the migration implements them as a sibling-<tbody> Rule-4 swap.
|
||||||
|
test('TOTAL/BALANCE percentages render and recompute on edit (implemented)', async ({ page }) => {
|
||||||
|
await navigateToInvoices(page);
|
||||||
|
await selectFirstInvoice(page);
|
||||||
|
await openBulkEditModal(page);
|
||||||
|
|
||||||
|
// default row is 100% -> TOTAL 100.0%
|
||||||
|
await expect(page.locator('#expense-totals')).toContainText('100.0%');
|
||||||
|
// edit to 50% -> the totals tbody refreshes via the targeted swap
|
||||||
|
const pct = page.locator('#bulk-edit-form input.amount-field').first();
|
||||||
|
await pct.click();
|
||||||
|
await pct.fill('');
|
||||||
|
await pct.pressSequentially('50'); // keyup -> Rule-4 swap of #expense-totals
|
||||||
|
await expect(page.locator('#expense-totals')).toContainText('50.0%', { timeout: 5000 });
|
||||||
|
});
|
||||||
|
|
||||||
test('rejects when account percentages do not total 100%', async ({ page }) => {
|
test('rejects when account percentages do not total 100%', async ({ page }) => {
|
||||||
const info = await getTestInfo(page);
|
const info = await getTestInfo(page);
|
||||||
await navigateToInvoices(page);
|
await navigateToInvoices(page);
|
||||||
|
|||||||
4
resources/templates/invoice-bulk-edit/edit-form.html
Normal file
4
resources/templates/invoice-bulk-edit/edit-form.html
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{# Top-level plain bulk-edit form (no wizard). The resolved (not-locked) invoice id set
|
||||||
|
rides in hidden ids[] fields so the selection survives form-changed / submit posts
|
||||||
|
without an EDN snapshot or a filter round-trip. #}
|
||||||
|
<form id="bulk-edit-form"{{ form_attrs|safe }}>{{ ids_hidden|safe }}{{ modal|safe }}</form>
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
{# Running TOTAL / BALANCE percentage rows in their own swappable <tbody>, a sibling of
|
||||||
|
the input rows, so a percentage edit refreshes them with a targeted swap (Rule 4) and
|
||||||
|
never replaces the input-bearing rows above. Replaces the old per-cell bulk-edit-total
|
||||||
|
/ bulk-edit-balance routes. #}
|
||||||
|
<tbody id="expense-totals">{{ rows|safe }}</tbody>
|
||||||
@@ -31,8 +31,12 @@
|
|||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
|
[auto-ap.ssr.components.link-dropdown :refer [link-dropdown]]
|
||||||
[auto-ap.ssr.components.multi-modal :as mm]
|
[auto-ap.ssr.components.multi-modal :as mm]
|
||||||
|
[auto-ap.ssr.components.selmer :as sc]
|
||||||
[auto-ap.ssr.form-cursor :as fc]
|
[auto-ap.ssr.form-cursor :as fc]
|
||||||
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
[auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]]
|
||||||
|
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
||||||
|
[auto-ap.ssr.selmer :as sel]
|
||||||
|
[auto-ap.ssr.transaction.edit :as tx-edit]
|
||||||
[auto-ap.ssr.hiccup-helper :as hh]
|
[auto-ap.ssr.hiccup-helper :as hh]
|
||||||
[auto-ap.ssr.hx :as hx]
|
[auto-ap.ssr.hx :as hx]
|
||||||
[auto-ap.ssr.invoice.common :refer [default-read]]
|
[auto-ap.ssr.invoice.common :refer [default-read]]
|
||||||
@@ -41,11 +45,11 @@
|
|||||||
[auto-ap.ssr.components.date-range :as dr]
|
[auto-ap.ssr.components.date-range :as dr]
|
||||||
[auto-ap.ssr.svg :as svg]
|
[auto-ap.ssr.svg :as svg]
|
||||||
[auto-ap.ssr.utils
|
[auto-ap.ssr.utils
|
||||||
:refer [apply-middleware-to-all-handlers assert-schema
|
:refer [->db-id apply-middleware-to-all-handlers assert-schema
|
||||||
clj-date-schema dissoc-nil-transformer entity-id
|
clj-date-schema dissoc-nil-transformer entity-id
|
||||||
form-validation-error html-response main-transformer
|
form-validation-error html-response main-transformer
|
||||||
many-entity modal-response money percentage
|
many-entity modal-response money path->name2 percentage
|
||||||
ref->enum-schema round-money strip wrap-entity
|
ref->enum-schema round-money strip wrap-entity wrap-form-4xx-2
|
||||||
wrap-implied-route-param wrap-merge-prior-hx
|
wrap-implied-route-param wrap-merge-prior-hx
|
||||||
wrap-schema-enforce]]
|
wrap-schema-enforce]]
|
||||||
[auto-ap.time :as atime]
|
[auto-ap.time :as atime]
|
||||||
@@ -1433,23 +1437,74 @@
|
|||||||
target-route)
|
target-route)
|
||||||
(:query-params request)))}}))
|
(:query-params request)))}}))
|
||||||
|
|
||||||
(defn initial-bulk-edit-state [request]
|
;; ---------------------------------------------------------------------------
|
||||||
(mm/->MultiStepFormState {:search-params (:query-params request)
|
;; Flat state plumbing for the bulk-edit modal (replaces the wizard +
|
||||||
:expense-accounts [{:db/id "123"
|
;; MultiStepFormState + the EDN snapshot). Mirrors transaction/bulk_code.clj.
|
||||||
|
;; ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
(declare all-ids-not-locked)
|
||||||
|
|
||||||
|
(def ^:dynamic *errors*
|
||||||
|
"Humanized form errors for the current bulk-edit render, keyed by schema paths
|
||||||
|
(e.g. {:expense-accounts {0 {:location [\"required\"]}}}). Bound by render-form."
|
||||||
|
{})
|
||||||
|
|
||||||
|
(defn- ferr [& path]
|
||||||
|
(get-in *errors* (vec path)))
|
||||||
|
|
||||||
|
(defn- account-field-name [index field]
|
||||||
|
(path->name2 :expense-accounts index field))
|
||||||
|
|
||||||
|
(defn- account-field-errors [index field]
|
||||||
|
(ferr :expense-accounts index field))
|
||||||
|
|
||||||
|
(def bulk-edit-schema
|
||||||
|
(mc/schema [:map
|
||||||
|
[:ids {:optional true} [:maybe [:vector {:coerce? true} entity-id]]]
|
||||||
|
[:expense-accounts {:optional true}
|
||||||
|
[:maybe
|
||||||
|
[:vector {:coerce? true}
|
||||||
|
[:map
|
||||||
|
[:db/id {:optional true} [:maybe :string]]
|
||||||
|
[:account entity-id]
|
||||||
|
[:location [:string {:min 1 :error/message "required"}]]
|
||||||
|
[:percentage percentage]]]]]]))
|
||||||
|
|
||||||
|
(def ^:private bulk-edit-form-keys [:expense-accounts])
|
||||||
|
|
||||||
|
(defn- default-expense-row []
|
||||||
|
{:db/id (str (java.util.UUID/randomUUID))
|
||||||
:location "Shared"
|
:location "Shared"
|
||||||
:account nil
|
:account nil
|
||||||
:percentage 1.0}]}
|
:percentage 1.0})
|
||||||
[]
|
|
||||||
{:search-params (:query-params request)
|
(defn wrap-bulk-state
|
||||||
:expense-accounts [{:db/id "123"
|
"Decodes the posted form into the flat bulk-edit state and resolves the target invoice
|
||||||
:location "Shared"
|
id set. On open (GET) the selection comes from the grid query-params (selected /
|
||||||
:account nil
|
all-selected + filters); on every post the concrete (not-locked) id list rides back in
|
||||||
:percentage 1.0}]}))
|
hidden ids[] fields, so no EDN snapshot / filter round-trip is needed."
|
||||||
|
[handler]
|
||||||
|
(-> (fn [request]
|
||||||
|
(let [decoded (mc/decode bulk-edit-schema (:form-params request) main-transformer)
|
||||||
|
decoded (if (map? decoded) decoded {})
|
||||||
|
posted-ids (some->> (:ids decoded) (keep ->db-id) vec)
|
||||||
|
ids (if (seq posted-ids)
|
||||||
|
posted-ids
|
||||||
|
(vec (all-ids-not-locked (selected->ids request (:query-params request)))))
|
||||||
|
accounts (or (seq (:expense-accounts decoded)) [(default-expense-row)])]
|
||||||
|
(handler (assoc request :bulk-state {:ids ids :expense-accounts (vec accounts)}))))
|
||||||
|
(wrap-nested-form-params)))
|
||||||
|
|
||||||
|
(defn- single-client-id
|
||||||
|
"The client id if the user has access to exactly one client, nil otherwise (the bulk
|
||||||
|
set may span clients)."
|
||||||
|
[request]
|
||||||
|
(when (= 1 (count (:clients request)))
|
||||||
|
(-> request :clients first :db/id)))
|
||||||
|
|
||||||
(defn- account-typeahead*
|
(defn- account-typeahead*
|
||||||
[{:keys [name value client-id x-model]}]
|
[{:keys [name value client-id x-model]}]
|
||||||
[:div.flex.flex-col
|
(sc/typeahead {:name name
|
||||||
(com/typeahead {:name name
|
|
||||||
:placeholder "Search..."
|
:placeholder "Search..."
|
||||||
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
||||||
{:purpose "invoice"})
|
{:purpose "invoice"})
|
||||||
@@ -1458,7 +1513,7 @@
|
|||||||
:value value
|
:value value
|
||||||
:content-fn (fn [value]
|
:content-fn (fn [value]
|
||||||
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
||||||
client-id)))})])
|
client-id)))}))
|
||||||
|
|
||||||
;; TODO clientize
|
;; TODO clientize
|
||||||
(defn all-ids-not-locked [all-ids]
|
(defn all-ids-not-locked [all-ids]
|
||||||
@@ -1472,121 +1527,135 @@
|
|||||||
[(>= ?d ?lu)]]
|
[(>= ?d ?lu)]]
|
||||||
(dc/db conn))
|
(dc/db conn))
|
||||||
(map first)))
|
(map first)))
|
||||||
(defn- bulk-edit-account-row* [{:keys [value client-id]}]
|
(defn- bulk-edit-account-row*
|
||||||
|
"One expense-account row (no cursor). The location cell swaps just itself
|
||||||
(com/data-grid-row
|
(#account-location-<index>, Rule 2); the percentage swaps only #expense-totals
|
||||||
(-> {:x-data (hx/json {:show (boolean (not (fc/field-value (:new? value))))
|
(Rule 4); remove swaps the whole #bulk-edit-form (Rule 3)."
|
||||||
:accountId (fc/field-value (:account value))})
|
[{:keys [value client-id index]}]
|
||||||
|
(let [account-val (let [av (:account value)]
|
||||||
|
(if (map? av) (:db/id av) av))
|
||||||
|
location-attrs {:x-hx-val:account-id "accountId"
|
||||||
|
:hx-vals (hx/json (cond-> {:name (account-field-name index :location)}
|
||||||
|
client-id (assoc :client-id client-id)))
|
||||||
|
:x-dispatch:changed "accountId"
|
||||||
|
:hx-trigger "changed"
|
||||||
|
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed)
|
||||||
|
:hx-target (str "#account-location-" index)
|
||||||
|
:hx-select (str "#account-location-" index)
|
||||||
|
:hx-swap "outerHTML"
|
||||||
|
:hx-include "closest form"}]
|
||||||
|
(sc/data-grid-row
|
||||||
|
(-> {:class "account-row"
|
||||||
|
:id (str "account-row-" index)
|
||||||
|
:x-data (hx/json {:show (boolean (not (:new? value)))
|
||||||
|
:accountId account-val})
|
||||||
:data-key "show"
|
:data-key "show"
|
||||||
:x-ref "p"}
|
:x-ref "p"}
|
||||||
hx/alpine-mount-then-appear)
|
hx/alpine-mount-then-appear)
|
||||||
(fc/with-field :db/id
|
(sc/hidden {:name (account-field-name index :db/id)
|
||||||
(com/hidden {:name (fc/field-name)
|
:value (:db/id value)})
|
||||||
:value (fc/field-value)}))
|
(sc/data-grid-cell
|
||||||
(fc/with-field :account
|
|
||||||
(com/data-grid-cell
|
|
||||||
{}
|
{}
|
||||||
(com/validated-field
|
(sc/validated-field
|
||||||
{:errors (fc/field-errors)}
|
{:errors (account-field-errors index :account)}
|
||||||
(account-typeahead* {:value (fc/field-value)
|
(account-typeahead* {:value account-val
|
||||||
:client-id client-id
|
:client-id client-id
|
||||||
:name (fc/field-name)
|
:name (account-field-name index :account)
|
||||||
:x-model "accountId"}))))
|
:x-model "accountId"})))
|
||||||
(fc/with-field :location
|
(sc/data-grid-cell
|
||||||
(com/data-grid-cell
|
{:id (str "account-location-" index)}
|
||||||
|
(sc/validated-field
|
||||||
|
(merge {:errors (account-field-errors index :location)} location-attrs)
|
||||||
|
(tx-edit/location-select* {:name (account-field-name index :location)
|
||||||
|
:account-location (:account/location (when (nat-int? account-val)
|
||||||
|
(dc/pull (dc/db conn) '[:account/location] account-val)))
|
||||||
|
:value (:location value)})))
|
||||||
|
(sc/data-grid-cell
|
||||||
{}
|
{}
|
||||||
(com/validated-field
|
(sc/validated-field
|
||||||
{:errors (fc/field-errors)
|
{:errors (account-field-errors index :percentage)}
|
||||||
:x-hx-val:account-id "accountId"
|
(sc/money-input {:name (account-field-name index :percentage)
|
||||||
:hx-vals (hx/json {:name (fc/field-name)})
|
|
||||||
:x-dispatch:changed "accountId"
|
|
||||||
:hx-trigger "changed"
|
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes ::route/location-select)
|
|
||||||
:hx-target "find *"
|
|
||||||
:hx-swap "outerHTML"}
|
|
||||||
(location-select* {:name (fc/field-name)
|
|
||||||
:account-location (:account/location (cond->> (:account @value)
|
|
||||||
(nat-int? (:account @value)) (dc/pull (dc/db conn)
|
|
||||||
'[:account/location])))
|
|
||||||
:value (fc/field-value)}))))
|
|
||||||
(fc/with-field :percentage
|
|
||||||
(com/data-grid-cell
|
|
||||||
{}
|
|
||||||
(com/validated-field
|
|
||||||
{:errors (fc/field-errors)}
|
|
||||||
(com/money-input {:name (fc/field-name)
|
|
||||||
:class "w-16 amount-field"
|
:class "w-16 amount-field"
|
||||||
:value (some-> (fc/field-value)
|
:value (some-> (:percentage value) (* 100) long)
|
||||||
(* 100)
|
:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed)
|
||||||
(long))}))))
|
:hx-target "#expense-totals"
|
||||||
(com/data-grid-cell {:class "align-top"}
|
:hx-select "#expense-totals"
|
||||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
:hx-swap "outerHTML"
|
||||||
|
:hx-trigger "keyup changed delay:300ms"
|
||||||
|
:hx-include "closest form"})))
|
||||||
|
(sc/data-grid-cell
|
||||||
|
{:class "align-top"}
|
||||||
|
(sc/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed)
|
||||||
|
:hx-vals (hx/json {:op "remove-account" :row-index (or index 0)})
|
||||||
|
:hx-target "#bulk-edit-form"
|
||||||
|
:hx-select "#bulk-edit-form"
|
||||||
|
:hx-swap "outerHTML"
|
||||||
|
:hx-include "closest form"
|
||||||
|
:class "account-remove-action"}
|
||||||
|
svg/x)))))
|
||||||
|
|
||||||
(defrecord AccountsStep [linear-wizard]
|
(defn- expense-total* [request]
|
||||||
mm/ModalWizardStep
|
(let [total (->> (-> request :bulk-state :expense-accounts)
|
||||||
(step-name [_]
|
(map (fnil :percentage 0.0))
|
||||||
"Expense Accounts")
|
(filter number?)
|
||||||
(step-key [_]
|
(reduce + 0.0))]
|
||||||
:accounts)
|
(format "%.1f%%" (* 100.0 total))))
|
||||||
|
|
||||||
(edit-path [_ _]
|
(defn- expense-balance* [request]
|
||||||
[])
|
(let [total (->> (-> request :bulk-state :expense-accounts)
|
||||||
|
(map (fnil :percentage 0.0))
|
||||||
|
(filter number?)
|
||||||
|
(reduce + 0.0))
|
||||||
|
balance (- 100.0 (* 100.0 total))]
|
||||||
|
(sel/raw (str "<span"
|
||||||
|
(when-not (dollars= 0.0 balance) " class=\"text-red-300\"")
|
||||||
|
">" (format "%.1f%%" balance) "</span>"))))
|
||||||
|
|
||||||
(step-schema [_]
|
(defn- expense-totals-tbody*
|
||||||
(mut/select-keys (mm/form-schema linear-wizard) #{:expense-accounts}))
|
"The separately-swappable TOTAL/BALANCE <tbody> (#expense-totals, Rule 4 target)."
|
||||||
|
[request]
|
||||||
|
(sel/render->hiccup
|
||||||
|
"templates/invoice-bulk-edit/expense-totals.html"
|
||||||
|
{:rows (str
|
||||||
|
(sc/data-grid-row {}
|
||||||
|
(sc/data-grid-cell {})
|
||||||
|
(sc/data-grid-cell {:class "text-right"} (sel/raw "<span class=\"font-bold text-right\">TOTAL</span>"))
|
||||||
|
(sc/data-grid-cell {:id "expense-total" :class "text-right"} (expense-total* request))
|
||||||
|
(sc/data-grid-cell {}))
|
||||||
|
(sc/data-grid-row {}
|
||||||
|
(sc/data-grid-cell {})
|
||||||
|
(sc/data-grid-cell {:class "text-right"} (sel/raw "<span class=\"font-bold text-right\">BALANCE</span>"))
|
||||||
|
(sc/data-grid-cell {:id "expense-balance" :class "text-right"} (expense-balance* request))
|
||||||
|
(sc/data-grid-cell {})))}))
|
||||||
|
|
||||||
(render-step [this {{:keys [snapshot] :as multi-form-state} :multi-form-state :as request}]
|
(defn- account-grid* [request]
|
||||||
(let [selected-ids (selected->ids (assoc request :query-params (:search-params snapshot)) (:search-params snapshot))
|
(let [client-id (single-client-id request)
|
||||||
all-ids (all-ids-not-locked selected-ids)]
|
accounts (vec (:expense-accounts (:bulk-state request)))]
|
||||||
(mm/default-render-step
|
(apply
|
||||||
linear-wizard this
|
sc/data-grid
|
||||||
:head [:div.p-2 "Bulk editing " (count all-ids) " invoices"]
|
{:headers [(sc/data-grid-header {} "Account")
|
||||||
:body (mm/default-step-body
|
(sc/data-grid-header {:class "w-32"} "Location")
|
||||||
{}
|
(sc/data-grid-header {:class "w-16"} "%")
|
||||||
[:div {}
|
(sc/data-grid-header {:class "w-16"})]
|
||||||
(fc/with-field :expense-accounts
|
:footer-tbody (expense-totals-tbody* request)}
|
||||||
(com/validated-field
|
(concat
|
||||||
{:errors (fc/field-errors)}
|
(map-indexed
|
||||||
(com/data-grid {:headers [(com/data-grid-header {} "Account")
|
(fn [index account]
|
||||||
(com/data-grid-header {:class "w-32"} "Location")
|
(bulk-edit-account-row* {:value account
|
||||||
(com/data-grid-header {:class "w-16"} "%")
|
:client-id client-id
|
||||||
(com/data-grid-header {:class "w-16"})]}
|
:index index}))
|
||||||
(fc/cursor-map #(bulk-edit-account-row* {:value %
|
accounts)
|
||||||
:client-id (:invoice/client snapshot)}))
|
[(sc/data-grid-row
|
||||||
|
{:class "new-row"}
|
||||||
(com/data-grid-new-row {:colspan 4
|
(sc/data-grid-cell {:colspan 4}
|
||||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
(sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-form-changed)
|
||||||
::route/bulk-edit-new-account)
|
:hx-vals (hx/json {:op "new-account"})
|
||||||
:row-offset 0
|
:hx-target "#bulk-edit-form"
|
||||||
:index (count (fc/field-value))}
|
:hx-select "#bulk-edit-form"
|
||||||
"New account")
|
:hx-swap "outerHTML"
|
||||||
(com/data-grid-row {}
|
:hx-include "closest form"
|
||||||
(com/data-grid-cell {})
|
:color :secondary}
|
||||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "TOTAL"])
|
"New account")))]))))
|
||||||
(com/data-grid-cell {:id "total"
|
|
||||||
:class "text-right"
|
|
||||||
:hx-trigger "change from:closest form target:.amount-field"
|
|
||||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/bulk-edit-total)
|
|
||||||
:hx-target "this"
|
|
||||||
:hx-swap "innerHTML"}
|
|
||||||
#_(invoice-expense-account-total* request))
|
|
||||||
(com/data-grid-cell {}))
|
|
||||||
|
|
||||||
(com/data-grid-row {}
|
|
||||||
(com/data-grid-cell {})
|
|
||||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
|
|
||||||
(com/data-grid-cell {:id "total"
|
|
||||||
:class "text-right"
|
|
||||||
:hx-trigger "change from:closest form target:.amount-field"
|
|
||||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/bulk-edit-balance)
|
|
||||||
:hx-target "this"
|
|
||||||
:hx-swap "innerHTML"}
|
|
||||||
#_(invoice-expense-account-balance* request))
|
|
||||||
(com/data-grid-cell {})))))])
|
|
||||||
:footer
|
|
||||||
(mm/default-step-footer linear-wizard this :validation-route ::route/new-wizard-navigate
|
|
||||||
:next-button (com/button {:color :primary :x-ref "next" :class "w-32"} "Save"))
|
|
||||||
:validation-route ::route/new-wizard-navigate))))
|
|
||||||
|
|
||||||
(defn maybe-code-accounts [invoice account-rules valid-locations]
|
(defn maybe-code-accounts [invoice account-rules valid-locations]
|
||||||
(with-precision 2
|
(with-precision 2
|
||||||
@@ -1629,96 +1698,121 @@
|
|||||||
(when-not (dollars= 1.0 expense-account-total)
|
(when-not (dollars= 1.0 expense-account-total)
|
||||||
(form-validation-error (str "Expense account total (" expense-account-total ") does not equal 100%")))))
|
(form-validation-error (str "Expense account total (" expense-account-total ") does not equal 100%")))))
|
||||||
|
|
||||||
(defrecord BulkEditWizard [_ current-step]
|
(defn- form-errors-html [errors]
|
||||||
mm/LinearModalWizard
|
(str "<div id=\"form-errors\">"
|
||||||
(hydrate-from-request
|
(when (seq errors)
|
||||||
[this request]
|
(str "<span class=\"error-content\"><p class=\"mt-2 text-xs text-red-600 dark:text-red-500 h-4\">"
|
||||||
this)
|
(str/join ", " (filter string? errors))
|
||||||
(navigate [this step-key]
|
"</p></span>"))
|
||||||
(assoc this :current-step step-key))
|
"</div>"))
|
||||||
(get-current-step [this]
|
|
||||||
(if current-step
|
|
||||||
(mm/get-step this current-step)
|
|
||||||
(mm/get-step this :accounts)))
|
|
||||||
(render-wizard [this {:keys [multi-form-state] :as request}]
|
|
||||||
(mm/default-render-wizard
|
|
||||||
this request
|
|
||||||
:form-params
|
|
||||||
(-> mm/default-form-props
|
|
||||||
(assoc :hx-put
|
|
||||||
(str (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-submit))))
|
|
||||||
:render-timeline? false))
|
|
||||||
(steps [_]
|
|
||||||
[:accounts])
|
|
||||||
(get-step [this step-key]
|
|
||||||
(let [step-key-result (mc/parse mm/step-key-schema step-key)
|
|
||||||
[step-key-type step-key] step-key-result]
|
|
||||||
(get {:accounts (->AccountsStep this)}
|
|
||||||
step-key)))
|
|
||||||
(form-schema [_]
|
|
||||||
(mc/schema [:map
|
|
||||||
[:expense-accounts
|
|
||||||
(many-entity {:min 1}
|
|
||||||
[:account entity-id]
|
|
||||||
[:location [:string {:min 1 :error/message "required"}]]
|
|
||||||
[:percentage percentage])]]))
|
|
||||||
(submit [this {:keys [multi-form-state request-method identity] :as request}]
|
|
||||||
(let [selected-ids (selected->ids (assoc request :query-params (:search-params (:snapshot multi-form-state))) (:search-params (:snapshot multi-form-state)))
|
|
||||||
all-ids (all-ids-not-locked selected-ids)
|
|
||||||
invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec all-ids))]
|
|
||||||
(assert-percentages-add-up (:snapshot multi-form-state))
|
|
||||||
|
|
||||||
(doseq [a (-> multi-form-state :snapshot :expense-accounts)
|
(defn- footer* [request]
|
||||||
|
(sel/raw
|
||||||
|
(str "<div class=\"flex justify-end\"><div class=\"flex items-baseline gap-x-4\">"
|
||||||
|
(form-errors-html (:errors (:form-errors request)))
|
||||||
|
(str (sc/button {:color :primary :x-ref "next" :class "w-32" :type "submit"} "Save"))
|
||||||
|
"</div></div>")))
|
||||||
|
|
||||||
|
(defn render-form
|
||||||
|
"Renders the whole plain bulk-edit form (no wizard). Binds *errors* so the field-level
|
||||||
|
lookups resolve. Reuses the edit modal chrome."
|
||||||
|
[request]
|
||||||
|
(binding [*errors* (or (:form-errors request) {})]
|
||||||
|
(let [ids (:ids (:bulk-state request))
|
||||||
|
ids-hidden (apply str
|
||||||
|
(map-indexed (fn [i id]
|
||||||
|
(str (sc/hidden {:name (path->name2 :ids i) :value id})))
|
||||||
|
ids))
|
||||||
|
body (str "<div class=\"space-y-4 p-4\">"
|
||||||
|
(str (sc/validated-field
|
||||||
|
{:errors (ferr :expense-accounts)}
|
||||||
|
(sel/raw (str (account-grid* request)))))
|
||||||
|
"</div>")
|
||||||
|
modal-card (sel/render "templates/transaction-edit/edit-modal.html"
|
||||||
|
{:head (str "<div class=\"p-2\">Bulk editing " (count ids) " invoices</div>")
|
||||||
|
:side_panel nil
|
||||||
|
:body body
|
||||||
|
:footer (str (footer* request))})]
|
||||||
|
(sel/render->hiccup
|
||||||
|
"templates/invoice-bulk-edit/edit-form.html"
|
||||||
|
{:ids_hidden ids-hidden
|
||||||
|
:form_attrs (sc/attrs->str {:hx-ext "response-targets"
|
||||||
|
:hx-swap "outerHTML"
|
||||||
|
:hx-target-400 "#form-errors .error-content"
|
||||||
|
:hx-trigger "submit"
|
||||||
|
:hx-target "this"
|
||||||
|
:hx-put (bidi/path-for ssr-routes/only-routes ::route/bulk-edit-submit)})
|
||||||
|
:modal (str (sc/modal {:id "wizardmodal"} (sel/raw modal-card)))}))))
|
||||||
|
|
||||||
|
(defn apply-new-account
|
||||||
|
"bulk-edit-form-changed op: append a fresh (blank, Shared) expense-account row."
|
||||||
|
[request]
|
||||||
|
(let [accounts (vec (:expense-accounts (:bulk-state request)))
|
||||||
|
new-account {:db/id (str (java.util.UUID/randomUUID))
|
||||||
|
:new? true
|
||||||
|
:location "Shared"
|
||||||
|
:percentage nil}]
|
||||||
|
(assoc-in request [:bulk-state :expense-accounts] (conj accounts new-account))))
|
||||||
|
|
||||||
|
(defn apply-remove-account
|
||||||
|
"bulk-edit-form-changed op: remove the expense-account row at form-param row-index."
|
||||||
|
[request]
|
||||||
|
(let [row-index (some-> request :form-params (get "row-index") Integer/parseInt)
|
||||||
|
accounts (vec (:expense-accounts (:bulk-state request)))
|
||||||
|
updated-accounts (if (and row-index (< row-index (count accounts)))
|
||||||
|
(vec (concat (subvec accounts 0 row-index)
|
||||||
|
(subvec accounts (inc row-index))))
|
||||||
|
accounts)]
|
||||||
|
(assoc-in request [:bulk-state :expense-accounts] updated-accounts)))
|
||||||
|
|
||||||
|
(defn bulk-edit-form-changed-handler
|
||||||
|
"Single whole-form re-render endpoint. Dispatches on `op` (add/remove a row); a missing
|
||||||
|
op (an account-selection location swap or a percentage keyup) just re-renders, and the
|
||||||
|
caller's hx-select picks the cell / #expense-totals it needs."
|
||||||
|
[request]
|
||||||
|
(let [op (get-in request [:form-params "op"])
|
||||||
|
request' (case op
|
||||||
|
"new-account" (apply-new-account request)
|
||||||
|
"remove-account" (apply-remove-account request)
|
||||||
|
request)]
|
||||||
|
(html-response (render-form request'))))
|
||||||
|
|
||||||
|
(defn open-handler [request]
|
||||||
|
(modal-response
|
||||||
|
(sel/render->hiccup "templates/transaction-edit/transitioner.html"
|
||||||
|
{:body (str (render-form request))})))
|
||||||
|
|
||||||
|
(defn- render-form-response [request]
|
||||||
|
(html-response (render-form request)
|
||||||
|
:headers {"HX-reswap" "outerHTML"}))
|
||||||
|
|
||||||
|
(defn submit
|
||||||
|
"Validates the posted expense-account coding (schema field errors + the percentage-sum
|
||||||
|
and per-account location checks), then applies it across every selected (not-locked)
|
||||||
|
invoice."
|
||||||
|
[request]
|
||||||
|
(let [{:keys [ids expense-accounts]} (:bulk-state request)
|
||||||
|
invoices (pull-many (dc/db conn) '[:db/id :invoice/total {:invoice/client [:client/locations]}] (vec ids))]
|
||||||
|
(assert-schema bulk-edit-schema (select-keys (:bulk-state request) bulk-edit-form-keys))
|
||||||
|
(assert-percentages-add-up {:expense-accounts expense-accounts})
|
||||||
|
(doseq [a expense-accounts
|
||||||
:let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]]
|
:let [{:keys [:account/location :account/name]} (dc/pull (dc/db conn) [:account/location :account/name] (:account a))]]
|
||||||
(when (and location (not= location (:location a)))
|
(when (and location (not= location (:location a)))
|
||||||
(let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)]
|
(let [err (str "Account " name " uses location " (:location a) ", but is supposed to be " location)]
|
||||||
(throw (ex-info err {:validation-error err})))))
|
(throw (ex-info err {:validation-error err})))))
|
||||||
(alog/info ::bulk-code :count (count all-ids))
|
(alog/info ::bulk-code :count (count ids))
|
||||||
(audit-transact-batch
|
(audit-transact-batch
|
||||||
(map (fn [i]
|
(map (fn [i]
|
||||||
[:upsert-invoice {:db/id (:db/id i)
|
[:upsert-invoice {:db/id (:db/id i)
|
||||||
:invoice/expense-accounts (maybe-code-accounts i (-> multi-form-state :snapshot :expense-accounts) (-> i :invoice/client :client/locations))}])
|
:invoice/expense-accounts (maybe-code-accounts i expense-accounts (-> i :invoice/client :client/locations))}])
|
||||||
invoices)
|
invoices)
|
||||||
(:identity request))
|
(:identity request))
|
||||||
|
|
||||||
(html-response
|
(html-response
|
||||||
[:div]
|
[:div]
|
||||||
:headers (cond-> {"hx-trigger" (hx/json {"modalclose" ""
|
:headers {"hx-trigger" (hx/json {"modalclose" ""
|
||||||
"invalidated" ""
|
"invalidated" ""
|
||||||
"notification" (str "Successfully coded " (count all-ids) " invoices.")})
|
"notification" (str "Successfully coded " (count ids) " invoices.")})
|
||||||
"hx-reswap" "outerHTML"})))))
|
"hx-reswap" "outerHTML"})))
|
||||||
|
|
||||||
(def bulk-edit-wizard (->BulkEditWizard nil nil))
|
|
||||||
|
|
||||||
(defn bulk-edit-total* [request]
|
|
||||||
(let [total (->> (-> request
|
|
||||||
:multi-form-state
|
|
||||||
:step-params
|
|
||||||
:expense-accounts)
|
|
||||||
(map (fnil :percentage 0.0))
|
|
||||||
(filter number?)
|
|
||||||
(reduce + 0.0))]
|
|
||||||
(format "%.1f%%" (* 100.0 total))))
|
|
||||||
|
|
||||||
(defn bulk-edit-balance* [request]
|
|
||||||
(let [total (->> (-> request
|
|
||||||
:multi-form-state
|
|
||||||
:step-params
|
|
||||||
:expense-accounts)
|
|
||||||
(map (fnil :percentage 0.0))
|
|
||||||
(filter number?)
|
|
||||||
(reduce + 0.0))
|
|
||||||
balance (- 100.0
|
|
||||||
(* 100.0 total))]
|
|
||||||
[:span {:class (when-not (dollars= 0.0 balance)
|
|
||||||
"text-red-300")}
|
|
||||||
(format "%.1f%%" balance)]))
|
|
||||||
|
|
||||||
(defn bulk-edit-total [request]
|
|
||||||
(html-response (bulk-edit-total* request)))
|
|
||||||
|
|
||||||
(defn bulk-edit-balance [request]
|
|
||||||
(html-response (bulk-edit-balance* request)))
|
|
||||||
|
|
||||||
(def key->handler
|
(def key->handler
|
||||||
(apply-middleware-to-all-handlers
|
(apply-middleware-to-all-handlers
|
||||||
@@ -1737,32 +1831,14 @@
|
|||||||
::route/legacy-paid-invoices (redirect-handler ::route/paid-page)
|
::route/legacy-paid-invoices (redirect-handler ::route/paid-page)
|
||||||
::route/legacy-voided-invoices (redirect-handler ::route/voided-page)
|
::route/legacy-voided-invoices (redirect-handler ::route/voided-page)
|
||||||
::route/legacy-new-invoice (redirect-handler ::route/new-wizard)
|
::route/legacy-new-invoice (redirect-handler ::route/new-wizard)
|
||||||
::route/bulk-edit (-> mm/open-wizard-handler
|
::route/bulk-edit (-> open-handler
|
||||||
(mm/wrap-wizard bulk-edit-wizard)
|
(wrap-bulk-state))
|
||||||
(mm/wrap-init-multi-form-state initial-bulk-edit-state))
|
::route/bulk-edit-submit (-> submit
|
||||||
::route/bulk-edit-submit (-> mm/submit-handler
|
(wrap-form-4xx-2 render-form-response)
|
||||||
(mm/wrap-wizard bulk-edit-wizard)
|
(wrap-bulk-state)
|
||||||
(mm/wrap-decode-multi-form-state)
|
|
||||||
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
||||||
::route/bulk-edit-total (-> bulk-edit-total
|
::route/bulk-edit-form-changed (-> bulk-edit-form-changed-handler
|
||||||
(mm/wrap-wizard bulk-edit-wizard)
|
(wrap-bulk-state))
|
||||||
(mm/wrap-decode-multi-form-state)
|
|
||||||
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
|
||||||
::route/bulk-edit-balance (-> bulk-edit-balance
|
|
||||||
|
|
||||||
(mm/wrap-wizard bulk-edit-wizard)
|
|
||||||
(mm/wrap-decode-multi-form-state)
|
|
||||||
(wrap-must {:subject :invoice :activity :bulk-edit}))
|
|
||||||
::route/bulk-edit-new-account (->
|
|
||||||
(add-new-entity-handler [:step-params :expense-accounts]
|
|
||||||
(fn render [cursor request]
|
|
||||||
(bulk-edit-account-row*
|
|
||||||
{:value cursor}))
|
|
||||||
(fn build-new-row [base _]
|
|
||||||
(assoc base :invoice-expense-account/location "Shared")))
|
|
||||||
(wrap-schema-enforce :query-schema [:map
|
|
||||||
[:client-id {:optional true}
|
|
||||||
[:maybe entity-id]]]))
|
|
||||||
|
|
||||||
::route/undo-autopay (-> undo-autopay
|
::route/undo-autopay (-> undo-autopay
|
||||||
(wrap-entity [:route-params :db/id] default-read)
|
(wrap-entity [:route-params :db/id] default-read)
|
||||||
|
|||||||
@@ -33,9 +33,7 @@
|
|||||||
:delete ::bulk-delete-confirm}
|
:delete ::bulk-delete-confirm}
|
||||||
"/bulk-edit" {:get ::bulk-edit
|
"/bulk-edit" {:get ::bulk-edit
|
||||||
:put ::bulk-edit-submit
|
:put ::bulk-edit-submit
|
||||||
"/account" ::bulk-edit-new-account
|
"/form-changed" ::bulk-edit-form-changed}
|
||||||
"/total" ::bulk-edit-total
|
|
||||||
"/balance" ::bulk-edit-balance}
|
|
||||||
["/" [#"\d+" :db/id]] {:delete ::delete
|
["/" [#"\d+" :db/id]] {:delete ::delete
|
||||||
"/undo-autopay" ::undo-autopay
|
"/undo-autopay" ::undo-autopay
|
||||||
"/unvoid" ::unvoid
|
"/unvoid" ::unvoid
|
||||||
|
|||||||
Reference in New Issue
Block a user