diff --git a/resources/templates/transaction-bulk-code/account-entries.html b/resources/templates/transaction-bulk-code/account-entries.html new file mode 100644 index 00000000..b0bd047e --- /dev/null +++ b/resources/templates/transaction-bulk-code/account-entries.html @@ -0,0 +1,2 @@ +{# Wrapper around the expense-account grid (the body of the accounts validated-field). #} +
{{ grid|safe }}
diff --git a/resources/templates/transaction-bulk-code/account-grid.html b/resources/templates/transaction-bulk-code/account-grid.html new file mode 100644 index 00000000..16f1a2c7 --- /dev/null +++ b/resources/templates/transaction-bulk-code/account-grid.html @@ -0,0 +1,22 @@ +{# Expense-account grid -- fully template-driven. A single for-loop over the per-row + view-models (bulk-code/account-row-vm), each delegating to account-row.html. Replaces + the Clojure data-grid / data-grid-row / data-grid-cell composition. The trailing + "New account" button posts the whole #bulk-code-form (op=new-account). #} +
+ + + + + + + + + + + {% for row in rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %} + + + + +
AccountLocation%
{{ new_account_button|safe }}
+
diff --git a/resources/templates/transaction-bulk-code/account-row.html b/resources/templates/transaction-bulk-code/account-row.html new file mode 100644 index 00000000..fc220848 --- /dev/null +++ b/resources/templates/transaction-bulk-code/account-row.html @@ -0,0 +1,35 @@ +{# One expense-account row, read entirely from a loop-bound `row` view-model + (bulk-code/account-row-vm). The account typeahead reuses the shared + components/typeahead.html partial (its context mapped in from row.account via a with + block); the location select, percentage input, and remove button are inlined plain + HTML. The location cell (#account-location-N) swaps just itself on account change; the + remove button swaps the whole #bulk-code-form. Every dynamic attribute arrives + pre-serialized as a string. #} + + + +
+
{% with x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_class=row.account.a_class a_xinit=row.account.a_xinit search_class=row.account.search_class placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}
+

{{ row.account_error }}

+
+ + +
+ +

{{ row.location_error }}

+
+ + +
+ +

{{ row.pct_error }}

+
+ + + +
{% include "templates/components/svg-x.html" %}
+
+ + diff --git a/resources/templates/transaction-bulk-code/footer.html b/resources/templates/transaction-bulk-code/footer.html new file mode 100644 index 00000000..8f2c5bab --- /dev/null +++ b/resources/templates/transaction-bulk-code/footer.html @@ -0,0 +1,5 @@ +{# Modal footer: the form-errors sink on the left, the Save button on the right. + Both are pre-rendered fragments (errors sink = form-errors.html, save = sc/button). #} +
+
{{ form_errors|safe }}{{ save_button|safe }}
+
diff --git a/resources/templates/transaction-bulk-code/form-errors.html b/resources/templates/transaction-bulk-code/form-errors.html new file mode 100644 index 00000000..5f6e29f8 --- /dev/null +++ b/resources/templates/transaction-bulk-code/form-errors.html @@ -0,0 +1,4 @@ +{# Submit-error sink. A 4xx submit swaps the inner `.error-content` (hx-target-400); + the span is present only when there are form-level errors, matching the prior + hand-rolled markup byte-for-byte. #} +
{% if errors_str %}

{{ errors_str }}

{% endif %}
diff --git a/resources/templates/transaction-bulk-code/head.html b/resources/templates/transaction-bulk-code/head.html new file mode 100644 index 00000000..8c86db0f --- /dev/null +++ b/resources/templates/transaction-bulk-code/head.html @@ -0,0 +1,2 @@ +{# Modal header label: how many transactions this bulk-code operation will touch. #} +
Bulk editing {{ count }} transactions
diff --git a/resources/templates/transaction-bulk-code/success-body.html b/resources/templates/transaction-bulk-code/success-body.html new file mode 100644 index 00000000..6cbc6b4c --- /dev/null +++ b/resources/templates/transaction-bulk-code/success-body.html @@ -0,0 +1,2 @@ +{# Post-submit confirmation message embedded in the shared success modal. #} +

Successfully coded {{ count }} transactions.

diff --git a/src/clj/auto_ap/ssr/components/selmer.clj b/src/clj/auto_ap/ssr/components/selmer.clj index e1330219..3b441bc8 100644 --- a/src/clj/auto_ap/ssr/components/selmer.clj +++ b/src/clj/auto_ap/ssr/components/selmer.clj @@ -72,14 +72,19 @@ (update :class #(str % (inputs/use-size size))))] (render "templates/components/text-input.html" {:attrs (attrs->str attrs)}))) -(defn money-input [{:keys [size] :as params}] - (let [attrs (-> params +(defn money-input-attrs + "The serialized attribute string for a money input. Split out so a template-driven + grid can inline `` without re-deriving the class logic." + [{:keys [size] :as params}] + (attrs->str (-> params (dissoc :size) (update :class (fnil hh/add-class "") inputs/default-input-classes) (update :class hh/add-class "appearance-none text-right") (update :class #(str % (inputs/use-size size))) - (assoc :type "number" :step "0.01"))] - (render "templates/components/money-input.html" {:attrs (attrs->str attrs)}))) + (assoc :type "number" :step "0.01")))) + +(defn money-input [params] + (render "templates/components/money-input.html" {:attrs (money-input-attrs params)})) (defn select "Generic contexts come from the shared edit/* ctx + builders so the URL / content-fn / options logic is not duplicated." [{:keys [value client-id index]}] (let [account-val (let [av (:account value)] (if (map? av) (:db/id av) av)) + tr-map (hx/alpine-mount-then-appear + {:class "account-row" + :id (str "account-row-" index) + :x-data (hx/json {:show (boolean (not (:new? value))) + :accountId account-val}) + :data-key "show" + :x-ref "p"}) location-attrs {:x-hx-val:account-id "accountId" :hx-vals (hx/json (cond-> {:name (account-field-name index :location)} client-id (assoc :client-id client-id))) @@ -140,79 +149,61 @@ :hx-select (str "#account-location-" index) :hx-swap "outerHTML" :hx-include "closest form"}] - (sc/data-grid-row - (-> {:class "account-row" - :id (str "account-row-" index) - :x-data (hx/json {:show (boolean (not (:new? value))) - :accountId account-val}) - :data-key "show" - :x-ref "p"} - hx/alpine-mount-then-appear) - (sc/hidden {:name (account-field-name index :db/id) - :value (:db/id value)}) - (sc/data-grid-cell - {} - (sc/validated-field - {:errors (account-field-errors index :account)} - (account-typeahead* {:value account-val - :client-id client-id - :name (account-field-name index :account) - :x-model "accountId"}))) - (sc/data-grid-cell - {:id (str "account-location-" index)} - (sc/validated-field - (merge {:errors (account-field-errors index :location)} - location-attrs) - (location-select* {:name (account-field-name index :location) - :account-location (:account/location (when (nat-int? account-val) - (dc/pull (dc/db conn) '[:account/location] account-val))) - :client-locations (pull-attr (dc/db conn) :client/locations client-id) - :value (:location value)}))) - (sc/data-grid-cell - {} - (sc/validated-field - {:errors (account-field-errors index :percentage)} - (sc/money-input {:name (account-field-name index :percentage) - :class "w-16" - :value (some-> (:percentage value) (* 100) long)}))) - (sc/data-grid-cell - {:class "align-top"} - (sc/a-icon-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) - :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) - :hx-target "#bulk-code-form" - :hx-select "#bulk-code-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :class "account-remove-action"} - (sc/render "templates/components/svg-x.html" {})))))) + {:tr_classes (str (:class tr-map) " border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700") + :tr_attrs (sc/attrs->str (dissoc tr-map :class)) + :db_id_name (account-field-name index :db/id) + :db_id_value (:db/id value) + :account_field_classes (sc/validated-field-classes {:errors (account-field-errors index :account)}) + :account_error (sc/errors-str (account-field-errors index :account)) + :account (account-typeahead-ctx {:value account-val + :client-id client-id + :name (account-field-name index :account) + :x-model "accountId"}) + :location_cell_id (str "account-location-" index) + :location_field_classes (sc/validated-field-classes {:errors (account-field-errors index :location)}) + :location_field_attrs (sc/attrs->str location-attrs) + :location_error (sc/errors-str (account-field-errors index :location)) + :location (location-select-ctx {:name (account-field-name index :location) + :account-location (:account/location (when (nat-int? account-val) + (dc/pull (dc/db conn) '[:account/location] account-val))) + :client-locations (pull-attr (dc/db conn) :client/locations client-id) + :value (:location value)}) + :pct_attrs (sc/money-input-attrs {:name (account-field-name index :percentage) + :class "w-16" + :value (some-> (:percentage value) (* 100) long)}) + :pct_field_classes (sc/validated-field-classes {:errors (account-field-errors index :percentage)}) + :pct_error (sc/errors-str (account-field-errors index :percentage)) + :remove_attrs (sc/attrs->str {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) + :hx-vals (hx/json {:op "remove-account" :row-index (or index 0)}) + :hx-target "#bulk-code-form" + :hx-select "#bulk-code-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :href ""})})) -(defn- account-grid* [request] +(defn- account-grid* + "Render the whole expense-account grid from account-grid.html: a {% for %} over the + per-row view-models, plus the pre-rendered New-account button. All grid structure is + in the template -- the Clojure here only assembles data." + [request] (let [client-id (single-client-id request) accounts (vec (:accounts (:bulk-state request)))] - (apply - sc/data-grid - {:headers [(sc/data-grid-header {} "Account") - (sc/data-grid-header {:class "w-32"} "Location") - (sc/data-grid-header {:class "w-16"} "%") - (sc/data-grid-header {:class "w-16"})]} - (concat - (map-indexed - (fn [index account] - (transaction-account-row* {:value account - :client-id client-id - :index index})) - accounts) - [(sc/data-grid-row - {:class "new-row"} - (sc/data-grid-cell {:colspan 4} - (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) - :hx-vals (hx/json {:op "new-account"}) - :hx-target "#bulk-code-form" - :hx-select "#bulk-code-form" - :hx-swap "outerHTML" - :hx-include "closest form" - :color :secondary} - "New account")))])))) + (sel/render->hiccup + "templates/transaction-bulk-code/account-grid.html" + {:rows (map-indexed + (fn [index account] + (account-row-vm {:value account + :client-id client-id + :index index})) + accounts) + :new_account_button (sc/a-button {:hx-post (bidi/path-for ssr-routes/only-routes ::route/bulk-code-form-changed) + :hx-vals (hx/json {:op "new-account"}) + :hx-target "#bulk-code-form" + :hx-select "#bulk-code-form" + :hx-swap "outerHTML" + :hx-include "closest form" + :color :secondary} + "New account")}))) (defn- bulk-code-body* [request] (let [bulk-state (:bulk-state request) @@ -249,24 +240,18 @@ ["requires-feedback" "Client Review"]]}))) :accounts_field (str (sc/validated-field {:errors (ferr :accounts)} - (sel/raw (str "
" - (str (account-grid* request)) - "
"))))}))) + (sc/render "templates/transaction-bulk-code/account-entries.html" + {:grid (str (account-grid* request))})))}))) (defn- form-errors-html [errors] - (str "
" - (when (seq errors) - (str "

" - (str/join ", " (filter string? errors)) - "

")) - "
")) + (sc/render "templates/transaction-bulk-code/form-errors.html" + {:errors_str (when (seq errors) + (str/join ", " (filter string? errors)))})) (defn- footer* [request] - (sel/raw - (str "
" - (form-errors-html (:errors (:form-errors request))) - (str (sc/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Save")) - "
"))) + (sc/render "templates/transaction-bulk-code/footer.html" + {:form_errors (form-errors-html (:errors (:form-errors request))) + :save_button (sc/button {:color :primary :x-ref "next" :class "w-32 wizard-save-action"} "Save")})) (defn render-form "Renders the whole plain bulk-code form (no wizard). Binds *errors* from the request's @@ -280,7 +265,7 @@ (str (sc/hidden {:name (path->name2 :ids i) :value id}))) ids)) modal-card (sel/render "templates/transaction-edit/edit-modal.html" - {:head (str "
Bulk editing " (count ids) " transactions
") + {:head (sc/render "templates/transaction-bulk-code/head.html" {:count (count ids)}) :side_panel nil :body (str (bulk-code-body* request)) :footer (str (footer* request))})] @@ -463,7 +448,8 @@ ;; Return success modal (html-response (com/success-modal {:title "Transactions Coded"} - [:p (str "Successfully coded " (count ids) " transactions.")]) + (sel/render->hiccup "templates/transaction-bulk-code/success-body.html" + {:count (count ids)})) :headers {"hx-trigger" "refreshTable, reset-selection"})))) ;; --------------------------------------------------------------------------- diff --git a/src/clj/auto_ap/ssr/transaction/edit.clj b/src/clj/auto_ap/ssr/transaction/edit.clj index 6b3c7569..daebe7fd 100644 --- a/src/clj/auto_ap/ssr/transaction/edit.clj +++ b/src/clj/auto_ap/ssr/transaction/edit.clj @@ -159,11 +159,10 @@ clientized (clientize-vendor vendor client-id)] (:vendor/default-account clientized)))) -(defn location-select* - "The location from its own row loop." [{:keys [name account-location client-locations value]}] (let [options (cond account-location [[account-location account-location]] @@ -177,28 +176,48 @@ [["Shared" "Shared"]]) selected (or value (ffirst options)) classes (str/join " " (conj (vec inputs/default-input-classes) "w-full"))] - (sel/render->hiccup - "templates/components/location-select.html" - {:name name - :classes classes - :options (for [[v label] options] - {:value v :label label :selected (= v selected)})}))) + {:name name + :classes classes + :options (for [[v label] options] + {:value v :label label :selected (= v selected)})})) + +(defn location-select* + "The location