# 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 `` 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 `` 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 `` / `` 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).