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]

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

@@ -206,7 +206,7 @@
["approved" "Approved"]
["unapproved" "Unapproved"]
["suppressed" "Suppressed"]
["requires_feedback" "Requires Feedback"]]})))]
["requires-feedback" "Client Review"]]})))]
;; Accounts section
[:div.col-span-2.pt-4

View File

@@ -72,17 +72,11 @@
(or (not= approval-status :transaction-approval-status/approved)
(seq accounts)))]])
(def edit-form-schema
(mc/schema
[:and
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:action [:enum :apply-rule :unlink-payment :link-unpaid-invoices :link-autopay-invoices :link-payment :manual]]
[: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}
(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
@@ -97,7 +91,18 @@
:error/path [:transaction-account/location]}
(fn [iea]
(check-location-belongs (:transaction-account/location iea)
(:transaction-account/account iea)))]]]]]]
(:transaction-account/account iea)))]]]])
(def edit-form-schema
(mc/schema
[:and
[:map
[:db/id {:optional true} [:maybe entity-id]]
[:action [:enum :apply-rule :unlink-payment :link-unpaid-invoices :link-autopay-invoices :link-payment :manual]]
[: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 "$" "%"]]]]
[: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