3 Commits

Author SHA1 Message Date
f239b114c3 Merge branch 'integreat-fix-errors' into staging 2026-05-24 21:54:54 -07:00
8e3aa13f4d fixes 2026-05-24 21:54:37 -07:00
5b2aba561c feat: support exact client code match in dropdown search
When typing in the company dropdown search, check for an exact match
on client code via Datomic before falling back to Solr name search.
This allows users to quickly find clients by typing their code (e.g. NGRV).
2026-05-23 13:24:40 -07:00
7 changed files with 5571 additions and 121 deletions

View File

@@ -5,7 +5,7 @@
"packages": {
"": {
"dependencies": {
"@opencode-ai/plugin": "1.14.31"
"@opencode-ai/plugin": "1.15.10"
}
},
"node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": {
@@ -87,32 +87,36 @@
]
},
"node_modules/@opencode-ai/plugin": {
"version": "1.14.31",
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.14.31.tgz",
"integrity": "sha512-ZF7UoNKtZDtgW/2KrcFw5I7R2HRj/NigBuRwKPonvSZS36LnghZ7PYcXYZFGCjEgBmLUMMrLVgxccKLyxsgB0g==",
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.15.10.tgz",
"integrity": "sha512-V2p7CvpBtKWB+FID7Dl1y0Ci02zUT40A9b2RD9R9BOiuD8ZcKhHWov+irN0xVJA0Eg6OhEBfA0lPKRn1FNKPlw==",
"license": "MIT",
"dependencies": {
"@opencode-ai/sdk": "1.14.31",
"effect": "4.0.0-beta.57",
"@opencode-ai/sdk": "1.15.10",
"effect": "4.0.0-beta.66",
"zod": "4.1.8"
},
"peerDependencies": {
"@opentui/core": ">=0.2.0",
"@opentui/solid": ">=0.2.0"
"@opentui/core": ">=0.2.15",
"@opentui/keymap": ">=0.2.15",
"@opentui/solid": ">=0.2.15"
},
"peerDependenciesMeta": {
"@opentui/core": {
"optional": true
},
"@opentui/keymap": {
"optional": true
},
"@opentui/solid": {
"optional": true
}
}
},
"node_modules/@opencode-ai/sdk": {
"version": "1.14.31",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.14.31.tgz",
"integrity": "sha512-QaV+ti3NYUITmgIDqtNMqGIYBXJOx2zheN1g+7w4HC8QQsbaW1c7glxXExQHRbdUzcQPP2vUQhnXOcEsTw5CcQ==",
"version": "1.15.10",
"resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.15.10.tgz",
"integrity": "sha512-CUhpmMGGOqzvPnNNjjWmEIodAfP6Qnuki2ChIUKWYF7UImZ4zUcMZnzO5BtUxu/Ni1P8qzWxDioXs+7aIZQEhA==",
"license": "MIT",
"dependencies": {
"cross-spawn": "7.0.6"
@@ -149,9 +153,9 @@
}
},
"node_modules/effect": {
"version": "4.0.0-beta.57",
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.57.tgz",
"integrity": "sha512-rg32VgXnLKaPRs9tbRDaZ5jxmzNY7ojXt85gSHGUTwdlbWH5Ik+OCUY2q14TXliygPGoHwCAvNWS4bQJOqf00g==",
"version": "4.0.0-beta.66",
"resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.66.tgz",
"integrity": "sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw==",
"license": "MIT",
"dependencies": {
"@standard-schema/spec": "^1.1.0",
@@ -167,9 +171,9 @@
}
},
"node_modules/fast-check": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.7.0.tgz",
"integrity": "sha512-NsZRtqvSSoCP0HbNjUD+r1JH8zqZalyp6gLY9e7OYs7NK9b6AHOs2baBFeBG7bVNsuoukh89x2Yg3rPsul8ziQ==",
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz",
"integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==",
"funding": [
{
"type": "individual",
@@ -216,9 +220,9 @@
"license": "Apache-2.0"
},
"node_modules/msgpackr": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.10.tgz",
"integrity": "sha512-iCZNq+HszvF+fC3anCm4nBmWEnbeIAfpDs6IStAEKhQ2YSgkjzVG2FF9XJqwwQh5bH3N9OUTUt4QwVN6MLMLtA==",
"version": "1.11.12",
"resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.12.tgz",
"integrity": "sha512-RBdJ1Un7yGlXWajrkxcSa93nvQ0w4zBf60c0yYv7YtBelP8H2FA7XsfBbMHtXKXUMUxH7zV3Zuozh+kUQWhHvg==",
"license": "MIT",
"optionalDependencies": {
"msgpackr-extract": "^3.0.2"
@@ -323,9 +327,9 @@
}
},
"node_modules/uuid": {
"version": "13.0.1",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.1.tgz",
"integrity": "sha512-9ezox2roIft6ExBVTVqibSd5dc5/47Sw/uY6b4SjQUT2TzQ0tltNquWA46y4xPQmdZYqvnio22SgWd41M86+jw==",
"version": "13.0.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz",
"integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
@@ -351,9 +355,9 @@
}
},
"node_modules/yaml": {
"version": "2.8.3",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz",
"integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==",
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
"bin": {
"yaml": "bin.mjs"

View File

@@ -1,14 +1,14 @@
import { test, expect } from '@playwright/test';
async function openEditModal(page: any) {
async function openEditModal(page: any, transactionIndex: number = 0) {
// Navigate to transactions page
await page.goto('/transaction2');
// Wait for the table to load
await page.waitForSelector('table tbody tr');
// Find and click the edit button for the test transaction
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').first();
// Find and click the edit button for the specified transaction
const editButton = page.locator('button[hx-get*="/transaction2/"][hx-get*="/edit"]').nth(transactionIndex);
await editButton.click();
// Wait for the modal to open
@@ -137,6 +137,49 @@ async function addNewAccount(page: any) {
await page.waitForTimeout(500);
}
async function setAccountLocation(page: any, rowIndex: number, location: string) {
const row = await findAccountRow(page, rowIndex);
const locationSelect = row.locator('select[name*="location"]').first();
// If the option doesn't exist, add it (for testing invalid locations)
const optionExists = await locationSelect.locator(`option[value="${location}"]`).count() > 0;
if (!optionExists) {
await locationSelect.evaluate((el: HTMLSelectElement, value: string) => {
const option = document.createElement('option');
option.value = value;
option.textContent = value;
el.appendChild(option);
}, location);
}
await locationSelect.selectOption(location);
await locationSelect.dispatchEvent('change');
await page.waitForTimeout(300);
}
async function getAccountLocation(page: any, rowIndex: number): Promise<string> {
const row = await findAccountRow(page, rowIndex);
const locationSelect = row.locator('select[name*="location"]').first();
return await locationSelect.inputValue();
}
async function removeAllAccounts(page: any) {
const allRows = page.locator('#account-grid-body tbody tr');
const rowCount = await allRows.count();
for (let i = rowCount - 1; i >= 0; i--) {
const row = allRows.nth(i);
const hasAccountInput = await row.locator('input[name*="transaction-account/account"]').count() > 0;
if (hasAccountInput) {
// Click the X button to remove (it's an <a> tag, not a <button>)
const removeButton = row.locator('a').first();
await removeButton.click();
// Wait for the Alpine.js removal animation (500ms + buffer)
await page.waitForTimeout(700);
}
}
}
async function saveTransaction(page: any) {
// Submit the form directly instead of clicking the button
// The Done button might not have type="submit"
@@ -175,6 +218,44 @@ async function toggleToDollarMode(page: any) {
test.describe.configure({ mode: 'serial' });
test.describe('Transaction Edit Shared Location', () => {
test('should spread Shared location to client locations on save and display correctly on reopen', async ({ page }) => {
// Use the second transaction to avoid interfering with other tests
const transactionIndex = 1;
// Step 1: Open edit modal and add an account with Shared location
await openEditModal(page, transactionIndex);
// Add a new account row
await addNewAccount(page);
// Select the account
await selectAccountFromTypeahead(page, 0, 'Test');
// Set location to Shared
await setAccountLocation(page, 0, 'Shared');
// Set amount to $200 (the full transaction amount for the second transaction)
await setAccountAmount(page, 0, '200');
// Save the transaction
await saveTransaction(page);
// Step 2: Re-open and verify location is not "Shared" but the actual client location
await openEditModal(page, transactionIndex);
// Wait for accounts to load
await page.waitForTimeout(500);
// Get the location of the first account
const location = await getAccountLocation(page, 0);
// The location should be the actual client location ("DT" in test data), not "Shared"
expect(location).not.toBe('Shared');
expect(location).toBe('DT');
});
});
test.describe('Transaction Edit Full Workflow', () => {
test('should code transaction with vendor using percentage, then split 50/50, then switch to dollars', async ({ page }) => {
// Step 1: Open edit modal and code with 100% to one account
@@ -246,14 +327,15 @@ test.describe('Transaction Edit Full Workflow', () => {
test.describe('Transaction Edit Validation', () => {
test('should show validation error when account totals do not match transaction amount', async ({ page }) => {
await openEditModal(page);
// Use the third transaction to avoid interference from other tests
await openEditModal(page, 2);
// Stay in dollar mode (default)
// Add an account
await addNewAccount(page);
await selectAccountFromTypeahead(page, 0, 'Test');
// Set amount to $50 (which doesn't match the $100 transaction)
// Set amount to $50 (which doesn't match the $300 transaction)
await setAccountAmount(page, 0, '50');
// Try to save - this should fail because $50 != $100
@@ -279,6 +361,12 @@ test.describe('Transaction Edit Validation', () => {
const amountInput = page.locator('input[name*="transaction-account/amount"]').first();
const value = await amountInput.inputValue();
expect(parseFloat(value)).toBeCloseTo(50.0, 1);
// Verify the user-friendly error message is displayed
const errorElement = page.locator('#form-errors .error-content');
await expect(errorElement).toBeVisible();
const errorText = await errorElement.textContent();
expect(errorText).toContain('The total of your expense accounts ($50.00) must equal the transaction amount ($300.00)');
});
});

5342
resources/public/js/htmx.js Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,25 +21,25 @@
[:li
[:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"}
(if group
[:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
[:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
:hx-headers (hx/json {"x-clients" (pr-str [:group group])})
"@click" (format "globalClientSelection={group: %s}" (hx/json group))
:hx-swap "outerHTML"
:hx-swap "outerHTML"
:hx-trigger "click"}
name]
[:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
[:a {:href "#" :class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
:hx-headers (format "{\"x-clients\": \"[%d]\"}" id)
"@click" (format "globalClientSelection={selected: [%d]}" id)
:hx-swap "outerHTML"
:hx-swap "outerHTML"
:hx-trigger "click"}
name])]])])
@@ -49,7 +49,7 @@
cleansed-search-query (str "name:(" query ")")
exec-search (fn []
(for [n (pull-many (dc/db conn) [:client/name :db/id]
(for [{:keys [id name]} (solr/query solr/impl "clients" {"query" cleansed-search-query
(for [{:keys [id name]} (solr/query solr/impl "clients" {"query" cleansed-search-query
"fields" "id, name"})
:let [client-id (Long/parseLong id)]
:when (can-see-client? identity client-id)]
@@ -61,15 +61,17 @@
:name (str "All clients matching " (subs query 2))}]
raw-query
(let [code-matches (for [n (pull-many (dc/db conn) [:client/name :db/id]
(for [{:keys [id name]} (solr/query solr/impl "clients" {"query" (format "code:\"%s\"" raw-query)
"fields" "id, name"})
:let [client-id (Long/parseLong id)]
:when (can-see-client? identity client-id)]
client-id))]
{:id (:db/id n)
:name (:client/name n)})]
(or (seq code-matches) (exec-search)))
(let [exact-code-matches (for [n (pull-many (dc/db conn) [:client/name :db/id]
(for [client-id (map first (dc/q '[:find ?e
:in $ ?code
:where [?e :client/code ?code]]
(dc/db conn)
query))
:when (can-see-client? identity client-id)]
client-id))]
{:id (:db/id n)
:name (:client/name n)})]
(or (seq exact-code-matches) (exec-search)))
cleansed-query
(exec-search))))
@@ -93,7 +95,7 @@
[:div
[:button#company-dropdown-button {:class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
"x-tooltip.on.click" "{content: ()=>$refs.tooltip.innerHTML, theme: 'light', onMount(i) { htmx.process(i.popper); }, allowHTML: true, interactive:true}"
:type "button"}
:type "button"}
(cond
(= :mine client-selection)
"My Companies"
@@ -119,43 +121,43 @@
svg/search]]
[:input#company-search {:placeholder "Company name"
:x-ref "company"
:name "search-text"
:class "block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
:autoFocus true
:tab-index -1
:hx-trigger "keyup changed delay:500ms, search"
:hx-get (bidi/path-for ssr-routes/only-routes
:company-dropdown-search-results)
:hx-target "#company-search-results"
:hx-swap "innerHTML"}]]
[:input#company-search-value {:type "hidden"
:name "x-clients"}]]
:name "search-text"
:class "block w-full p-2 pl-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-600 dark:border-gray-500 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500"
:autoFocus true
:tab-index -1
:hx-trigger "keyup changed delay:500ms, search"
:hx-get (bidi/path-for ssr-routes/only-routes
:company-dropdown-search-results)
:hx-target "#company-search-results"
:hx-swap "innerHTML"}]]
[:input#company-search-value {:type "hidden"
:name "x-clients"}]]
[:div.divide-y.divide-gray-100
[:div#company-search-results {:class "h-48 px-3 pb-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}]
(when (= "admin" (:user/role identity))
[:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"}
[:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
[:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
"@click" "globalClientSelection=\"mine\""
:hx-headers "{\"x-clients\": \":mine\"}"
:hx-swap "outerHTML"
:hx-swap "outerHTML"
:hx-trigger "click"}
"Mine"]])
[:div {:class "flex items-center pl-2 rounded hover:bg-green-100 dark:hover:bg-green-600"}
[:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
[:button {:class "w-full py-2 ml-2 text-sm font-medium text-gray-900 rounded dark:text-gray-300"
:hx-put (bidi/path-for ssr-routes/only-routes
:active-client
:request-method :put)
:hx-target "#company-dropdown"
"@click" "globalClientSelection=\"all\""
:hx-headers "{\"x-clients\": \":all\"}"
:hx-swap "outerHTML"
:hx-swap "outerHTML"
:hx-trigger "click"}
"All"]]]]]
[:script {:lang "text/javascript"}
@@ -188,7 +190,7 @@ function initCompanyDropdown() {
(defn active-client [{:keys [identity params] :as request}]
(assoc
(html-response
(dropdown {:client-selection (:client-selection request)
(dropdown {:client-selection (:client-selection request)
:clients (:clients request)
:client (:client request)
:identity identity}))

View File

@@ -215,11 +215,11 @@
(fn [{:keys [wizard multi-form-state] :as request}]
(assert-schema (step-schema (get-current-step wizard)) (:step-params multi-form-state))
(handler request))
(wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST
(html-response
(render-wizard wizard request)
:headers {"x-transition-type" "none"
"HX-reswap" "outerHTML"})))))
(wrap-form-4xx-2 (fn [{:keys [wizard] :as request}] ;; THIS MAY BE BETTER TO JUST MAKE THE LINEAR WIZARD POPULATE FROM THE REQUEST
(html-response
(render-wizard wizard request)
:headers {"x-transition-type" "none"
"HX-reswap" "outerHTML"})))))
(defn get-transition-type [wizard from-step-key to-step-key]
(let [to-step-index (.indexOf (steps wizard) to-step-key)

View File

@@ -161,7 +161,7 @@
[["Shared" "Shared"]]))]
(com/select {:options options
:name name
:value (ffirst options)
:value (or value (ffirst options))
:class "w-full"})))
(defn account-typeahead*
@@ -313,7 +313,9 @@
(filter number?)
(reduce + 0.0))
balance (-
(Math/abs (-> request :multi-form-state :snapshot :transaction/amount))
(Math/abs (or (-> request :entity :transaction/amount)
(-> request :multi-form-state :snapshot :transaction/amount)
0.0))
total)]
[:span {:class (when-not (dollars= 0.0 balance)
"text-red-300")}
@@ -355,7 +357,9 @@
(defn account-grid-body* [request]
(let [snapshot (-> request :multi-form-state :snapshot)
amount-mode (or (:amount-mode snapshot) "$")
total (Math/abs (or (:transaction/amount snapshot) 0.0))]
total (Math/abs (or (-> request :entity :transaction/amount)
(:transaction/amount snapshot)
0.0))]
(com/data-grid {:headers [(com/data-grid-header {} "Account")
(com/data-grid-header {:class "w-32"} "Location")
(com/data-grid-header {:class "w-16"}
@@ -913,7 +917,7 @@
[:div {:x-show "activeForm === 'manual'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
[:div {}
[:div {:hx-trigger "change"
:hx-get (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
:hx-post (bidi/path-for ssr-routes/only-routes ::route/edit-vendor-changed)
:hx-target "#account-grid-body"
:hx-swap "outerHTML"
:hx-include "closest form"}
@@ -1246,14 +1250,16 @@
(when (and (= :transaction-approval-status/approved (keyword (:transaction/approval-status tx-data)))
(not (seq (:transaction/accounts tx-data))))
(throw (ex-info "Approved transactions must have accounts assigned."
{:validation-error "Approved transactions must have accounts assigned."})))
{:type :form-validation
:form-validation-errors ["Approved transactions must have accounts assigned."]})))
(when (seq (:transaction/accounts tx-data))
(let [account-total (reduce + 0 (map :transaction-account/amount (:transaction/accounts tx-data)))]
(when (not (dollars= (Math/abs (:transaction/amount existing-tx)) account-total))
(throw (ex-info (str "Account total (" account-total ") does not equal transaction amount ("
(Math/abs (:transaction/amount existing-tx)) ")")
{:validation-error "Account totals must match transaction amount."})))))
(let [account-total (reduce + 0 (map :transaction-account/amount (:transaction/accounts tx-data)))
tx-amount (Math/abs (:transaction/amount existing-tx))]
(when (not (dollars= tx-amount account-total))
(throw (ex-info (format "The total of your expense accounts ($%,.2f) must equal the transaction amount ($%,.2f)." account-total tx-amount)
{:type :form-validation
:form-validation-errors [(format "The total of your expense accounts ($%,.2f) must equal the transaction amount ($%,.2f)." account-total tx-amount)]})))))
(let [transaction-result (audit-transact [transaction] (:identity request))]
(try
@@ -1348,8 +1354,6 @@
(mm/get-step this current-step)
(mm/get-step this :basic-details)))
(render-wizard [this {:keys [multi-form-state] :as request}]
(println "HERE XYZ" (:form-errors request))
(clojure.pprint/pprint (:snapshot multi-form-state))
(mm/default-render-wizard
this request
:form-params
@@ -1394,34 +1398,40 @@
[]
entity)))
(defn- render-account-grid-body [request]
(fc/start-form (:multi-form-state request) nil
(fc/with-field :step-params
(fc/with-field :transaction/accounts
(account-grid-body* request)))))
(defn edit-vendor-changed-handler [request]
(let [snapshot (:snapshot (:multi-form-state request))
(let [multi-form-state (:multi-form-state request)
snapshot (:snapshot multi-form-state)
step-params (:step-params multi-form-state)
client-id (or (:transaction/client snapshot)
(-> request :entity :transaction/client :db/id))
vendor-id (:transaction/vendor snapshot)
total (Math/abs (or (:transaction/amount snapshot) 0.0))
amount-mode (or (:amount-mode snapshot) "$")]
(if (and (empty? (:transaction/accounts snapshot))
vendor-id
client-id)
(if-let [default-account (vendor-default-account vendor-id client-id)]
(let [new-account {:db/id (str (java.util.UUID/randomUUID))
:transaction-account/account (:db/id default-account)
:transaction-account/location (or (:account/location default-account) "Shared")}
new-account (if (= amount-mode "%")
(assoc new-account :transaction-account/amount 100.0)
(assoc new-account :transaction-account/amount total))
updated-snapshot (assoc snapshot :transaction/accounts [new-account])
updated-request (assoc-in request [:multi-form-state :snapshot] updated-snapshot)]
(html-response
[:div#account-grid-body
(account-grid-body* updated-request)]))
(html-response
[:div#account-grid-body
(account-grid-body* request)]))
(html-response
[:div#account-grid-body
(account-grid-body* request)]))))
vendor-id (or (:transaction/vendor step-params) (:transaction/vendor snapshot))
total (Math/abs (or (-> request :entity :transaction/amount)
(:transaction/amount snapshot)
0.0))
amount-mode (or (:amount-mode snapshot) "$")
existing-accounts (or (seq (:transaction/accounts step-params))
(seq (:transaction/accounts snapshot)))
default-account (when (and (empty? existing-accounts) vendor-id client-id)
(vendor-default-account vendor-id client-id))
render-request
(if (and (empty? existing-accounts) vendor-id client-id)
(let [new-account (cond-> {:db/id (str (java.util.UUID/randomUUID))
:transaction-account/location (or (:account/location default-account) "Shared")
:transaction-account/amount (if (= amount-mode "%") 100.0 total)}
default-account (assoc :transaction-account/account (:db/id default-account)))]
(-> request
(assoc-in [:multi-form-state :snapshot :transaction/accounts] [new-account])
(assoc-in [:multi-form-state :step-params :transaction/accounts] [new-account])))
request)]
(html-response
[:div#account-grid-body
(render-account-grid-body render-request)])))
(def key->handler
(apply-middleware-to-all-handlers
@@ -1443,6 +1453,7 @@
(mm/wrap-decode-multi-form-state))
::route/edit-vendor-changed (-> edit-vendor-changed-handler
(mm/wrap-wizard edit-wizard)
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state))
::route/location-select (-> location-select
(wrap-schema-enforce :query-schema [:map
@@ -1453,12 +1464,15 @@
[:maybe entity-id]]]))
::route/account-total (-> account-total
(mm/wrap-wizard edit-wizard)
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state))
::route/account-balance (-> account-balance
(mm/wrap-wizard edit-wizard)
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state))
::route/toggle-amount-mode (-> toggle-amount-mode
(mm/wrap-wizard edit-wizard)
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
(mm/wrap-decode-multi-form-state))
::route/edit-wizard-new-account (->
(add-new-entity-handler [:step-params :transaction/accounts]

View File

@@ -30,7 +30,7 @@
[:script {:src "https://cdn.jsdelivr.net/npm/@ryangjchandler/alpine-tooltip@1.x.x/dist/cdn.min.js" :defer true}]
[:link {:rel "stylesheet" :href "/css/tippy/tippy.css"}]
[:link {:rel "stylesheet" :href "/css/tippy/light.css"}]
[:script {:src "/js/htmx.min.js"
[:script {:src "/js/htmx.js"
:crossorigin= "anonymous"}]
[:script {:src "https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"}]