614 lines
29 KiB
Markdown
614 lines
29 KiB
Markdown
# Inline Account Editing Implementation Plan
|
|
|
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
|
|
**Goal:** Replace the sales summary wizard's flat data-grid with a two-column debit/credit layout matching the embedded grid, and add HTMX-based inline account editing (click pencil → typeahead → confirm → swap back to display mode).
|
|
|
|
**Architecture:** Each item's account cell renders in "display mode" (account name + hidden input + pencil icon). Clicking the pencil fires an HTMX GET that swaps in a typeahead + confirm/cancel buttons. Confirm fires an HTMX PUT that swaps back to display mode with an updated hidden input. No DB writes until the wizard form is submitted.
|
|
|
|
**Tech Stack:** Clojure, Hiccup, HTMX, Alpine.js (for typeahead), form-cursor, multi-modal wizard middleware, Datomic.
|
|
|
|
---
|
|
|
|
### Task 1: Add route keys to route definitions
|
|
|
|
**Files:**
|
|
- Modify: `src/cljc/auto_ap/routes/pos/sales_summaries.cljc`
|
|
|
|
- [ ] **Step 1: Add three new route keys**
|
|
|
|
Add these routes inside the existing `routes` map, alongside `"/edit/sales-summary-item"`:
|
|
|
|
```clojure
|
|
"/edit/item-account" ::edit-item-account
|
|
"/edit/save-item-account" ::save-item-account
|
|
"/edit/cancel-item-account" ::cancel-item-account
|
|
```
|
|
|
|
The full routes map should become:
|
|
|
|
```clojure
|
|
(def routes {"" {:get ::page
|
|
:put ::edit-wizard-submit}
|
|
"/table" ::table
|
|
["/" [#"\d+" :db/id]] {:get ::edit-wizard}
|
|
"/edit/navigate" ::edit-wizard-navigate
|
|
"/edit/sales-summary-item" ::new-summary-item
|
|
"/edit/item-account" ::edit-item-account
|
|
"/edit/save-item-account" ::save-item-account
|
|
"/edit/cancel-item-account" ::cancel-item-account})
|
|
```
|
|
|
|
- [ ] **Step 2: Verify the route file parses**
|
|
|
|
Run: `clj -M:check` or similar. If no checker is available, move on — the Clojure compiler will catch errors at load time.
|
|
|
|
---
|
|
|
|
### Task 2: Add account display cell helper and account edit cell helper
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
These are pure rendering functions — no routes, no handlers, just hiccup.
|
|
|
|
- [ ] **Step 1: Make `account-typeahead*` public**
|
|
|
|
Change `defn-` to `defn` for `account-typeahead*` so it can be used from the new handlers:
|
|
|
|
```clojure
|
|
(defn account-typeahead*
|
|
[{:keys [name value client-id]}]
|
|
[:div.flex.flex-col
|
|
(com/typeahead {:name name
|
|
:placeholder "Search..."
|
|
:url (hu/url (bidi/path-for ssr-routes/only-routes :account-search)
|
|
{:client-id client-id
|
|
:purpose "invoice"})
|
|
:value value
|
|
:content-fn (fn [value]
|
|
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value)
|
|
client-id)))})])
|
|
```
|
|
|
|
- [ ] **Step 2: Add `account-display-cell` function**
|
|
|
|
This renders the display-mode account cell: account name (or "Missing acct" pill), hidden input, and pencil icon. Insert after the `truncate` defn:
|
|
|
|
```clojure
|
|
(defn account-display-cell [{:keys [item field-name-prefix client-id]}]
|
|
(let [account-id (:ledger-mapped/account item)
|
|
account-name (when account-id
|
|
(:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read account-id)
|
|
client-id)))]
|
|
[:div.flex.items-center.gap-2
|
|
(com/hidden {:name (str field-name-prefix "[ledger-mapped/account]")
|
|
:value (or account-id "")})
|
|
(if account-id
|
|
[:span.text-sm account-name]
|
|
(com/pill {:color :red} "Missing acct"))
|
|
(com/a-icon-button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-item-account)
|
|
:hx-target "closest td"
|
|
:hx-swap "innerHTML"
|
|
:hx-vals (hx/json {:item-index (or (:item-index item) 0)
|
|
:client-id client-id
|
|
:current-account-id (or account-id "")})}
|
|
svg/pencil)]))
|
|
```
|
|
|
|
- [ ] **Step 3: Add `account-edit-cell` function**
|
|
|
|
This renders the edit-mode account cell: typeahead + confirm/cancel buttons. This is what `::route/edit-item-account` returns:
|
|
|
|
```clojure
|
|
(defn account-edit-cell [{:keys [field-name-prefix client-id current-account-id]}]
|
|
(let [account-input-name (str field-name-prefix "[ledger-mapped/account]")]
|
|
[:div.flex.flex-col.gap-2
|
|
(account-typeahead* {:name account-input-name
|
|
:value current-account-id
|
|
:client-id client-id})
|
|
[:div.flex.gap-1
|
|
(com/a-icon-button {:hx-put (bidi/path-for ssr-routes/only-routes ::route/save-item-account)
|
|
:hx-target "closest td"
|
|
:hx-swap "innerHTML"
|
|
:hx-include "closest td"
|
|
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
|
:client-id client-id})}
|
|
svg/check)
|
|
(com/a-icon-button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/cancel-item-account)
|
|
:hx-target "closest td"
|
|
:hx-swap "innerHTML"
|
|
:hx-vals (hx/json {:field-name-prefix field-name-prefix
|
|
:client-id client-id
|
|
:current-account-id (or current-account-id "")})}
|
|
svg/x)]]))
|
|
```
|
|
|
|
**Note:** We construct the input name directly from `field-name-prefix` + `[ledger-mapped/account]` instead of using form-cursor, because the HTMX handler doesn't have access to the wizard's form state. The typeahead component accepts a `:name` string directly.
|
|
|
|
---
|
|
|
|
### Task 3: Verify svg/check exists
|
|
|
|
**Files:**
|
|
- Check: `src/clj/auto_ap/ssr/svg.clj`
|
|
|
|
- [ ] **Step 1: Search for check icon**
|
|
|
|
Run: `rg "def.*check" src/clj/auto_ap/ssr/svg.clj`
|
|
|
|
If `svg/check` does not exist, look for alternatives like `svg/tick`, `svg/confirm`, or `svg/save`. If none exist, use `svg/pencil` with a different label, or use a simple `[:span "✓"]` instead.
|
|
|
|
---
|
|
|
|
### Task 4: Rewrite MainStep render-step to use two-column layout
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
This is the core UI change. Replace the flat data-grid in `render-step` with a two-column layout matching the embedded grid.
|
|
|
|
- [ ] **Step 1: Replace the MainStep record's render-step body**
|
|
|
|
Replace the existing `render-step` implementation in the `defrecord MainStep` with:
|
|
|
|
```clojure
|
|
(render-step
|
|
[this {:keys [multi-form-state] :as request}]
|
|
(let [client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))
|
|
items (sort-items (:sales-summary/items (:step-params multi-form-state)))
|
|
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side %)) items)
|
|
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side %)) items)
|
|
max-rows (max (count debit-items) (count credit-items))
|
|
padded-debits (concat debit-items (repeat (- max-rows (count debit-items)) nil))
|
|
padded-credits (concat credit-items (repeat (- max-rows (count credit-items)) nil))]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head [:div.p-2 "Edit Summary"]
|
|
:body (mm/default-step-body
|
|
{}
|
|
[:div
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
[:div.grid.grid-cols-2.gap-4
|
|
[:div
|
|
[:div.font-semibold.text-sm.mb-2 "Debits"]
|
|
[:div.space-y-1
|
|
(for [[idx item] (map-indexed vector padded-debits)]
|
|
(if item
|
|
(let [manual? (:sales-summary-item/manual? item)]
|
|
[:div.flex.items-center.gap-2.text-sm
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" idx "][db/id]")
|
|
:value (:db/id item)})
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" idx "][sales-summary-item/category]")
|
|
:value (:sales-summary-item/category item)})
|
|
(when manual?
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" idx "][sales-summary-item/manual?]")
|
|
:value "true"}))
|
|
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
|
(account-display-cell {:item (assoc item :item-index idx)
|
|
:field-name-prefix (str "step-params[sales-summary/items][" idx "]")
|
|
:client-id client-id})
|
|
[:span.font-mono (format "$%,.2f" (:ledger-mapped/amount item))]])
|
|
[:div.h-6]))]
|
|
(summary-total-row* request)
|
|
(unbalanced-row* request)]
|
|
[:div
|
|
[:div.font-semibold.text-sm.mb-2 "Credits"]
|
|
[:div.space-y-1
|
|
(for [[idx item] (map-indexed vector padded-credits)]
|
|
(if item
|
|
(let [actual-idx (+ (count debit-items) idx)
|
|
manual? (:sales-summary-item/manual? item)]
|
|
[:div.flex.items-center.gap-2.text-sm
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
|
:value (:db/id item)})
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/category]")
|
|
:value (:sales-summary-item/category item)})
|
|
(when manual?
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/manual?]")
|
|
:value "true"}))
|
|
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
|
(account-display-cell {:item (assoc item :item-index actual-idx)
|
|
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
|
:client-id client-id})
|
|
[:span.font-mono (format "$%,.2f" (:ledger-mapped/amount item))]])
|
|
[:div.h-6]))]
|
|
(summary-total-row* request)
|
|
(unbalanced-row* request)]]
|
|
[:div.mt-4
|
|
(fc/with-field :sales-summary/items
|
|
(com/data-grid-new-row {:colspan 2
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
|
|
:row-offset 0
|
|
:index (count (fc/field-value))
|
|
:tr-params {:hx-vals (hx/json {:client-id client-id})}}
|
|
"New Summary Item"))]])
|
|
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate)
|
|
:validation-route ::route/edit-wizard-navigate
|
|
:width-height-class "lg:w-[900px] lg:h-[600px]")))
|
|
```
|
|
|
|
**Important note on item indexing:** The padded lists are for display alignment only. The hidden inputs must use the *actual* index in the `:sales-summary/items` vector, not the display index. Debit items keep their original indices; credit items' indices start after all debit items. This is a simplification — if items are interspersed (debit, credit, debit), this approach breaks. We need to compute actual indices from the sorted list, not from the filtered sublists. See Step 2.
|
|
|
|
- [ ] **Step 2: Fix index calculation to use actual sorted position**
|
|
|
|
The approach in Step 1 has an indexing bug. Items in the form are stored as a vector and submitted by index. We must preserve the actual vector index for each item. Replace the layout logic with:
|
|
|
|
```clojure
|
|
(render-step
|
|
[this {:keys [multi-form-state] :as request}]
|
|
(let [client-id (:db/id (:sales-summary/client (:snapshot multi-form-state)))
|
|
items (sort-items (:sales-summary/items (:step-params multi-form-state)))
|
|
indexed-items (map-indexed vector items)
|
|
debit-items (filter #(= :ledger-side/debit (:ledger-mapped/ledger-side (second %))) indexed-items)
|
|
credit-items (filter #(= :ledger-side/credit (:ledger-mapped/ledger-side (second %))) indexed-items)
|
|
max-rows (max (count debit-items) (count credit-items))
|
|
padded-debits (concat debit-items (repeat (- max-rows (count debit-items)) nil))
|
|
padded-credits (concat credit-items (repeat (- max-rows (count credit-items)) nil))]
|
|
(mm/default-render-step
|
|
linear-wizard this
|
|
:head [:div.p-2 "Edit Summary"]
|
|
:body (mm/default-step-body
|
|
{}
|
|
[:div
|
|
(fc/with-field :db/id
|
|
(com/hidden {:name (fc/field-name)
|
|
:value (fc/field-value)}))
|
|
[:div.grid.grid-cols-2.gap-4
|
|
[:div
|
|
[:div.font-semibold.text-sm.mb-2 "Debits"]
|
|
[:div.space-y-1
|
|
(for [[actual-idx item] padded-debits]
|
|
(if item
|
|
(let [manual? (:sales-summary-item/manual? item)]
|
|
[:div.flex.items-center.gap-2.text-sm
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
|
:value (:db/id item)})
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/category]")
|
|
:value (:sales-summary-item/category item)})
|
|
(when manual?
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/manual?]")
|
|
:value "true"}))
|
|
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
|
(account-display-cell {:item (assoc item :item-index actual-idx)
|
|
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
|
:client-id client-id})
|
|
[:span.font-mono (format "$%,.2f" (:ledger-mapped/amount item))]])
|
|
[:div.h-6]))]
|
|
[:div
|
|
[:div.font-semibold.text-sm.mb-2 "Credits"]
|
|
[:div.space-y-1
|
|
(for [[actual-idx item] padded-credits]
|
|
(if item
|
|
(let [manual? (:sales-summary-item/manual? item)]
|
|
[:div.flex.items-center.gap-2.text-sm
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
|
:value (:db/id item)})
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/category]")
|
|
:value (:sales-summary-item/category item)})
|
|
(when manual?
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/manual?]")
|
|
:value "true"}))
|
|
[:span.text-gray-500 (truncate (:sales-summary-item/category item) 30)]
|
|
(account-display-cell {:item (assoc item :item-index actual-idx)
|
|
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
|
:client-id client-id})
|
|
[:span.font-mono (format "$%,.2f" (:ledger-mapped/amount item))]])
|
|
[:div.h-6]))]]]
|
|
[:div.mt-4
|
|
(fc/with-field :sales-summary/items
|
|
(com/data-grid-new-row {:colspan 2
|
|
:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-summary-item)
|
|
:row-offset 0
|
|
:index (count (fc/field-value))
|
|
:tr-params {:hx-vals (hx/json {:client-id client-id})}}
|
|
"New Summary Item"))]])
|
|
|
|
:footer
|
|
(mm/default-step-footer linear-wizard this :validation-route ::route/edit-wizard-navigate)
|
|
:validation-route ::route/edit-wizard-navigate
|
|
:width-height-class "lg:w-[900px] lg:h-[600px]")))
|
|
```
|
|
|
|
**Key design decisions:**
|
|
- `map-indexed vector items` preserves actual vector position for hidden input names
|
|
- `padded-debits` / `padded-credits` are sequences of `[actual-idx item]` or `nil` for padding rows
|
|
- Padding rows render as empty `[:div.h-6]` to maintain alignment
|
|
- Total/unbalanced rows are not repeated per column — they go below the two-column grid, shared
|
|
|
|
- [ ] **Step 3: Move total/unbalanced rows outside the two-column grid**
|
|
|
|
The `summary-total-row*` and `unbalanced-row*` functions currently render as `<tr>` elements inside a data-grid. In the new layout, these should be simple flex rows below the grid, not table rows. For now, keep them as-is but render them in a single section below both columns (remove the duplicate from the credit column). Adjust the `:body` content:
|
|
|
|
After the `[:div.grid.grid-cols-2.gap-4 ...]` block, add:
|
|
|
|
```clojure
|
|
[:div.mt-2.border-t.pt-2
|
|
(summary-total-row* request)
|
|
(unbalanced-row* request)]
|
|
```
|
|
|
|
But since `summary-total-row*` and `unbalanced-row*` currently return `<tr>` elements, they won't render correctly outside a table. For the initial implementation, replace them with inline hiccup that renders the same info in a flex layout. See Task 5.
|
|
|
|
---
|
|
|
|
### Task 5: Rewrite total and unbalanced display as non-table hiccup
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
The existing `summary-total-row*` and `unbalanced-row*` return `<tr>` / `<td>` elements for the data-grid. The new layout is not a table, so these need to be simple div-based layouts.
|
|
|
|
- [ ] **Step 1: Add `summary-total-display` function**
|
|
|
|
Insert after `unbalanced-row*`:
|
|
|
|
```clojure
|
|
(defn summary-total-display [request]
|
|
(let [total-credits (-> request
|
|
:multi-form-state
|
|
:step-params
|
|
:sales-summary/items
|
|
(total-credits))
|
|
total-debits (-> request
|
|
:multi-form-state
|
|
:step-params
|
|
:sales-summary/items
|
|
(total-debits))]
|
|
[:div.flex.justify-between.text-sm.py-1
|
|
[:span.font-semibold "Total"]
|
|
[:div.flex.gap-8
|
|
[:span.font-mono (format "$%,.2f" total-debits)]
|
|
[:span.font-mono (format "$%,.2f" total-credits)]]]))
|
|
```
|
|
|
|
- [ ] **Step 2: Add `unbalanced-display` function**
|
|
|
|
```clojure
|
|
(defn unbalanced-display [request]
|
|
(let [total-credits (-> request
|
|
:multi-form-state
|
|
:step-params
|
|
:sales-summary/items
|
|
(total-credits))
|
|
total-debits (-> request
|
|
:multi-form-state
|
|
:step-params
|
|
:sales-summary/items
|
|
(total-debits))
|
|
delta (- total-debits total-credits)]
|
|
(when-not (dollars-0? delta)
|
|
[:div.flex.justify-between.text-sm.py-1
|
|
[:span.font-semibold {:class (if (pos? delta) "text-red-600" "text-green-600")} "Unbalanced"]
|
|
[:div.flex.gap-8
|
|
[:span.font-mono (when (pos? delta) (format "$%,.2f" delta))]
|
|
[:span.font-mono (when (neg? delta) (format "$%,.2f" (Math/abs delta)))]]]])))
|
|
```
|
|
|
|
- [ ] **Step 3: Use these in the render-step body**
|
|
|
|
In Task 4's render-step, replace the total/unbalanced section at the bottom with:
|
|
|
|
```clojure
|
|
[:div.mt-2.border-t.pt-2
|
|
(summary-total-display request)
|
|
(unbalanced-display request)]
|
|
```
|
|
|
|
---
|
|
|
|
### Task 6: Add `edit-item-account` handler
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
- [ ] **Step 1: Write the handler**
|
|
|
|
Insert before `key->handler`:
|
|
|
|
```clojure
|
|
(defn edit-item-account [request]
|
|
(let [{:keys [item-index client-id current-account-id]} (:query-params request)
|
|
item-index (if (string? item-index) (Integer/parseInt item-index) item-index)
|
|
field-name-prefix (str "step-params[sales-summary/items][" item-index "]")]
|
|
(html-response
|
|
(account-edit-cell {:field-name-prefix field-name-prefix
|
|
:client-id (if (string? client-id) (Long/parseLong client-id) client-id)
|
|
:current-account-id (when (and current-account-id
|
|
(not= current-account-id ""))
|
|
(if (string? current-account-id)
|
|
(Long/parseLong current-account-id)
|
|
current-account-id))}))))
|
|
```
|
|
|
|
- [ ] **Step 2: Add it to key->handler**
|
|
|
|
Add to the handler map inside `key->handler`:
|
|
|
|
```clojure
|
|
::route/edit-item-account (-> edit-item-account
|
|
(wrap-schema-enforce :query-schema [:map
|
|
[:item-index nat-int?]
|
|
[:client-id {:optional true} [:maybe entity-id]]
|
|
[:current-account-id {:optional true} [:maybe :string]]]))
|
|
```
|
|
|
|
---
|
|
|
|
### Task 7: Add `save-item-account` handler
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
- [ ] **Step 1: Write the handler**
|
|
|
|
This handler receives the typeahead's selected value via `hx-include "closest td"`, which includes the hidden input from the typeahead. The typeahead's hidden input name will be something like `step-params[sales-summary/items][2][ledger-mapped/account]`. We need to extract the selected account ID from the form params and return a display cell with the updated value.
|
|
|
|
```clojure
|
|
(defn save-item-account [request]
|
|
(let [{:keys [field-name-prefix client-id]} (some-> request :query-params)
|
|
account-input-name (str field-name-prefix "[ledger-mapped/account]")
|
|
account-id-str (get-in request [:form-params account-input-name])
|
|
account-id (when (and account-id-str (not= account-id-str ""))
|
|
(Long/parseLong account-id-str))
|
|
item {:ledger-mapped/account account-id
|
|
:item-index (second (re-find #"\[(\d+)\]" field-name-prefix))}]
|
|
(html-response
|
|
(account-display-cell {:item item
|
|
:field-name-prefix field-name-prefix
|
|
:client-id (if (string? client-id) (Long/parseLong client-id) client-id)}))))
|
|
```
|
|
|
|
**Note:** `field-name-prefix` comes from `hx-vals` in the confirm button. `account-input-name` is constructed by appending `[ledger-mapped/account]` to the prefix. The typeahead's hidden input will have this name.
|
|
|
|
- [ ] **Step 2: Add it to key->handler**
|
|
|
|
```clojure
|
|
::route/save-item-account (-> save-item-account
|
|
(mm/wrap-wizard edit-wizard)
|
|
(mm/wrap-decode-multi-form-state))
|
|
```
|
|
|
|
Wait — we said no DB writes until wizard submit. So we should NOT wrap with `wrap-wizard` and `wrap-decode-multi-form-state`. The handler just returns HTML. It doesn't need the wizard state. The form params contain the typeahead value, and the query params contain the field name prefix and client-id. Simple.
|
|
|
|
```clojure
|
|
::route/save-item-account save-item-account
|
|
```
|
|
|
|
---
|
|
|
|
### Task 8: Add `cancel-item-account` handler
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
- [ ] **Step 1: Write the handler**
|
|
|
|
```clojure
|
|
(defn cancel-item-account [request]
|
|
(let [{:keys [field-name-prefix client-id current-account-id]} (:query-params request)
|
|
account-id (when (and current-account-id (not= current-account-id ""))
|
|
(if (string? current-account-id)
|
|
(Long/parseLong current-account-id)
|
|
current-account-id))
|
|
item {:ledger-mapped/account account-id
|
|
:item-index (second (re-find #"\[(\d+)\]" field-name-prefix))}]
|
|
(html-response
|
|
(account-display-cell {:item item
|
|
:field-name-prefix field-name-prefix
|
|
:client-id (if (string? client-id) (Long/parseLong client-id) client-id)}))))
|
|
```
|
|
|
|
- [ ] **Step 2: Add it to key->handler**
|
|
|
|
```clojure
|
|
::route/cancel-item-account cancel-item-account
|
|
```
|
|
|
|
---
|
|
|
|
### Task 9: Wire routes in key->handler
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
- [ ] **Step 1: Add all three new handlers to key->handler**
|
|
|
|
The final additions to the handler map (before the closing `}`):
|
|
|
|
```clojure
|
|
::route/edit-item-account (-> edit-item-account
|
|
(wrap-schema-enforce :query-schema [:map
|
|
[:item-index nat-int?]
|
|
[:client-id {:optional true} [:maybe entity-id]]
|
|
[:current-account-id {:optional true} [:maybe :string]]]))
|
|
::route/save-item-account save-item-account
|
|
::route/cancel-item-account cancel-item-account
|
|
```
|
|
|
|
These get the same middleware applied via `apply-middleware-to-all-handlers` at the bottom of `key->handler`.
|
|
|
|
---
|
|
|
|
### Task 10: Handle manual items in the two-column layout
|
|
|
|
**Files:**
|
|
- Modify: `src/clj/auto_ap/ssr/pos/sales_summaries.clj`
|
|
|
|
Manual items have editable category text inputs and debit/credit money inputs. In the two-column layout, manual items need to stay in "edit mode" with their inputs visible.
|
|
|
|
- [ ] **Step 1: Add manual item rendering in the debit/credit columns**
|
|
|
|
In the render-step `for` loop, when `manual?` is true, render the editable fields instead of display-mode:
|
|
|
|
For a debit manual item:
|
|
|
|
```clojure
|
|
[:div.flex.items-center.gap-2.text-sm
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][db/id]")
|
|
:value (:db/id item)})
|
|
(com/hidden {:name (str "step-params[sales-summary/items][" actual-idx "][sales-summary-item/manual?]")
|
|
:value "true"})
|
|
(fc/start-form-with-prefix [(str "step-params[sales-summary/items][" actual-idx "]")]
|
|
item []
|
|
(fc/with-field :sales-summary-item/category
|
|
(com/text-input {:placeholder "Category/Explanation"
|
|
:name (fc/field-name)
|
|
:value (fc/field-value)
|
|
:class "w-32 text-sm"}))
|
|
(account-display-cell {:item (assoc item :item-index actual-idx)
|
|
:field-name-prefix (str "step-params[sales-summary/items][" actual-idx "]")
|
|
:client-id client-id}))
|
|
(fc/start-form-with-prefix [(str "step-params[sales-summary/items][" actual-idx "]")]
|
|
item []
|
|
(fc/with-field :debit
|
|
(com/money-input {:class "w-24 text-sm"
|
|
:name (fc/field-name)
|
|
:value (fc/field-value)})))]
|
|
```
|
|
|
|
For a credit manual item, replace `:debit` with `:credit`.
|
|
|
|
---
|
|
|
|
### Task 11: Test the complete flow end-to-end
|
|
|
|
**Files:**
|
|
- Manual testing
|
|
|
|
- [ ] **Step 1: Open a sales summary row in the wizard**
|
|
|
|
Verify the two-column layout renders correctly with debits on the left, credits on the right.
|
|
|
|
- [ ] **Step 2: Verify display-mode account cells**
|
|
|
|
Each item should show the account name (or "Missing acct" pill) + hidden input + pencil icon.
|
|
|
|
- [ ] **Step 3: Click a pencil icon**
|
|
|
|
The cell should swap to show a typeahead search + confirm (check) and cancel (X) buttons.
|
|
|
|
- [ ] **Step 4: Search and select an account in the typeahead**
|
|
|
|
After selecting, click confirm. The cell should swap back to display mode with the updated account name and hidden input value.
|
|
|
|
- [ ] **Step 5: Click cancel**
|
|
|
|
The cell should swap back to the original display mode.
|
|
|
|
- [ ] **Step 6: Submit the wizard**
|
|
|
|
All hidden inputs (including the updated account) should be submitted. Verify the transaction updates the correct accounts.
|
|
|
|
- [ ] **Step 7: Test manual items**
|
|
|
|
Add a new summary item. Verify it renders with editable category + money inputs. Verify the account cell still uses the pencil-to-typeahead pattern.
|
|
|
|
- [ ] **Step 8: Test total/unbalanced display**
|
|
|
|
Verify totals and unbalanced indicators update correctly (if the `expense-account-total` route is fixed — out of scope for this plan but note if broken).
|