Removes old login page, more progress on transactions
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -245,25 +245,25 @@
|
||||
vec)
|
||||
[]))
|
||||
|
||||
(def default-read '[* {:transaction/client [:client/name :db/id :client/code :client/locations :client/groups]
|
||||
:transaction/approval-status [:db/ident :db/id]
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance]
|
||||
:transaction/vendor [:db/id :vendor/name]
|
||||
:transaction/matched-rule [:db/id :transaction-rule/note]
|
||||
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
|
||||
:transaction/accounts [:transaction-account/amount
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
{:transaction-account/account [:account/name :db/id
|
||||
:account/location
|
||||
{:account/client-overrides [:account-client-override/name
|
||||
{:account-client-override/client [:db/id]}]}]}]
|
||||
:transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]
|
||||
:transaction/plaid-merchant [:db/id :plaid-merchant/name]}])
|
||||
|
||||
(defn get-by-id [id]
|
||||
(->
|
||||
(dc/pull (dc/db conn)
|
||||
'[* {:transaction/client [:client/name :db/id :client/code :client/locations :client/groups]
|
||||
:transaction/approval-status [:db/ident :db/id]
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance]
|
||||
:transaction/vendor [:db/id :vendor/name]
|
||||
:transaction/matched-rule [:db/id :transaction-rule/note]
|
||||
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
|
||||
:transaction/accounts [:transaction-account/amount
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
{:transaction-account/account [:account/name :db/id
|
||||
:account/location
|
||||
{:account/client-overrides [:account-client-override/name
|
||||
{:account-client-override/client [:db/id]}]}]}]
|
||||
:transaction/yodlee-merchant [:db/id :yodlee-merchant/yodlee-id :yodlee-merchant/name]
|
||||
:transaction/plaid-merchant [:db/id :plaid-merchant/name]}]
|
||||
id)
|
||||
(dc/pull (dc/db conn) default-read id)
|
||||
(update :transaction/date coerce/from-date)
|
||||
(update :transaction/post-date coerce/from-date)
|
||||
(dissoc :transaction/id)))
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
(ns auto-ap.routes.auth
|
||||
(:require
|
||||
[auto-ap.datomic.users :as users]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.session-version :as session-version]
|
||||
[auto-ap.routes.dashboard :as dashboard]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[bidi.bidi :as bidi]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[clj-http.client :as http]
|
||||
[clj-time.core :as time]
|
||||
[auto-ap.logging :as alog]
|
||||
[config.core :refer [env]]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.edn :as edn]
|
||||
[auto-ap.session-version :as session-version]))
|
||||
[clojure.java.io :as io]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]))
|
||||
|
||||
(def google-client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com")
|
||||
(def google-client-secret "OC-WemHurPXYpuIw5cT-B90g")
|
||||
@@ -91,7 +94,9 @@
|
||||
|
||||
(if-let [jwt (user->jwt user token)]
|
||||
{:status 301
|
||||
:headers {"Location" (str (or (not-empty state) "/") "?jwt="
|
||||
:headers {"Location" (str (or (not-empty state)
|
||||
(bidi/path-for ssr-routes/only-routes
|
||||
::dashboard/page)) "?jwt="
|
||||
(jwt/sign jwt
|
||||
(:jwt-secret env)
|
||||
{:alg :hs512}))}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
(ns auto-ap.ssr.auth
|
||||
(:require [auto-ap.session-version :as session-version]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[config.core :refer [env]]))
|
||||
(:require
|
||||
[auto-ap.session-version :as session-version]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[config.core :refer [env]]
|
||||
[hiccup.util :as hu]))
|
||||
|
||||
(defn logout [request]
|
||||
{:status 301
|
||||
@@ -16,3 +22,86 @@
|
||||
{:alg :hs512})
|
||||
:exp)
|
||||
:version session-version/current-session-version}})
|
||||
|
||||
(defn login-url
|
||||
([] (login-url nil))
|
||||
([next]
|
||||
|
||||
(let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com"
|
||||
redirect-uri (str (:base-url env) "/api/oauth")]
|
||||
(str (hu/url "https://accounts.google.com/o/oauth2/auth"
|
||||
(cond-> {"access_type" "online"
|
||||
"client_id" client-id
|
||||
"redirect_uri" redirect-uri
|
||||
"response_type" "code"
|
||||
"max_auth_age" "0"
|
||||
"scope" "https://www.googleapis.com/auth/userinfo.email https://www.googleapis.com/auth/userinfo.profile"}
|
||||
next (assoc "state" (hu/url-encode next))))))))
|
||||
|
||||
(defn- page-contents [request]
|
||||
[:div#app { "@notification.document" "notificationDetails=event.detail.value; showNotification=true"
|
||||
|
||||
:x-data (hx/json {:showError false
|
||||
:errorDetails ""
|
||||
:showNotification false
|
||||
:notificationDetails ""})
|
||||
"@htmx:response-error.camel" "errorDetails = $event.detail.xhr.response; showError=true;"
|
||||
}
|
||||
[:div#app-contents.flex.overflow-hidden
|
||||
[:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " }
|
||||
[:div#notification-holder
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg {:x-show "showNotification" }
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-blue-400
|
||||
{ "@click" "showNotification=false"}
|
||||
svg/filled-x]]
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-blue-800.bg-blue-50.dark:bg-gray-800.dark:text-blue-400.border-blue-300.rounded-lg.border.max-h-96
|
||||
{:x-show "showNotification"
|
||||
"x-transition:enter" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:enter-start" "opacity-0 translate-y-full"
|
||||
"x-transition:enter-end" "opacity-100 translate-y-0"
|
||||
"x-transition:leave" "transition duration-300 transform ease-in-out"
|
||||
"x-transition:leave-start" "opacity-100 translate-y-0"
|
||||
"x-transition:leave-end" "opacity-0 translate-y-full"}
|
||||
|
||||
[:div {:class "p-4 text-lg w-full" :role "alert"}
|
||||
[:div.text-sm
|
||||
[:pre#notification-details.text-xs {:x-html "notificationDetails"}]]]]]]
|
||||
[:div {:x-show "showError"
|
||||
:x-init ""}
|
||||
[:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg
|
||||
[:div.relative
|
||||
[:button.absolute.right-2.top-2.w-6.h-6.z-50.text-red-600
|
||||
{ "@click" "showError=false"}
|
||||
svg/filled-x]]
|
||||
|
||||
[:div.m-4.overflow-auto.z-30.flex.center-items.justify-center.text-red-800.bg-red-50.dark:bg-gray-800.dark:text-red-400.border-red-300.rounded-lg.border.max-h-96
|
||||
{:x-show "showError"
|
||||
"x-transition:enter" "transition duration-300"
|
||||
"x-transition:enter-start" "opacity-0"
|
||||
"x-transition:enter-end" "opacity-100"}
|
||||
|
||||
[:div {:class "p-4 mb-4 text-lg w-full" :role "alert"}
|
||||
[:div.inline-block.w-8.h-8.mr-2 svg/alert]
|
||||
[:span.font-medium "Oh, drat! An unexpected error has occurred."]
|
||||
[:div.text-sm {:x-data (hx/json {"expandError" false})}
|
||||
[:p "Integreat staff have been notified and are looking into it. "]
|
||||
[:p "To see error details, " [:a.underline.cursor-pointer {"@click" "expandError=true"} "click here"] "."]
|
||||
[:pre#error-details.text-xs {:x-show "expandError" :x-text "errorDetails"}]]]]]]
|
||||
[:div.p-4.flex.flex-row.justify-center.items-center.h-screen
|
||||
(com/card {:class "animate-slideUp"}
|
||||
|
||||
[:div.p-4
|
||||
[:img {:src "/img/logo-big.png"}]
|
||||
[:div
|
||||
[:a.button.is-large.is-primary {:href (login-url (get (:query-params request) "redirect-to"))} "Login with Google"]]
|
||||
"HELLO"])
|
||||
]]] ])
|
||||
|
||||
(defn login [request]
|
||||
(base-page
|
||||
request
|
||||
(page-contents request)
|
||||
|
||||
"Dashboard"))
|
||||
@@ -1,23 +1,25 @@
|
||||
(ns auto-ap.ssr.components.aside
|
||||
(:require [auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.graphql.utils :refer [is-admin?]]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
[auto-ap.routes.admin.clients :as ac-routes]
|
||||
[auto-ap.routes.admin.excel-invoices :as ei-routes]
|
||||
[auto-ap.routes.admin.import-batch :as ib-routes]
|
||||
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
|
||||
[auto-ap.routes.admin.vendors :as v-routes]
|
||||
[auto-ap.routes.invoice :as invoice-route]
|
||||
[auto-ap.routes.ledger :as ledger-routes]
|
||||
[auto-ap.routes.outgoing-invoice :as oi-routes]
|
||||
[auto-ap.routes.payments :as payment-routes]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components.tags :as tags]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]
|
||||
[hiccup.util :as hu]))
|
||||
(:require
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.graphql.utils :refer [is-admin?]]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
[auto-ap.routes.admin.clients :as ac-routes]
|
||||
[auto-ap.routes.admin.excel-invoices :as ei-routes]
|
||||
[auto-ap.routes.admin.import-batch :as ib-routes]
|
||||
[auto-ap.routes.admin.transaction-rules :as transaction-rules]
|
||||
[auto-ap.routes.admin.vendors :as v-routes]
|
||||
[auto-ap.routes.dashboard :as dashboard]
|
||||
[auto-ap.routes.invoice :as invoice-route]
|
||||
[auto-ap.routes.ledger :as ledger-routes]
|
||||
[auto-ap.routes.outgoing-invoice :as oi-routes]
|
||||
[auto-ap.routes.payments :as payment-routes]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components.tags :as tags]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]
|
||||
[hiccup.util :as hu]))
|
||||
|
||||
(defn menu-button- [params & children]
|
||||
[:div
|
||||
@@ -103,7 +105,8 @@
|
||||
|
||||
[:li
|
||||
(menu-button- {:icon svg/pie
|
||||
:href "/"}
|
||||
:href (bidi/path-for ssr-routes/only-routes
|
||||
::dashboard/page)}
|
||||
"Dashboard")]
|
||||
|
||||
(when (can? (:identity request)
|
||||
|
||||
@@ -167,7 +167,7 @@
|
||||
true (assoc :type (or (:type params) "button"))
|
||||
true (update :class (fn [c]
|
||||
(cond-> c
|
||||
true (str " font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-green-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500 dark:focus:text-white")
|
||||
true (str " font-medium text-gray-900 bg-white border border-gray-200 hover:bg-gray-100 hover:text-primary-700 focus:z-10 focus:ring-2 focus:ring-green-700 focus:text-green-700 dark:bg-gray-700 dark:border-gray-600 dark:text-white dark:hover:text-white dark:hover:bg-gray-600 dark:focus:ring-green-500 dark:focus:text-white disabled:opacity-50")
|
||||
|
||||
(= :small size)
|
||||
(str " text-xs px-3 py-2")
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
(ns auto-ap.ssr.components.navbar
|
||||
(:require [auto-ap.graphql.utils :refer [is-admin? limited-clients]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.company-dropdown :as cd]
|
||||
[auto-ap.ssr.components.buttons :refer [icon-button-]]
|
||||
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]))
|
||||
(:require
|
||||
[auto-ap.graphql.utils :refer [is-admin? limited-clients]]
|
||||
[auto-ap.routes.dashboard :as dashboard]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.company-dropdown :as cd]
|
||||
[auto-ap.ssr.components.buttons :refer [icon-button-]]
|
||||
[auto-ap.ssr.components.user-dropdown :as user-dropdown]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[bidi.bidi :as bidi]))
|
||||
|
||||
(defn navbar- [{:keys [client-selection client identity clients dd-env]}]
|
||||
[:nav {:class "fixed z-30 w-full bg-white border-b border-gray-200 dark:bg-gray-800 dark:border-gray-700"}
|
||||
@@ -17,7 +19,7 @@
|
||||
[:span {:class "sr-only"} "Open sidebar"]
|
||||
[:svg {:class "w-6 h-6", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"}
|
||||
[:path {:clip-rule "evenodd", :fill-rule "evenodd", :d "M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"}]]]
|
||||
[:a {:href "/" :class "flex ml-2 hidden md:mr-24 sm:inline"}
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes ::dashboard/page) :class "flex ml-2 hidden md:mr-24 sm:inline"}
|
||||
[:img {:src "/img/logo-big2.png", :class "h-10", :alt "Integreat logo"}]]
|
||||
(when-not (= "prod" dd-env) [:div.rounded-full.bg-yellow-200.text-lg.text-yellow-800.px-4.hidden.md:block.mr-8 "environment: " dd-env])]
|
||||
|
||||
|
||||
@@ -46,6 +46,7 @@
|
||||
|
||||
(def key->handler
|
||||
(-> {:logout auth/logout
|
||||
:login auth/login
|
||||
:impersonate (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin auth/impersonate)))
|
||||
:admin-history (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page)))
|
||||
:admin-history-search (wrap-client-redirect-unauthenticated (wrap-secure (wrap-admin history/page)))
|
||||
|
||||
@@ -370,7 +370,7 @@
|
||||
(:transaction/payment i)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::route/payment-page)
|
||||
::payment-routes/all-page)
|
||||
{:exact-match-id (:db/id (:transaction/payment i))})
|
||||
:color :primary
|
||||
:content (format "Payment '%s'" (-> i :transaction/payment :payment/date (atime/unparse-local atime/normal-date)))})
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
[auto-ap.ssr.utils
|
||||
:refer [->db-id apply-middleware-to-all-handlers check-allowance
|
||||
check-location-belongs entity-id html-response modal-response
|
||||
ref->enum-schema strip wrap-schema-enforce]]
|
||||
ref->enum-schema strip wrap-entity wrap-schema-enforce]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[clj-time.coerce :as coerce]
|
||||
@@ -261,8 +261,7 @@
|
||||
:body (mm/default-step-body
|
||||
{}
|
||||
[:div {:x-data (hx/json {:clientId (or (fc/field-value (:transaction/client fc/*current*))
|
||||
(:db/id (:client request)))
|
||||
:vendorId (fc/field-value (:transaction/vendor fc/*current*))})}
|
||||
(:db/id (:client request))) })}
|
||||
|
||||
;; Read-only transaction details
|
||||
[:div.mb-6.border.rounded-lg.p-4.bg-gray-50
|
||||
@@ -380,8 +379,7 @@
|
||||
:placeholder "Search..."
|
||||
: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))
|
||||
:x-model "vendorId"})]))
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c)) })]))
|
||||
[:div.mb-4
|
||||
[:span.text-sm.text-gray-500 "Can't find the vendor? "
|
||||
(com/link {:href (bidi.bidi/path-for
|
||||
@@ -525,11 +523,11 @@
|
||||
|
||||
|
||||
(defn get-available-payments [request]
|
||||
(let [tx-id (or (-> request :multi-form-state :snapshot :db/id)
|
||||
(let [tx-id (or (get-in request [:form-params :transaction-id])
|
||||
(-> request :multi-form-state :snapshot :db/id)
|
||||
(get-in request [:route-params :db/id]))
|
||||
tx (when tx-id (d-transactions/get-by-id tx-id))
|
||||
client-id (or (get-in request [:multi-form-state :snapshot :transaction/client])
|
||||
(get-in request [:client :db/id]))
|
||||
client-id (-> tx :transaction/client :db/id)
|
||||
payments (when client-id
|
||||
(dc/q '[:find [(pull ?p [:db/id :payment/invoice-number :payment/amount :payment/date
|
||||
{:payment/vendor [:db/id :vendor/name]}]) ...]
|
||||
@@ -541,36 +539,7 @@
|
||||
client-id))]
|
||||
(filter #(dollars= (Math/abs (:transaction/amount tx)) (:payment/amount %)) payments)))
|
||||
|
||||
(defn payment-matches-view [request]
|
||||
(let [payments (get-available-payments request)]
|
||||
[:div
|
||||
(if (seq payments)
|
||||
[:div
|
||||
[:h3.text-lg.font-bold.mb-4 "Available Payments"]
|
||||
[:div {:hx-post (bidi/path-for ssr-routes/only-routes ::route/match-payment)
|
||||
:hx-trigger "matchPayment"
|
||||
:hx-target "#modal-holder"
|
||||
:hx-include "this"
|
||||
:hx-swap "outerHTML"}
|
||||
(com/hidden {:name "action"
|
||||
:value "link-payment"
|
||||
:form ""})
|
||||
(com/hidden {:name "transaction-id"
|
||||
:value (-> request :multi-form-state :snapshot :db/id)
|
||||
:form ""})
|
||||
[:div.space-y-2
|
||||
[:label.block.text-sm.font-medium.mb-1 "Select a payment to match:"]
|
||||
(when payments
|
||||
(com/radio-card {:options (for [payment payments]
|
||||
{:value (:db/id payment)
|
||||
:content (str (:payment/invoice-number payment) " - "
|
||||
(-> payment :payment/vendor :vendor/name)
|
||||
" - Amount: $" (format "%.2f" (:payment/amount payment))
|
||||
" • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))})
|
||||
:name "payment-id"
|
||||
:width "w-full"}))
|
||||
(com/a-button {"@click" "$dispatch('matchPayment')"} "Match" #_[:button.mt-4.w-full.py-2.bg-blue-500.text-white.rounded.hover:bg-blue-600 "Match"])]] ]
|
||||
[:div.text-center.py-4.text-gray-500 "No matching payments available for this transaction."]) ]))
|
||||
|
||||
|
||||
(defn get-available-autopay-invoices [request]
|
||||
(let [tx-id (or (-> request :multi-form-state :snapshot :db/id)
|
||||
@@ -705,7 +674,7 @@
|
||||
[:button.mt-4.w-full.py-2.bg-blue-500.text-white.rounded.hover:bg-blue-600 "Apply"]]]
|
||||
[:div.text-center.py-4.text-gray-500 "No matching rules found for this transaction."])]))
|
||||
|
||||
(defn payment-info-view [request]
|
||||
(defn linked-payment-view [request]
|
||||
(let [tx-id (or (-> request :multi-form-state :snapshot :db/id)
|
||||
(get-in request [:route-params :db/id]))
|
||||
tx (when tx-id (d-transactions/get-by-id tx-id))
|
||||
@@ -738,17 +707,52 @@
|
||||
[:div.font-medium "Date"]
|
||||
[:div (some-> payment :payment/date (atime/unparse-local atime/normal-date))]]
|
||||
[:div.mt-4 {:hx-post (bidi/path-for ssr-routes/only-routes ::route/unlink-payment)
|
||||
:hx-params "transaction-id"
|
||||
:hx-params "transaction-id, action"
|
||||
:hx-trigger "unlinkPayment"
|
||||
:hx-target "#payment-matches"
|
||||
:hx-include "this"
|
||||
:hx-target "#modal-holder"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-confirm "Are you sure you want to unlink this payment?"}
|
||||
|
||||
|
||||
(com/hidden {:name "action"
|
||||
:value "unlink-payment"
|
||||
:form ""})
|
||||
(com/hidden {:name "transaction-id" :value tx-id :form ""})
|
||||
(com/a-button {:color :red :size :small
|
||||
"@click" "$dispatch('unlinkPayment')"} "Unlink Payment")]]])))
|
||||
|
||||
(defn payment-matches-view [request]
|
||||
(let [payments (get-available-payments request)]
|
||||
[:div#payment-matches
|
||||
(linked-payment-view request)
|
||||
(if (seq payments)
|
||||
[:div
|
||||
[:h3.text-lg.font-bold.mb-4 "Available Payments"]
|
||||
[:div {:hx-post (bidi/path-for ssr-routes/only-routes ::route/link-payment)
|
||||
:hx-trigger "matchPayment"
|
||||
:hx-target "#modal-holder"
|
||||
:hx-include "this"
|
||||
:hx-swap "outerHTML"}
|
||||
(com/hidden {:name "action"
|
||||
:value "link-payment"
|
||||
:form ""})
|
||||
(com/hidden {:name "transaction-id"
|
||||
:value (-> request :entity :db/id)
|
||||
:form ""})
|
||||
[:div.space-y-2
|
||||
[:label.block.text-sm.font-medium.mb-1 "Select a payment to match:"]
|
||||
(when payments
|
||||
(com/radio-card {:options (for [payment payments]
|
||||
{:value (:db/id payment)
|
||||
:content (str (:payment/invoice-number payment) " - "
|
||||
(-> payment :payment/vendor :vendor/name)
|
||||
" - Amount: $" (format "%.2f" (:payment/amount payment))
|
||||
" • Date: " (some-> payment :payment/date coerce/to-date-time (atime/unparse-local atime/normal-date)))})
|
||||
:name "payment-id"
|
||||
:width "w-full"}))
|
||||
(com/a-button {"@click" "$dispatch('matchPayment')"} "Match" #_[:button.mt-4.w-full.py-2.bg-blue-500.text-white.rounded.hover:bg-blue-600 "Match"])]]]
|
||||
[:div.text-center.py-4.text-gray-500 "No matching payments available for this transaction."])]))
|
||||
|
||||
(defn count-payment-matches [request]
|
||||
(count (get-available-payments request)))
|
||||
|
||||
@@ -781,8 +785,12 @@
|
||||
:body (mm/default-step-body
|
||||
{}
|
||||
[:div
|
||||
(payment-info-view request)
|
||||
[:div {:x-data "{ activeForm: null }"}
|
||||
|
||||
[:div {:x-data (hx/json {:activeForm (if (:transaction/payment (:entity request))
|
||||
"payment"
|
||||
nil)
|
||||
:canChange (boolean (not (:transaction/payment (:entity request))))})
|
||||
"@unlinked" "canChange=true"}
|
||||
[:div {:class "flex space-x-2 mb-4"}
|
||||
(com/button-group {:name "method"}
|
||||
(com/button-group-button {"@click" "activeForm = 'payment'" :value "payment" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'payment'}" :class "relative"}
|
||||
@@ -790,24 +798,29 @@
|
||||
(when (> count 0)
|
||||
(com/badge {:color "green"} (str count))))
|
||||
"Link to payment")
|
||||
(com/button-group-button {"@click" "activeForm = 'unpaid'" :value "unpaid" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'unpaid'}" :class "relative"}
|
||||
(com/button-group-button {"@click" "activeForm = 'unpaid'" :value "unpaid" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'unpaid'}" :class "relative"
|
||||
":disabled" "!canChange"}
|
||||
(let [count (count-unpaid-invoice-matches request)]
|
||||
(when (> count 0)
|
||||
(com/badge {:color "green"} (str count))))
|
||||
"Link to unpaid invoices")
|
||||
(com/button-group-button {"@click" "activeForm = 'autopay'" :value "autopay" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'autopay'}" :class "relative"}
|
||||
(com/button-group-button {"@click" "activeForm = 'autopay'" :value "autopay" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'autopay'}" :class "relative"
|
||||
":disabled" "!canChange"}
|
||||
(let [count (count-autopay-invoice-matches request)]
|
||||
(when (> count 0)
|
||||
(com/badge {:color "green"} (str count))))
|
||||
"Link to autopay invoices")
|
||||
(com/button-group-button {"@click" "activeForm = 'rule'" :value "rule" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'rule'}" :class "relative"}
|
||||
(com/button-group-button {"@click" "activeForm = 'rule'" :value "rule" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'rule'}" :class "relative"
|
||||
":disabled" "!canChange"}
|
||||
(let [count (count-rule-matches request)]
|
||||
(when (> count 0)
|
||||
(com/badge {:color "green"} (str count))))
|
||||
"Apply rule")
|
||||
(com/button-group-button {"@click" "activeForm = 'manual'" :value "manual" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'manual'}"}
|
||||
(com/button-group-button {"@click" "activeForm = 'manual'" :value "manual" ":class" "{ '!bg-primary-200 text-primary-800': activeForm === 'manual'}"
|
||||
":disabled" "!canChange"}
|
||||
"Manual"))]
|
||||
[:div {:x-show "activeForm === 'payment'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
|
||||
(payment-matches-view request)]
|
||||
[:div {:x-show "activeForm === 'unpaid'", :x-transition:enter "transition ease-out duration-500", :x-transition:enter-start "opacity-0 transform scale-95", :x-transition:enter-end "opacity-100 transform scale-100"}
|
||||
(unpaid-invoices-view request)]
|
||||
@@ -960,9 +973,9 @@
|
||||
[]
|
||||
entity)))
|
||||
|
||||
(defn match-payment [{{:keys [transaction-id match-payment-id]} :form-params :as request}]
|
||||
(defn link-payment [{{:keys [transaction-id payment-id]} :form-params :as request}]
|
||||
(let [transaction (d-transactions/get-by-id transaction-id)
|
||||
payment (d-checks/get-by-id match-payment-id)]
|
||||
payment (d-checks/get-by-id payment-id)]
|
||||
|
||||
(exception->4xx #(assert-can-see-client (:identity request) (-> transaction :transaction/client :db/id)))
|
||||
(exception->4xx #(assert-can-see-client (:identity request) (-> payment :payment/client :db/id)))
|
||||
@@ -1000,7 +1013,7 @@
|
||||
[:p.text-gray-600.mt-2 "The transaction has been linked to the autopay invoices."]
|
||||
[:p.text-gray-600.mt-2 "To view the new payment, click "
|
||||
(com/link {:href (hu/url (bidi/path-for ssr-routes/only-routes ::payment-route/all-page)
|
||||
{:exact-match-id match-payment-id})
|
||||
{:exact-match-id payment-id})
|
||||
:hx-boost true}
|
||||
"here")
|
||||
" to view it."])
|
||||
@@ -1201,8 +1214,10 @@
|
||||
(:identity request))))
|
||||
|
||||
(solr/touch-with-ledger transaction-id)
|
||||
(html-response (payment-matches-view request)
|
||||
:headers {"hx-trigger" "unlinked"})
|
||||
|
||||
(modal-response
|
||||
#_(modal-response
|
||||
(com/success-modal {:title "Transaction unlinked successfully"}
|
||||
|
||||
[:p.text-gray-600.mt-2 "The transaction has been unlinked from its payment."]
|
||||
@@ -1245,40 +1260,42 @@
|
||||
{::route/edit-wizard (-> mm/open-wizard-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-init-multi-form-state initial-edit-wizard-state)
|
||||
(wrap-entity [:route-params :db/id] d-transactions/default-read)
|
||||
(wrap-schema-enforce :route-schema [:map [:db/id entity-id]]))
|
||||
::route/edit-wizard-navigate (-> mm/next-handler
|
||||
(wrap-entity [:multi-form-state :snapshot :db/id] d-transactions/default-read)
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-submit (-> mm/submit-handler
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/location-select (-> location-select
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:name :string]
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]
|
||||
[:account-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:name :string]
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]
|
||||
[:account-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/account-total (-> account-total
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/account-balance (-> account-balance
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/account-balance (-> account-balance
|
||||
(mm/wrap-wizard edit-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/edit-wizard-new-account (->
|
||||
(add-new-entity-handler [:step-params :transaction/accounts]
|
||||
(fn render [cursor request]
|
||||
(transaction-account-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request))}))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :transaction-account/location "Shared")))
|
||||
(fn render [cursor request]
|
||||
(transaction-account-row*
|
||||
{:value cursor
|
||||
:client-id (:client-id (:query-params request))}))
|
||||
(fn build-new-row [base _]
|
||||
(assoc base :transaction-account/location "Shared")))
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:client-id {:optional true}
|
||||
[:maybe entity-id]]]))
|
||||
::route/match-payment (-> match-payment
|
||||
(wrap-schema-enforce :form-schema
|
||||
save-schema))
|
||||
::route/link-payment (-> link-payment
|
||||
(wrap-schema-enforce :form-schema
|
||||
save-schema))
|
||||
::route/match-autopay-invoices (-> match-autopay-invoices
|
||||
(wrap-schema-enforce :form-schema
|
||||
[:map [:transaction-id entity-id]
|
||||
@@ -1288,11 +1305,12 @@
|
||||
[:map [:transaction-id entity-id]
|
||||
[:unpaid-invoice-ids [:vector {:coerce? true} entity-id]]]))
|
||||
::route/apply-rule (-> apply-rule
|
||||
(wrap-schema-enforce :form-schema
|
||||
[:map [:transaction-id entity-id]
|
||||
[:rule-id entity-id]]))
|
||||
(wrap-schema-enforce :form-schema
|
||||
[:map [:transaction-id entity-id]
|
||||
[:rule-id entity-id]]))
|
||||
::route/unlink-payment (-> unlink-payment
|
||||
(wrap-schema-enforce :form-schema
|
||||
(wrap-entity [:form-params :transaction-id] d-transactions/default-read)
|
||||
(wrap-schema-enforce :form-schema
|
||||
save-schema))}
|
||||
(fn [h]
|
||||
(-> h
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
(ns auto-ap.client-routes)
|
||||
|
||||
(def routes ["/" {"" :index
|
||||
"login" :login
|
||||
"login/" :login
|
||||
#_#_"login" :login
|
||||
#_#_"login/" :login
|
||||
"needs-activation/" :needs-activation
|
||||
"needs-activation" :needs-activation
|
||||
"payments/" :payments
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
"/account-total" ::account-total
|
||||
"/account-balance" ::account-balance
|
||||
"/edit-wizard-new-account" ::edit-wizard-new-account
|
||||
"/match-payment" ::match-payment
|
||||
"/match-payment" ::link-payment
|
||||
"/match-autopay-invoices" ::match-autopay-invoices
|
||||
"/match-unpaid-invoices" ::match-unpaid-invoices
|
||||
"/apply-rule" ::apply-rule
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
|
||||
(def routes {"impersonate" :impersonate
|
||||
"logout" :logout
|
||||
"login" :login
|
||||
"search" :search
|
||||
"indicators" indicator-routes/routes
|
||||
|
||||
|
||||
@@ -32,11 +32,17 @@ module.exports = {
|
||||
"transform": "scale(1.0)",
|
||||
"animation-timing-function": "cubic-bezier(0.8, 0, 1, 1)"
|
||||
}
|
||||
},
|
||||
slideUp: {
|
||||
'0%': { transform: 'translateY(20px)', opacity: '0' },
|
||||
'100%': { transform: 'translateY(0)', opacity: '1' },
|
||||
|
||||
}
|
||||
},
|
||||
animation: {
|
||||
'shake': 'shake 0.5s ease-out 1',
|
||||
"gg": "gentleGrow 1s infinite"
|
||||
"gg": "gentleGrow 1s infinite",
|
||||
"slideUp": 'slideUp 0.5s ease-out forwards'
|
||||
},
|
||||
"fontFamily": {
|
||||
"sans": ["Calibri", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"]
|
||||
|
||||
8
tasks
Normal file
8
tasks
Normal file
@@ -0,0 +1,8 @@
|
||||
* Convert transaction form to one where you pick options and hit a single endpoint to persist.
|
||||
* Add tests for edit transaction.
|
||||
* Make it so you can create a new vendor again.
|
||||
* Switch login screen
|
||||
* Hide unhelpful report from the dashboard
|
||||
* Check permissions on ledger, transactions, reports
|
||||
* Make sure that you can't change a transaction if its payment is set
|
||||
* also add tests
|
||||
@@ -7,7 +7,7 @@
|
||||
check-vendor-default-account
|
||||
clientize-vendor get-vendor
|
||||
location-select*
|
||||
match-autopay-invoices match-payment
|
||||
match-autopay-invoices link-payment
|
||||
match-unpaid-invoices
|
||||
transaction-account-row*
|
||||
unlink-payment]]
|
||||
@@ -87,7 +87,7 @@
|
||||
:transaction/amount 50.0
|
||||
:transaction/client {:db/id "client-id"}}])
|
||||
;; Perform match-payment
|
||||
(match-payment {:form-params {"transaction-id" "transaction-id" "payment-id" "payment-id"}})
|
||||
(link-payment {:form-params {"transaction-id" "transaction-id" "payment-id" "payment-id"}})
|
||||
;; Verify payment and transaction are linked
|
||||
(let [transaction (d-transactions/get-by-id "transaction-id")
|
||||
payment (dc/pull (dc/db conn) '[:db/id :payment/status] "payment-id")]
|
||||
|
||||
Reference in New Issue
Block a user