diff --git a/config/prod-background-worker.edn b/config/prod-background-worker.edn index 3a94ea1c..7aa8c468 100644 --- a/config/prod-background-worker.edn +++ b/config/prod-background-worker.edn @@ -1,6 +1,6 @@ {:db {:server "database"} :datomic-url "datomic:ddb://us-east-1/integreat/integreat-prod" - :solr-uri "http://solr-prod.local:8983" + :solr-uri "http://solr-ec2-prod.local:8983" :solr-impl :solr :scheme "https" :dd-env "prod" diff --git a/resources/public/js/alpine-vals.js b/resources/public/js/alpine-vals.js index b2836748..31ad0fca 100644 --- a/resources/public/js/alpine-vals.js +++ b/resources/public/js/alpine-vals.js @@ -42,27 +42,26 @@ Alpine.directive('hx-header', (el, { value, expression }, { evaluateLater, effec cleanup(onDestroy); } ); -Alpine.directive('popper', (el, { value, expression }, { evaluateLater, effect, cleanup, evaluate, Alpine}) => { - let dependent_properties = evaluate(expression) - let tooltip = evaluate(dependent_properties['tooltip']) - let source = evaluate(dependent_properties['source']) + Alpine.data('popper', () => ({ + show: false, + popper: null, + init() { + this.$nextTick(() => this.popper = Popper.createPopper(this.$refs.source, this.$refs.tooltip, + { placement: 'bottom', strategy: 'fixed', modifiers: [{ name: 'preventOverflow' }, { name: 'offset', options: { offset: [0, 10] } }] })) + let reveal = () => { + if (!this.show) { + this.show = true; + } + }; + let hide = () => this.show = false; + this.$refs.source.addEventListener('mouseover', reveal) + this.$refs.source.addEventListener('mouseout', hide) + }, + tooltip: { - let popper = Popper.createPopper(source, tooltip, {placement: 'bottom', strategy: 'fixed', modifiers: [{name: 'preventOverflow'}, {name: 'offset', options: {offset: [0, 10]}}]});; - let d=Alpine.reactive({show: false}); - tooltip.classList.add('opacity-0', 'transition-opacity') - - let show = () => d.show = true; - let hide = () => d.show = false; - source.addEventListener('mouseover', show) - source.addEventListener('mouseout', hide) - effect(() => { - if (d.show) { - tooltip.classList.remove('opacity-0') - } else { - tooltip.classList.add('opacity-0') + [':class']() { return { 'opacity-0': !this.show, 'opacity-100': this.show, 'pointer-events-none': !this.show, 'transition': true } }, + ["x-ref"]: 'tooltip', + ['x-transition']: true } - - }) - cleanup(() => {popper.destroy(); source.removeEventListener('mouseover', show); source.removeEventListener('mouseout', hide) }) - }) -}) \ No newline at end of file + })) +}) diff --git a/src/clj/auto_ap/ssr/company/plaid.clj b/src/clj/auto_ap/ssr/company/plaid.clj index e168ec4f..811a93a0 100644 --- a/src/clj/auto_ap/ssr/company/plaid.clj +++ b/src/clj/auto_ap/ssr/company/plaid.clj @@ -186,13 +186,11 @@ :bank-account/integration-status)] [:div (when bad-integration - {:x-popper (hx/json {:source "$refs.button" - :tooltip "$refs.tooltip"}) - :x-data (hx/json {}) - }) + { :x-data "popper()" }) [:div.cursor-pointer (com/pill {:color (if bad-integration :red - :primary) :x-ref "button"} + :primary) + :x-ref "source"} [:div.inline-flex.gap-2 (or @@ -206,7 +204,8 @@ (when bad-integration - (com/tooltip {:x-ref "tooltip"} + (com/tooltip {:x-bind "tooltip" + :x-ref "tooltip"} [:div.text-red-700 (:integration-status/message bad-integration)]))])] [:div.grid.grid-cols-2.gap-1.auto-cols-min.grid-flow-row.shrink diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index 9dc04345..be2c1b77 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -42,8 +42,10 @@ (def hidden inputs/hidden-) (def select inputs/select-) (def typeahead inputs/typeahead-) +(def multi-typeahead inputs/multi-typeahead-) (def field-errors inputs/field-errors-) (def field inputs/field-) +(def inline-field inputs/inline-field-) (def validated-field inputs/validated-field-) (def errors inputs/errors-) (def form-errors inputs/form-errors-) diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index b6fb5a38..7efaa576 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -235,5 +235,5 @@ (defn tooltip- [{:as params} & children] [:div (assoc params - :class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4 opacity-0") + :class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4 absolute") [:span children]]) diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index cbf8a8a8..0b5a701b 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -129,6 +129,99 @@ [:li {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800 dark:text-gray-100 text-xs "} "No results found"]]]]]) +(defn multi-typeahead- [params] + [:div.relative {:x-data (hx/json {:open false + :baseUrl (if (str/includes? (:url params) "?") + (str (:url params) "&q=") + (str (:url params) "?q=")) + :value (map (fn [v] {:value ((:value-fn params identity) v) + :label ((:content-fn params identity) v)}) + (:value params)) + :x-init (str "$watch('value', v => $dispatch('change')); ") + :search "" + :active -1 + :elements (if ((:value-fn params identity) (:value params)) + [{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}] + []) + :popper nil + :warning_badge nil}) + ;; :x-modelable "value.value" TODO + ;; :x-model (:x-model params) TODO + :x-init "popper = Popper.createPopper($refs.input, $refs.dropdown, {placement: 'bottom-start', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [0, 10]}}}) + warning_badge = Popper.createPopper($refs.warning_badge, $refs.warning_pop, {placement: 'top', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [10,0 ]}}})"} + (if (:disabled params) + [:span {:x-text "value.label"}] + [:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes) + (hh/add-class "cursor-pointer")) + "@click.prevent" "open = !open; popper.update()" + "@keydown.down.prevent.stop" "open = true; popper.update()" + :tabindex 0 + :x-init (:x-init params) + :x-ref "input"} + [:template {:x-for "v in value"} + [:input (-> params + (dissoc :class :value-fn :content-fn :placeholder :x-model) + (assoc + :type "hidden" + "x-bind:value" "v" + ))]] + [:div.flex.w-full.justify-items-stretch + [:span.flex-grow.text-left [:span {"x-text" "value.length"} ] + " selected"] + + [:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"} + svg/drop-down] + [:div {:x-show "value.warning" + :x-ref "warning_badge" + :x-effect "if (value.warning) { $nextTick(()=> warning_badge.update()) }"} + (tags/badge- {:class "peer"} "!") + + + [:div {:x-show "value.warning" + :x-ref "warning_pop" + :class "hidden peer-hover:block bg-red-50 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 p-4" + :x-text "value.warning"}]]]]) + + [:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1" + "x-ref" "dropdown" + "@keydown.escape" "open = false;" + "x-transition:enter" "ease-[cubic-bezier(.3,2.3,.6,1)] duration-200" + "x-transition:enter-start" "!opacity-0" + "x-transition:enter-end" "!opacity-1" + "x-transition:leave" "ease-out duration-200" + "x-transition:leave-start" "!opacity-1" + "x-transition:leave-end" "!opacity-0" + "x-show " "open" + "x-trap" "open" + "@click.outside" "open=false;"} + + [:input {:type "text" + :class (-> (:class params) + (or "") + (hh/add-class default-input-classes) + (hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full")) + "x-model" "search" + "placeholder" (:placeholder params) + "@keydown.down.prevent" "active ++; active = active >= elements.length - 1 ? elements.length - 1 : active" + "@keydown.up.prevent" "active --; active = active < 0 ? 0 : active" + "@keydown.enter.prevent.stop" "if ($data.elements[active]) { value.push($data.elements[active].value) }" + "x-init" "$watch('search', s => { if($el.value.length > 2) {fetch(baseUrl + s).then(data=>data.json()).then(data => {elements = data; active=-1}) }})"}] + [:div.dropdown-options {:class "rounded-b-lg overflow-hidden"} + [:template {:x-for "(element, index) in elements"} + [:li + [:label {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 text-gray-800 dark:text-gray-100 cursor-pointer" + :href "#" + ":class" "active == index ? 'active' : ''" + "@mouseover" "active = index" + "@mouseout" "active = -1" + "@click.prevent" "value.push(element.value);" + } + [ :input {:type "checkbox" ":checked" "value.includes(element.value)"} ] + [:span {"x-html" "element.label"}]]]] + [:template {:x-if "elements.length == 0"} + [:li {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800 dark:text-gray-100 text-xs "} + "No results found"]]]]]) + (defn use-size [size] (if (= :small size) @@ -209,6 +302,16 @@ (field-errors- {:source (:error-source params) :key (:error-key params)}))]) +(defn inline-field- [params & rest] + [:div (-> params + (update :class #(hh/add-class (or % "") "group flex items-baseline gap-2" ))) + (when (:label params) + [:label {:class "mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)]) + rest + (when (:error-source params) + (field-errors- {:source (:error-source params) + :key (:error-key params)}))]) + (defn errors- [{:keys [errors]}] [:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 (when (sequential? errors) diff --git a/src/clj/auto_ap/ssr/invoices.clj b/src/clj/auto_ap/ssr/invoices.clj index 18722c57..8871fe17 100644 --- a/src/clj/auto_ap/ssr/invoices.clj +++ b/src/clj/auto_ap/ssr/invoices.clj @@ -382,7 +382,8 @@ :hx-get (bidi/path-for ssr-routes/only-routes ::route/pay-wizard) :hx-trigger "click from:#pay-button" - :x-popper (hx/json {:source "$refs.button", :tooltip "$refs.tooltip"}) } + :x-data "popper()" + } (com/button {:color :primary :id "pay-button" :disabled (or (= (count (:ids params)) 0) @@ -393,7 +394,7 @@ :hx-get (bidi/path-for ssr-routes/only-routes ::route/pay-button) :hx-swap "outerHTML" :hx-trigger "selectedChanged from:body, htmx:afterSwap from:#entity-table" - :x-ref "button" + :x-ref "source" :minimal-loading? true :class "relative"} (if (> (count ids) 0) @@ -405,7 +406,7 @@ (when (or (= 0 (count ids)) (> selected-client-count 1)) (com/badge {} "!"))) - (com/tooltip {:x-ref "tooltip" } + (com/tooltip {:x-bind "tooltip" } (cond (not all-credits-or-debits) [:div "All vendor totals must be either positive or negative."] diff --git a/src/clj/auto_ap/ssr/ledger.clj b/src/clj/auto_ap/ssr/ledger.clj index e2ceb7d0..2fa93b04 100644 --- a/src/clj/auto_ap/ssr/ledger.clj +++ b/src/clj/auto_ap/ssr/ledger.clj @@ -6,7 +6,6 @@ observable-query pull-many remove-nils]] [auto-ap.datomic.accounts :as d-accounts] [auto-ap.datomic.accounts :as a] - [auto-ap.graphql.checks :as gq-checks] [auto-ap.graphql.utils :refer [assert-admin assert-can-see-client exception->notification extract-client-ids notify-if-locked]] @@ -24,17 +23,17 @@ [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper :refer [wrap-apply-sort]] [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.ledger.balance-sheet :as balance-sheet] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.pos.common :refer [date-range-field*]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers clj-date-schema - dissoc-nil-transformer entity-id html-response - main-transformer modal-response money ref->enum-schema - strip wrap-form-4xx-2 wrap-implied-route-param - wrap-merge-prior-hx wrap-schema-decode - wrap-schema-enforce]] + entity-id html-response main-transformer money + ref->enum-schema strip wrap-form-4xx-2 + wrap-implied-route-param wrap-merge-prior-hx + wrap-schema-decode wrap-schema-enforce]] [auto-ap.time :as atime] [auto-ap.utils :refer [dollars-0? dollars=]] [bidi.bidi :as bidi] @@ -49,7 +48,6 @@ [hiccup2.core :as hiccup] [iol-ion.utils :refer [by random-tempid]] [malli.core :as mc] - [malli.transform :as mt] [slingshot.slingshot :refer [throw+]])) @@ -306,6 +304,7 @@ :journal-entry/client [:client/name :client/code :db/id] :journal-entry/line-items [:journal-entry-line/debit :journal-entry-line/location + :journal-entry-line/running-balance :journal-entry-line/credit {:journal-entry-line/account [:account/name :db/id :account/numeric-code :bank-account/name :bank-account/numeric-code @@ -417,13 +416,17 @@ (:bank-account/name (:journal-entry-line/account jel)))]] (list - (if account-name - [:div.text-left - (:journal-entry-line/location jel) ": " - (or (:account/numeric-code account) (:bank-account/numeric-code account)) - " - " account-name] - [:div.text-left (com/pill {:color :yellow} "Unassigned")]) - [:div.text-right (format "$%,.2f" (key jel))])) + (if account-name + [:div {:x-data "popper()" } + [:div.text-left.underline.cursor-pointer {:x-ref "source"} + (:journal-entry-line/location jel) ": " + (or (:account/numeric-code account) (:bank-account/numeric-code account)) + " - " account-name] + (com/tooltip {:x-bind "tooltip" :class "absolute"} + "Running Balance: " (some->> (:journal-entry-line/running-balance jel) + (format "$%,.2f")))] + [:div.text-left (com/pill {:color :yellow} "Unassigned")]) + [:div.text-right.text-underline (format "$%,.2f" (key jel))])) (when-not (= 1 (count lines)) [:div.col-span-2 (com/pill {:color :primary} "Total: " (->> lines @@ -703,12 +706,11 @@ [:div.p-2 (cond (seq (fc/field-errors)) [:div - { :x-popper (hx/json {:source "$refs.button" - :tooltip "$refs.tooltip"})} + { :x-data "popper()"} [:div.w-8.h-8.bg-red-50.rounded-full.p-2.text-red-300.flex.items-start - { :x-ref "button"} + { :x-ref "source"} svg/alert] - (com/tooltip {:x-ref "tooltip"} + (com/tooltip {:x-bind "tooltip"} [:span (pr-str (fc/field-errors))])] :else nil) @@ -1197,34 +1199,38 @@ :headers {"hx-trigger" (hx/json { "notification" (pr-str (import-ledger request))})})) + + (def key->handler - (apply-middleware-to-all-handlers - (-> - {::route/all-page (-> (helper/page-route grid-page :parse-query-params? false) - (wrap-implied-route-param :external? false)) - ::route/external-page (-> (helper/page-route grid-page :parse-query-params? false) - (wrap-implied-route-param :external? true)) - - ::route/table (helper/table-route grid-page :parse-query-params? false) - ::route/external-import-page external-import-page - ::route/bank-account-filter bank-account-filter - ::route/external-import-parse (-> external-import-parse - (wrap-schema-enforce :form-schema parse-form-schema) - (wrap-form-4xx-2 external-import-parse) - (wrap-schema-decode :form-schema parse-form-schema)) - ::route/external-import-import (-> external-import-import + (merge + (apply-middleware-to-all-handlers + (-> + {::route/all-page (-> (helper/page-route grid-page :parse-query-params? false) + (wrap-implied-route-param :external? false)) + ::route/external-page (-> (helper/page-route grid-page :parse-query-params? false) + (wrap-implied-route-param :external? true)) + + ::route/table (helper/table-route grid-page :parse-query-params? false) + ::route/external-import-page external-import-page + ::route/bank-account-filter bank-account-filter + ::route/external-import-parse (-> external-import-parse (wrap-schema-enforce :form-schema parse-form-schema) (wrap-form-4xx-2 external-import-parse) - #_(wrap-schema-decode :form-schema parse-form-schema) - (wrap-nested-form-params))}) - (fn [h] - (-> h - (wrap-copy-qp-pqp) - (wrap-apply-sort grid-page) - (wrap-ensure-bank-account-belongs) - (wrap-merge-prior-hx) - (wrap-external-from-route) - (wrap-schema-enforce :query-schema query-schema) - (wrap-schema-enforce :hx-schema query-schema) - (wrap-must {:activity :import :subject :ledger}) - (wrap-client-redirect-unauthenticated))))) \ No newline at end of file + (wrap-schema-decode :form-schema parse-form-schema)) + ::route/external-import-import (-> external-import-import + (wrap-schema-enforce :form-schema parse-form-schema) + (wrap-form-4xx-2 external-import-parse) + #_(wrap-schema-decode :form-schema parse-form-schema) + (wrap-nested-form-params))}) + (fn [h] + (-> h + (wrap-copy-qp-pqp) + (wrap-apply-sort grid-page) + (wrap-ensure-bank-account-belongs) + (wrap-merge-prior-hx) + (wrap-external-from-route) + (wrap-schema-enforce :query-schema query-schema) + (wrap-schema-enforce :hx-schema query-schema) + (wrap-must {:activity :import :subject :ledger}) + (wrap-client-redirect-unauthenticated)))) + balance-sheet/key->handler)) \ No newline at end of file diff --git a/src/cljc/auto_ap/routes/ledger.cljc b/src/cljc/auto_ap/routes/ledger.cljc index 800b9c3b..7a6e1f9e 100644 --- a/src/cljc/auto_ap/routes/ledger.cljc +++ b/src/cljc/auto_ap/routes/ledger.cljc @@ -6,4 +6,6 @@ "/parse" ::external-import-parse "/import" ::external-import-import} "/table" ::table - "/bank-account-filter" ::bank-account-filter}) \ No newline at end of file + "/bank-account-filter" ::bank-account-filter + "/reports/balance-sheet" {"" ::balance-sheet + "run" ::run-balance-sheet}}) \ No newline at end of file