3 Commits

Author SHA1 Message Date
f9438ba983 fix(ssr): only require account coding for manual transaction edits
Account coding lived in the always-applied base map of edit-form-schema, so
every action (including the link/apply-rule/unlink actions) required a valid
transaction-account/account. The edit modal always submits the Manual tab's
(usually blank) account row, so link submits failed validation before reaching
their save-handler and silently no-op'd. Move account validation into the
:manual branch of the action :multi so link actions validate without it.

Also surface whole-form validation errors in the wizard footer error bar:
default-step-footer only handled top-level/sequential error shapes, so nested
field-error maps (e.g. a hidden tab's account error) produced an empty bar and
a silent failure. Add flatten-form-errors to flatten the humanized error tree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 21:30:29 -07:00
7d34b8a5f6 money 2026-06-18 20:26:07 -07:00
c09d85ede6 fix(ssr): fix Client Review (requires-feedback) status in bulk-code dialog
The bulk-code "Requires Feedback" option submitted "requires_feedback"
(underscore), which decoded to an enum keyword not present in the
schema (idents use a hyphen), so selecting it failed validation. Use
the hyphenated value and relabel the option, the reconciliation report
header to "Client Review" to unify with the sidebar terminology.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 10:38:00 -07:00
5 changed files with 93 additions and 59 deletions

View File

@@ -28,7 +28,7 @@
(com/data-grid-header {} "Synced count")
(com/data-grid-header {} "Approved transactions")
(com/data-grid-header {} "Unapproved transactions")
(com/data-grid-header {} "Requires feedback transactions")
(com/data-grid-header {} "Client Review transactions")
(com/data-grid-header {} "Missing transactions")])
#_#_:thead-params {:class "sticky top-0 z-50"}}
(for [row report]
@@ -84,18 +84,18 @@
(com/validated-field {:label "Start"
:errors (fc/field-errors)}
[:div {:class "w-64"}
(com/date-input {:name (fc/field-name)
(com/date-input {:name (fc/field-name)
:class "w-64"
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))})]))
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))})]))
(fc/with-field :end-date
(com/validated-field {:label "End"
:errors (fc/field-errors)}
[:div {:class "w-64"}
(com/date-input {:name (fc/field-name)
(com/date-input {:name (fc/field-name)
:class "w-64"
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))})]))
:value (some-> (fc/field-value)
(atime/unparse-local atime/normal-date))})]))
(com/button {:color :primary :class "self-center w-24"} "Run")])]
(if report
(report* {:request request :report report})
@@ -104,15 +104,15 @@
(defn page [request]
(base-page
request
(com/page {:nav com/company-aside-nav
(com/page {:nav com/company-aside-nav
:client-selection (:client-selection request)
:client (:client request)
:clients (:clients request)
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company-reconciliation-report)
:identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company-reconciliation-report)
:hx-trigger "clientSelected from:body"
:hx-select "#app-contents"
:hx-swap "outerHTML swap:300ms"}}
:hx-swap "outerHTML swap:300ms"}}
(com/breadcrumbs {}
[:a {:href (bidi/path-for ssr-routes/only-routes :company)}
"My Company"]
@@ -133,7 +133,7 @@
(defn get-report-data [start-date end-date client-ids]
(let [client-codes (map first (dc/q '[:find ?cc :in $ [?c ...] :where [?c :client/code ?cc]] (dc/db conn) client-ids))]
(for [[ib ba c] (seq (apply get-intuit-bank-accounts (dc/db conn) client-codes))
(for [[ib ba c] (seq (apply get-intuit-bank-accounts (dc/db conn) client-codes))
:let [raw-transactions (get-transactions (atime/unparse-local start-date atime/iso-date)
(atime/unparse-local end-date atime/iso-date)
ib)

View File

@@ -138,6 +138,33 @@
[:div.space-y-1 {}
children])
(defn flatten-form-errors
"Walks a malli-humanized error structure and returns a flat sequence of
human-readable strings, prefixing each leaf message with the nearest
field name for context. Lets the footer's error bar surface every
validation error for the whole form, even ones whose field lives on a
hidden step/tab and so would otherwise be invisible."
([errors] (flatten-form-errors nil errors))
([field errors]
(let [label (cond (keyword? field) (name field)
(string? field) field
:else nil)
decorate (fn [msg] (if label (str label ": " msg) msg))]
(cond
(map? errors)
(mapcat (fn [[k v]] (flatten-form-errors k v)) errors)
(and (sequential? errors) (every? string? errors))
(map decorate errors)
(sequential? errors)
(mapcat #(flatten-form-errors field %) errors)
(string? errors)
[(decorate errors)]
:else nil))))
(defn default-step-footer [linear-wizard step & {:keys [validation-route
discard-button
next-button
@@ -146,7 +173,8 @@
[:div.flex.items-baseline.gap-x-4
(let [step-errors (:step-params fc/*form-errors*)]
(com/form-errors {:errors (or (:errors step-errors)
(when (sequential? step-errors) step-errors))}))
(when (sequential? step-errors) step-errors)
(seq (distinct (flatten-form-errors step-errors))))}))
(when (not= (first (steps linear-wizard))
(step-key step))
(when validation-route

View File

@@ -185,28 +185,28 @@
:hx-target "#account-entries"
:hx-swap "innerHTML"
:hx-include "closest form"}
(fc/with-field :vendor
(com/validated-field {:label "Vendor"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:placeholder "Search for vendor..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))]
(fc/with-field :vendor
(com/validated-field {:label "Vendor"
:errors (fc/field-errors)}
(com/typeahead {:name (fc/field-name)
:placeholder "Search for vendor..."
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))})))]
;; Status field
[:div
(fc/with-field :approval-status
(com/validated-field {:label "Status"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:value (some-> (fc/field-value)
name)
:options [["" "No Change"]
["approved" "Approved"]
["unapproved" "Unapproved"]
["suppressed" "Suppressed"]
["requires_feedback" "Requires Feedback"]]})))]
[:div
(fc/with-field :approval-status
(com/validated-field {:label "Status"
:errors (fc/field-errors)}
(com/select {:name (fc/field-name)
:value (some-> (fc/field-value)
name)
:options [["" "No Change"]
["approved" "Approved"]
["unapproved" "Unapproved"]
["suppressed" "Suppressed"]
["requires-feedback" "Client Review"]]})))]
;; Accounts section
[:div.col-span-2.pt-4
@@ -219,10 +219,10 @@
(com/data-grid {:headers [(com/data-grid-header {} "Account")
(com/data-grid-header {:class "w-32"} "Location")
(com/data-grid-header {:class "w-16"} "%")
(com/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-account-row* {:value %}))
(com/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-account-row* {:value %}))
(com/data-grid-new-row {:colspan 4
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
::route/bulk-code-new-account)
:row-offset 0
@@ -357,10 +357,10 @@
{:errors (fc/field-errors)}
(com/data-grid {:headers [(com/data-grid-header {} "Account")
(com/data-grid-header {:class "w-32"} "Location")
(com/data-grid-header {:class "w-16"} "%")
(com/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-account-row* {:value %}))
(com/data-grid-new-row {:colspan 4
(com/data-grid-header {:class "w-16"} "%")
(com/data-grid-header {:class "w-16"})]}
(fc/cursor-map #(transaction-account-row* {:value %}))
(com/data-grid-new-row {:colspan 4
:hx-get (bidi/path-for ssr-routes/only-routes
::route/bulk-code-new-account)
:row-offset 0

View File

@@ -72,6 +72,27 @@
(or (not= approval-status :transaction-approval-status/approved)
(seq accounts)))]])
(def account-coding-schema
"Validation for manually-coded transaction account rows. Applied only for
the :manual action: link / apply-rule actions build their own accounts
server-side, so the (often blank) account row carried along by the manual
tab must not be required when one of those actions is submitted."
[:maybe
[:vector {:coerce? true}
[:and
[:map
[:db/id {:optional true} [:maybe [:or temp-id entity-id]]]
[:transaction-account/account [:and entity-id
[:fn {:error/message "Not an allowed account."}
#(check-allowance % :account/default-allowance)]]]
[:transaction-account/location :string]
[:transaction-account/amount :double]]
[:fn {:error/fn (fn [r x] (:type r))
:error/path [:transaction-account/location]}
(fn [iea]
(check-location-belongs (:transaction-account/location iea)
(:transaction-account/account iea)))]]]])
(def edit-form-schema
(mc/schema
[:and
@@ -81,23 +102,7 @@
[:transaction/memo {:optional true} [:maybe [:string {:decode/string strip}]]]
[:transaction/vendor {:optional true} [:maybe entity-id]]
[:transaction/approval-status {:optional true} [:maybe (ref->enum-schema "transaction-approval-status")]]
[:amount-mode {:optional true} [:maybe [:enum "$" "%"]]]
[:transaction/accounts {:optional true}
[:maybe
[:vector {:coerce? true}
[:and
[:map
[:db/id {:optional true} [:maybe [:or temp-id entity-id]]]
[:transaction-account/account [:and entity-id
[:fn {:error/message "Not an allowed account."}
#(check-allowance % :account/default-allowance)]]]
[:transaction-account/location :string]
[:transaction-account/amount :double]]
[:fn {:error/fn (fn [r x] (:type r))
:error/path [:transaction-account/location]}
(fn [iea]
(check-location-belongs (:transaction-account/location iea)
(:transaction-account/account iea)))]]]]]]
[:amount-mode {:optional true} [:maybe [:enum "$" "%"]]]]
[:multi {:dispatch :action}
[:apply-rule [:map
[:rule-id {:optional true} [:maybe entity-id]]]]
@@ -110,7 +115,8 @@
[:autopay-invoice-ids {:decode/string (fn [x] (edn/read-string x))} [:vector {:coerce? true} entity-id]]]]
[:link-payment [:map
[:payment-id entity-id]]]
[:manual (require-approval [:map])]]]))
[:manual (require-approval [:map
[:transaction/accounts {:optional true} account-coding-schema]])]]]))
(defn clientize-vendor [{:vendor/keys [terms-overrides automatically-paid-when-due default-account account-overrides] :as vendor} client-id]
(if (nil? vendor)

View File

@@ -211,7 +211,7 @@
(com/data-grid-cell {} (fc/with-field :description-original
(com/text-input {:value (fc/field-value) :name (fc/field-name)})))
(com/data-grid-cell {} (fc/with-field :amount
(com/money-input {:value (fc/field-value) :name (fc/field-name) :class "w-28"})))
(com/text-input {:value (fc/field-value) :name (fc/field-name) :class "w-28 text-right" :inputmode "decimal"})))
(com/data-grid-cell {} (fc/with-field :bank-account-code
(com/text-input {:value (fc/field-value) :name (fc/field-name) :class "w-28"})))
(com/data-grid-cell {} (fc/with-field :client-code