(ns auto-ap.ssr.admin.transaction-rules (:require [auto-ap.datomic :refer [add-sorter-fields apply-pagination apply-sort-3 audit-transact conn merge-query pull-attr pull-many query2]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.query-params :as query-params] [auto-ap.routes.utils :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.company :refer [bank-account-typeahead*]] [auto-ap.ssr.components :as com] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.hx :as hx] [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers entity-id field-validation-error form-validation-error html-response many-entity money path->name2 percentage ref->enum-schema ref->radio-options regex temp-id wrap-form-4xx-2 wrap-schema-decode]] [auto-ap.utils :refer [dollars=]] [bidi.bidi :as bidi] [cheshire.core :as cheshire] [clojure.string :as str] [datomic.api :as dc] [hiccup2.core :as hiccup] [iol-ion.query :refer [ident]] [malli.core :as mc] [auto-ap.cursor :as cursor] [auto-ap.ssr.hiccup-helper :as hh])) ;; TODO with dependencies, I really don't like that you have to be ultra specific in what ;; you want to include, and generating the routes and interconnection is weird too. ;; I'm tempted to say to include a full snapshot of the form, and the indicator ;; as to which one to generate. (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" "hx-get" (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-table) "hx-target" "#transaction-rule-table" "hx-indicator" "#transaction-rule-table"} [:fieldset.space-y-6 (com/field {:label "Vendor"} (com/typeahead-2 {:name "vendor" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :vendor-search) :id (str "vendor-search") :value [(:db/id (:vendor (:parsed-query-params request))) (:vendor/name (:vendor (:parsed-query-params request)))]})) (com/field {:label "Note"} (com/text-input {:name "note" :id "note" :class "hot-filter" :value (:note (:parsed-query-params request)) :placeholder "HOME DEPOT lte 250.0" :size :small})) (com/field {:label "Description"} (com/text-input {:name "description" :id "description" :class "hot-filter" :value (:description (:parsed-query-params request)) :placeholder "LOWES" :size :small}))]]) (def default-read '[:db/id :transaction-rule/description :transaction-rule/note :transaction-rule/amount-lte :transaction-rule/amount-gte :transaction-rule/dom-lte :transaction-rule/dom-gte {:transaction-rule/client [:client/name :db/id :client/code :client/locations]} {:transaction-rule/bank-account [:db/id :bank-account/name]} {:transaction-rule/yodlee-merchant [:db/id :yodlee-merchant/name :yodlee-merchant/yodlee-id]} {[:transaction-rule/transaction-approval-status :xform iol-ion.query/ident] [:db/id :db/ident]} {:transaction-rule/vendor [:vendor/name :db/id :vendor/default-account]} {:transaction-rule/accounts [:transaction-rule-account/percentage :transaction-rule-account/location {:transaction-rule-account/account [:account/name :db/id :account/numeric-code :account/location {:account/client-overrides [:db/id :account-client-override/name {:account-client-override/client [:db/id :client/name]}]}]} :db/id]}]) (defn fetch-ids [db request] (let [query-params (:parsed-query-params request) valid-clients (extract-client-ids (:clients request) (:client request) (:client-id query-params) (when (:client-code query-params) [:client/code (:client-code query-params)])) query (cond-> {:query {:find [] :in ['$] :where []} :args [db]} (:sort query-params) (add-sorter-fields {"client" ['[?e :transaction-rule/client ?c] '[?c :client/name ?sort-client]] "yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?ym] '[?ym :yodlee-merchant/name ?sort-yodlee-merchant]] "bank-account" ['[?e :transaction-rule/bank-account ?ba] '[?ba :bank-account/name ?sort-bank-account]] "description" ['[?e :transaction-rule/description ?sort-description]] "note" ['[?e :transaction-rule/note ?sort-note]] "amount-lte" ['[?e :transaction-rule/amount-lte ?sort-amount-lte]] "amount-gte" ['[?e :transaction-rule/amount-gte ?sort-amount-gte]]} query-params) (seq valid-clients) (merge-query {:query {:in ['[?xx ...]] :where ['(or-join [?e] (and [?e :transaction-rule/client ?xx]) (and (not [?e :transaction-rule/client]) [?e :transaction-rule/note]))]} :args [valid-clients]}) (-> query-params :vendor :db/id) (merge-query {:query {:in ['?vendor-id] :where ['[?e :transaction-rule/vendor ?vendor-id]]} :args [(-> query-params :vendor :db/id)]}) (not (str/blank? (:note query-params))) (merge-query {:query {:in ['?note-pattern] :where ['[?e :transaction-rule/note ?n] '[(re-find ?note-pattern ?n)]]} :args [(re-pattern (str "(?i)" (:note query-params)))]}) (not (str/blank? (:description query-params))) (merge-query {:query {:in ['?description] :where ['[?e :transaction-rule/description ?d] '[(clojure.string/lower-case ?d) ?d2] '[(clojure.string/includes? ?d2 ?description)]]} :args [(clojure.string/lower-case (:description query-params))]}) true (merge-query {:query {:find ['?e] :where ['[?e :transaction-rule/transaction-approval-status]]}}))] (cond->> (query2 query) true (apply-sort-3 query-params) true (apply-pagination query-params)))) (defn hydrate-results [ids db _] (let [results (->> (pull-many db default-read ids) (group-by :db/id)) refunds (->> ids (map results) (map first))] refunds)) (defn fetch-page [request] (let [db (dc/db conn) {ids-to-retrieve :ids matching-count :count} (fetch-ids db request)] [(->> (hydrate-results ids-to-retrieve db request)) matching-count])) (def grid-page (helper/build {:id "transaction-rule-table" :nav (com/admin-aside-nav) :page-specific-nav filters :fetch-page fetch-page :parse-query-params (comp (query-params/parse-key :vendor #(dc/pull (dc/db conn) '[:vendor/name :db/id] (Long/parseLong %))) (helper/default-parse-query-params grid-page)) :action-buttons (fn [request] [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-new-dialog)) :hx-target "#modal-content" :hx-swap "outerHTML" :color :primary} "New Transaction Rule")]) :row-buttons (fn [request entity] [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-dialog :db/id (:db/id entity))) :hx-target "#modal-content" :hx-swap "outerHTML"} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} "Admin"] [:a {:href (bidi/path-for ssr-routes/only-routes :admin-transaction-rules)} "Transaction Rules"]] :title "Rules" :entity-name "Rule" :route :admin-transaction-rule-table :headers [{:key "client" :name "Client" :sort-key "client" :render #(-> % :transaction-rule/client :client/name)} {:key "bank-account" :name "Bank account" :sort-key "bank-account" :render #(-> % :transaction-rule/bank-account :bank-account/name) :show-starting "lg"} {:key "description" :name "Description" :sort-key "description" :render :transaction-rule/description} {:key "amount" :name "Amount" :sort-key "amount" :render (fn [{:transaction-rule/keys [amount-gte amount-lte]}] [:div.flex.gap-2 (when amount-gte (com/pill {:color :red} (format "more than $%.2f" amount-gte))) (when amount-lte (com/pill {:color :primary} (format "less than $%.2f" amount-lte)))]) :show-starting "md"} {:key "note" :name "Note" :sort-key "note" :render :transaction-rule/note} ]})) (def row* (partial helper/row* grid-page)) (def table* (partial helper/table* grid-page)) (defn entity->note [{:transaction-rule/keys [amount-lte amount-gte description client dom-lte dom-gte]}] (str/join " - " (filter (complement str/blank?) [(when client (pull-attr (dc/db conn) :client/code client)) description (when (or amount-lte amount-gte) (str (when amount-gte (str amount-gte "<")) "amt" (when amount-lte (str "<" amount-lte)))) (when (or dom-lte dom-gte) (str (when dom-gte (str dom-gte "<")) "dom" (when dom-lte (str "<" dom-lte))))]))) (defn bank-account-belongs-to-client? [bank-account-id client-id] (get (->> (dc/pull (dc/db conn) [{:client/bank-accounts [:db/id]}] client-id) :client/bank-accounts (map :db/id) (set)) bank-account-id)) (defn transaction-rule-save [{:keys [form-params request-method identity] :as request}] (let [entity (cond-> form-params (= :post request-method) (assoc :db/id "new") true (assoc :transaction-rule/note (entity->note form-params))) _ (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts entity) (range)) :let [account-location (pull-attr (dc/db conn) :account/location account)] :when (and account-location (not= account-location location))] (field-validation-error (str "must be " account-location) [:transaction-rule/accounts i :transaction-rule-account/location] :form form-params)) total (reduce + 0.0 (map :transaction-rule-account/percentage (:transaction-rule/accounts entity))) _ (when-not (dollars= 1.0 total) (form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total))) :form form-params)) _ (when (and (:transaction-rule/bank-account entity) (not (bank-account-belongs-to-client? (:transaction-rule/bank-account entity) (:transaction-rule/client entity)))) (field-validation-error "does not belong to client" [:transaction-rule/bank-account] :form form-params)) {:keys [tempids]} (audit-transact [[:upsert-entity entity]] (:identity request)) updated-account (dc/pull (dc/db conn) default-read (or (get tempids (:db/id entity)) (:db/id entity)))] (html-response (row* identity updated-account {:flash? true}) :headers {"hx-trigger" "modalclose" "hx-retarget" (format "#transaction-rule-table tr[data-id=\"%d\"]" (:db/id updated-account))}))) (defn- location-select* [{:keys [ name account-location client-locations value]}] (com/select {:options (into [["" ""]] (cond account-location [[account-location account-location]] (seq client-locations) (into [["Shared" "Shared"]] (for [cl client-locations] [cl cl])) :else [["Shared" "Shared"]])) :name name :value value :class "w-full"})) (defn- account-typeahead* [{:keys [name value client-id x-model]}] [:div.flex.flex-col (com/typeahead-2 {:name name :placeholder "Search..." :url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id) :id name :x-model x-model :value value :value-fn (some-fn :db/id identity) :content-fn (fn [value] (:account/name (d-accounts/clientize (cond->> value (nat-int? value) (dc/pull (dc/db conn) d-accounts/default-read)) client-id)))})]) (defn- transaction-rule-account-row* [transaction-rule account] (com/data-grid-row {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account))) (fc/field-value (:transaction-rule-account/account account)))})} (let [account-name (fc/field-name (:transaction-rule-account/account account))] (list (fc/with-field :db/id (com/hidden {:name (fc/field-name) :value (fc/field-value)})) (fc/with-field :transaction-rule-account/account (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} [:div {:hx-trigger "changed" :hx-target "next div" :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" account-name) :hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead)) :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] (account-typeahead* {:value (fc/field-value) :client-id (:db/id (:transaction-rule/client transaction-rule)) :name (fc/field-name) :x-model "accountId" })))) (fc/with-field :transaction-rule-account/location (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors) :x-data (hx/json {:location (fc/field-value)})} [:div {:hx-trigger "changed" :hx-target "next *" :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || ''}" (fc/field-name) ) :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select) :x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data))"}] (location-select* {:name (fc/field-name) :account-location (:account/location (cond->> (:transaction-rule-account/account @account) (nat-int? (:transaction-rule-account/account @account)) (dc/pull (dc/db conn) '[:account/location]))) :client-locations (:client/locations (:transaction-rule/client transaction-rule)) :hx-model "location" :value (fc/field-value)})))) (fc/with-field :transaction-rule-account/percentage (com/data-grid-cell {} (com/validated-field {:errors (fc/field-errors)} (com/money-input {:name (fc/field-name) :class "w-16" :value (some-> (fc/field-value) (* 100 ) (long ))})))))) (com/data-grid-cell {:class "align-top"} (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest