Merge branch 'staging'
This commit is contained in:
@@ -8,6 +8,15 @@ document.addEventListener('alpine:init', () => {
|
||||
el.removeEventListener('htmx:configRequest', config);
|
||||
})
|
||||
})
|
||||
Alpine.directive('hx-header', (el, { value, expression }, { evaluateLater, effect, cleanup, evaluate }) => {
|
||||
var config = function(evt) {
|
||||
evt.detail.headers[value] = evaluate(expression); // add a new parameter into the request
|
||||
}
|
||||
el.addEventListener('htmx:configRequest', config);
|
||||
cleanup(() => {
|
||||
el.removeEventListener('htmx:configRequest', config);
|
||||
})
|
||||
})
|
||||
|
||||
Alpine.directive('dispatch', (el, { value, expression }, { evaluateLater, effect, cleanup, evaluate }) => {
|
||||
let dependent_properties = evaluateLater(expression)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -601,7 +601,8 @@
|
||||
(:sort args)))
|
||||
|
||||
(defn apply-sort-3 [args results]
|
||||
(let [sort-bys (conj (:sort args)
|
||||
|
||||
(let [sort-bys (conj (into [] (:sort args))
|
||||
{:sort-key "default" :asc (if (contains? args :default-asc?)
|
||||
(:default-asc? args)
|
||||
true)})
|
||||
@@ -609,16 +610,17 @@
|
||||
comparator (fn [xs ys]
|
||||
(reduce
|
||||
(fn [_ i]
|
||||
|
||||
(let [comparison (if (:asc (nth sort-bys i))
|
||||
(compare (nth xs i) (nth ys i))
|
||||
(compare (nth ys i) (nth xs i)))]
|
||||
|
||||
|
||||
(if (not= 0 comparison)
|
||||
(reduced comparison)
|
||||
0)))
|
||||
0
|
||||
(range length)))]
|
||||
(sort comparator results )))
|
||||
(sort comparator results)))
|
||||
|
||||
(defn apply-pagination-raw [args results]
|
||||
{:entries (->> results
|
||||
@@ -633,7 +635,8 @@
|
||||
(:per-page args)
|
||||
default-pagination-size))
|
||||
(map last))
|
||||
:count (count results)})
|
||||
:count (count results)
|
||||
:all-ids (map last results)})
|
||||
|
||||
(defn audit-transact-batch [txes id]
|
||||
(let [batch-id (.toString (java.util.UUID/randomUUID))]
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
(:require [amazonica.core :refer [defcredential]]
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.datomic :refer [conn pull-many]]
|
||||
[auto-ap.datomic.clients :as d-clients]
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client
|
||||
limited-clients]]
|
||||
[auto-ap.graphql.utils :refer [limited-clients]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.routes.auth :as auth]
|
||||
[auto-ap.routes.exports :as exports]
|
||||
@@ -14,8 +12,10 @@
|
||||
[auto-ap.routes.invoices :as invoices]
|
||||
[auto-ap.routes.queries :as queries]
|
||||
[auto-ap.routes.yodlee2 :as yodlee2]
|
||||
[auto-ap.session-version :as session-version]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.core :as ssr]
|
||||
[auto-ap.ssr.utils :refer [entity-id main-transformer]]
|
||||
[bidi.bidi :as bidi]
|
||||
[bidi.ring :refer [->ResourcesMaybe make-handler]]
|
||||
[buddy.auth.backends.session :refer [session-backend]]
|
||||
@@ -26,13 +26,14 @@
|
||||
[cheshire.core :as cheshire]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.data.json :as json]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[config.core :refer [env]]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]
|
||||
[malli.core :as mc]
|
||||
[ring.middleware.edn :refer [wrap-edn-params]]
|
||||
[ring.middleware.multipart-params :as mp]
|
||||
[ring.middleware.params :refer [wrap-params]]
|
||||
@@ -110,10 +111,10 @@
|
||||
uri
|
||||
:request-method request-method))
|
||||
matched-hx-current-url-route (some->> (get headers "hx-current-url")
|
||||
url/url
|
||||
:path
|
||||
(bidi/match-route ssr-routes/only-routes)
|
||||
:handler)]
|
||||
url/url
|
||||
:path
|
||||
(bidi/match-route ssr-routes/only-routes)
|
||||
:handler)]
|
||||
(handler (assoc request
|
||||
:matched-route
|
||||
matched-route
|
||||
@@ -135,6 +136,7 @@
|
||||
(:uri request)
|
||||
:request-method (:request-method request)))
|
||||
|
||||
:client-selection (:client-selection request)
|
||||
:source "request"
|
||||
:query (:uri request)
|
||||
:request-method (:request-method request)
|
||||
@@ -158,10 +160,12 @@
|
||||
:exception e)
|
||||
(throw e)))))))
|
||||
|
||||
|
||||
|
||||
(defn wrap-idle-session-timeout
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [session (:session request {})
|
||||
(let [session (:session request {:version session-version/current-session-version})
|
||||
end-time (coerce/to-date-time (::idle-timeout session))]
|
||||
(if (and end-time (time/before? end-time (time/now)))
|
||||
(if (get (:headers request) "hx-request")
|
||||
@@ -185,10 +189,17 @@
|
||||
request (assoc request :hx-query-params query-params)]
|
||||
(handler request))))
|
||||
|
||||
(def client-selection-schema
|
||||
(mc/schema
|
||||
[:orn
|
||||
[:global [:enum :all :mine]]
|
||||
[:group-name [:map [:group :string]]]
|
||||
[:specific [:map [:selected [:vector entity-id]]]]]))
|
||||
|
||||
(defn wrap-hydrate-clients
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [x-clients (-> request :session :client-selection)
|
||||
(let [x-clients (-> request :client-selection)
|
||||
identity (or (-> request :identity)
|
||||
(-> request :session :identity))
|
||||
ideal-ids (set (cond
|
||||
@@ -202,28 +213,21 @@
|
||||
(= :mine x-clients)
|
||||
(map :db/id (:user/clients identity))
|
||||
|
||||
(= :group (first x-clients))
|
||||
(:group x-clients)
|
||||
(->>
|
||||
(dc/q '[:find ?c
|
||||
:in $ ?g
|
||||
:where [?c :client/groups ?g]]
|
||||
(dc/db conn)
|
||||
(str/upper-case (or (second x-clients) "INVALID")))
|
||||
(str/upper-case (or (:group x-clients) "INVALID")))
|
||||
(map first)
|
||||
set)
|
||||
|
||||
(seq x-clients)
|
||||
(seq (:selected x-clients))
|
||||
(->> x-clients
|
||||
(map (fn [c]
|
||||
(if (string? c)
|
||||
(try
|
||||
(Long/parseLong c)
|
||||
(catch Exception e
|
||||
nil))
|
||||
c)))
|
||||
:selected
|
||||
(filter #(not (nil? %)))
|
||||
set)))
|
||||
|
||||
limited-clients (some->> (limited-clients identity)
|
||||
(map :db/id)
|
||||
set)
|
||||
@@ -236,12 +240,11 @@
|
||||
(pull-many (dc/db conn)
|
||||
'[:db/id :client/name :client/code :client/locations
|
||||
:client/matches :client/feature-flags
|
||||
{:client/bank-accounts [:db/id
|
||||
{:bank-account/type [:db/ident]}
|
||||
{:client/bank-accounts [:db/id
|
||||
{:bank-account/type [:db/ident]}
|
||||
:bank-account/number
|
||||
:bank-account/name
|
||||
:bank-account/code]}]))]
|
||||
|
||||
(mu/with-context {:clients (take 10 (map :client/code clients))}
|
||||
(handler (assoc request
|
||||
:clients clients
|
||||
@@ -251,33 +254,22 @@
|
||||
(defn wrap-store-client-in-session
|
||||
[handler]
|
||||
(fn [{:keys [headers identity] :as request}]
|
||||
(let [x-clients (edn/read-string (get headers "x-clients"))
|
||||
x-clients (try (if-let [client-id (and x-clients
|
||||
(sequential? x-clients)
|
||||
(first x-clients)
|
||||
(not= :group (first x-clients))
|
||||
(first x-clients))]
|
||||
(do
|
||||
(assert-can-see-client identity (cond-> client-id
|
||||
(string? client-id) (Long/parseLong)))
|
||||
[(if (string? client-id)
|
||||
(Long/parseLong client-id)
|
||||
client-id)])
|
||||
x-clients)
|
||||
(catch Exception e
|
||||
(alog/warn ::cant-access :error e
|
||||
:identity identity
|
||||
:x-clients (pr-str x-clients))
|
||||
:all))
|
||||
new-request (if x-clients
|
||||
(assoc-in request [:session :client-selection] x-clients)
|
||||
request)]
|
||||
(let [client-selection (try (mc/decode client-selection-schema (some-> (get headers "x-clients") not-empty json/read-str) main-transformer)
|
||||
(catch Exception e
|
||||
(alog/warn ::cant-access :error e
|
||||
:identity identity
|
||||
:x-clients (pr-str (get headers "x-clients")))
|
||||
nil))
|
||||
|
||||
new-request (if client-selection
|
||||
(assoc-in request [:client-selection] client-selection)
|
||||
(assoc-in request [:client-selection] (get-in request [:session :client-selection] :all)))]
|
||||
(cond-> (handler new-request)
|
||||
x-clients (update :session
|
||||
(fn [new-session]
|
||||
(-> (:session request)
|
||||
(into new-session)
|
||||
(assoc :client-selection x-clients))))))))
|
||||
client-selection (update :session
|
||||
(fn [new-session]
|
||||
(-> (:session request)
|
||||
(into new-session)
|
||||
(assoc :client-selection client-selection))))))))
|
||||
|
||||
(defn wrap-gunzip-jwt
|
||||
[handler]
|
||||
@@ -317,9 +309,9 @@
|
||||
(-> route-handler
|
||||
(wrap-hx-current-url-params)
|
||||
(wrap-guess-route)
|
||||
(wrap-logging)
|
||||
(wrap-hydrate-clients)
|
||||
(wrap-store-client-in-session)
|
||||
(wrap-logging)
|
||||
(wrap-gunzip-jwt)
|
||||
(wrap-authorization auth-backend)
|
||||
(wrap-authentication auth-backend
|
||||
@@ -327,11 +319,13 @@
|
||||
(dissoc auth :exp))}))
|
||||
|
||||
#_(wrap-pprint-session)
|
||||
|
||||
(session-version/wrap-session-version)
|
||||
(wrap-idle-session-timeout)
|
||||
(wrap-session {:store (cookie-store
|
||||
{:key
|
||||
(byte-array
|
||||
[42, 52, -31, 105, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])})})
|
||||
[42, 52, -31, 101, -126, -33, -118, -69, -82, -59, -15, -69, -38, 103, -102, -1])})})
|
||||
|
||||
#_(wrap-reload)
|
||||
(wrap-params)
|
||||
|
||||
@@ -20,9 +20,7 @@
|
||||
|
||||
(defn send-email-about-failed-message [mail-bucket mail-key message]
|
||||
(let [target-key (str "failed-emails/" mail-key ".eml")
|
||||
target-url (str "http://" (:data-bucket env)
|
||||
".s3-website-us-east-1.amazonaws.com/"
|
||||
target-key)]
|
||||
target-url (str "https://" (:data-bucket env) "/" target-key)]
|
||||
(alog/info ::sending-failure-email :who (:import-failure-destination-emails env))
|
||||
(s3/copy-object mail-bucket mail-key (:data-bucket env) target-key)
|
||||
(ses/send-email {:destination {:to-addresses (:import-failure-destination-emails env)}
|
||||
@@ -66,8 +64,8 @@
|
||||
:content-length (.length (io/file filename))})
|
||||
(let [imports (->> (parse/parse-file filename filename)
|
||||
(map #(assoc %
|
||||
:source-url (str "http://" (:data-bucket env)
|
||||
".s3-website-us-east-1.amazonaws.com/"
|
||||
:source-url (str "https://" (:data-bucket env)
|
||||
"/"
|
||||
s3-location)
|
||||
:import-status :import-status/imported)))]
|
||||
(alog/info ::found-imports :imports imports)
|
||||
|
||||
@@ -273,7 +273,7 @@
|
||||
(mapcat (fn [k]
|
||||
(try
|
||||
(let [invoice-key (copy-readable-version k)
|
||||
invoice-url (str "http://" bucket-name ".s3-website-us-east-1.amazonaws.com/" invoice-key)]
|
||||
invoice-url (str "https://" bucket-name "/" invoice-key)]
|
||||
(with-open [is (-> (s3/get-object {:bucket-name bucket-name
|
||||
:key k})
|
||||
:input-stream)]
|
||||
|
||||
@@ -134,7 +134,7 @@
|
||||
(mapcat (fn [k]
|
||||
(try
|
||||
(let [invoice-key (str "invoice-files/" (UUID/randomUUID) ".csv") ;
|
||||
invoice-url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" invoice-key)]
|
||||
invoice-url (str "https://" (:data-bucket env) "/" invoice-key)]
|
||||
(s3/copy-object {:source-bucket-name (:data-bucket env)
|
||||
:destination-bucket-name (:data-bucket env)
|
||||
:source-key k
|
||||
|
||||
@@ -338,7 +338,7 @@
|
||||
pdf-data (make-pnl args data)
|
||||
name (pnl-args->name args)
|
||||
key (str "reports/pnl/" uuid "/" name ".pdf")
|
||||
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
|
||||
url (str "https://" (:data-bucket env) "/" key)]
|
||||
(s3/put-object :bucket-name (:data-bucket env)
|
||||
:key key
|
||||
:input-stream (io/make-input-stream pdf-data {})
|
||||
@@ -359,7 +359,7 @@
|
||||
pdf-data (make-cash-flows args data)
|
||||
name (cash-flows-args->name args)
|
||||
key (str "reports/cash-flows/" uuid "/" name ".pdf")
|
||||
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
|
||||
url (str "https://" (:data-bucket env) "/" key)]
|
||||
(s3/put-object :bucket-name (:data-bucket env)
|
||||
:key key
|
||||
:input-stream (io/make-input-stream pdf-data {})
|
||||
@@ -380,7 +380,7 @@
|
||||
pdf-data (make-balance-sheet args data)
|
||||
name (balance-sheet-args->name args)
|
||||
key (str "reports/balance-sheet/" uuid "/" name ".pdf")
|
||||
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
|
||||
url (str "https://" (:data-bucket env) "/" key)]
|
||||
(s3/put-object :bucket-name (:data-bucket env)
|
||||
:key key
|
||||
:input-stream (io/make-input-stream pdf-data {})
|
||||
@@ -401,7 +401,7 @@
|
||||
pdf-data (make-journal-detail-report args data)
|
||||
name (journal-detail-args->name args)
|
||||
key (str "reports/journal-detail/" uuid "/" name ".pdf")
|
||||
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
|
||||
url (str "https://" (:data-bucket env) "/" key)]
|
||||
(s3/put-object :bucket-name (:data-bucket env)
|
||||
:key key
|
||||
:input-stream (io/make-input-stream pdf-data {})
|
||||
|
||||
@@ -4,6 +4,9 @@
|
||||
[clj-time.core :as time]
|
||||
[clojure.string :as str]))
|
||||
|
||||
(defn wrap-copy-qp-pqp [handler]
|
||||
(fn [request]
|
||||
(handler (assoc request :parsed-query-params (:query-params request)))))
|
||||
|
||||
(defn wrap-parse-query-params [handler parser]
|
||||
(fn parsed-handler [request]
|
||||
@@ -42,13 +45,13 @@
|
||||
[]))
|
||||
|
||||
(defn parse-long [l]
|
||||
(try
|
||||
(try
|
||||
(Long/parseLong l)
|
||||
(catch Exception e
|
||||
nil)))
|
||||
|
||||
(defn parse-double [l]
|
||||
(try
|
||||
(try
|
||||
(Double/parseDouble l)
|
||||
(catch Exception e
|
||||
nil)))
|
||||
@@ -58,9 +61,10 @@
|
||||
(dissoc
|
||||
(condp = (source-key query-params)
|
||||
"week"
|
||||
(assoc query-params
|
||||
start-date-key (time/plus (time/now) (time/days -7))
|
||||
end-date-key (time/now))
|
||||
(let [last-monday (atime/last-monday)]
|
||||
(assoc query-params
|
||||
start-date-key (time/plus last-monday (time/days -7))
|
||||
end-date-key last-monday))
|
||||
|
||||
"month"
|
||||
(assoc query-params
|
||||
@@ -74,7 +78,7 @@
|
||||
|
||||
"all"
|
||||
(assoc query-params
|
||||
start-date-key (time/plus (time/now) (time/years -3))
|
||||
start-date-key (time/plus (time/now) (time/years -6))
|
||||
end-date-key (time/now))
|
||||
|
||||
query-params)
|
||||
@@ -88,18 +92,18 @@
|
||||
presently-sorted? ((set (map :sort-key current-sort)) key-to-toggle)
|
||||
new-sort (if presently-sorted?
|
||||
(mapv
|
||||
(fn [s]
|
||||
(if (= (:sort-key s)
|
||||
key-to-toggle)
|
||||
(-> s
|
||||
(update :asc
|
||||
#(boolean (not %)))
|
||||
(update :sort-icon (fn [x]
|
||||
(if (= x svg/sort-down)
|
||||
svg/sort-up
|
||||
svg/sort-down))))
|
||||
s))
|
||||
current-sort)
|
||||
(fn [s]
|
||||
(if (= (:sort-key s)
|
||||
key-to-toggle)
|
||||
(-> s
|
||||
(update :asc
|
||||
#(boolean (not %)))
|
||||
(update :sort-icon (fn [x]
|
||||
(if (= x svg/sort-down)
|
||||
svg/sort-up
|
||||
svg/sort-down))))
|
||||
s))
|
||||
current-sort)
|
||||
(conj current-sort {:sort-key key-to-toggle
|
||||
:asc true
|
||||
:name (:name (first (filter #(= (str key-to-toggle) (:sort-key %)) (:headers grid-spec))))
|
||||
|
||||
@@ -8,7 +8,8 @@
|
||||
[config.core :refer [env]]
|
||||
[com.brunobonacci.mulog :as mu]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.edn :as edn]))
|
||||
[clojure.edn :as edn]
|
||||
[auto-ap.session-version :as session-version]))
|
||||
|
||||
(def google-client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com")
|
||||
(def google-client-secret "OC-WemHurPXYpuIw5cT-B90g")
|
||||
@@ -94,7 +95,8 @@
|
||||
(jwt/sign jwt
|
||||
(:jwt-secret env)
|
||||
{:alg :hs512}))}
|
||||
:session {:identity (dissoc jwt :exp)}}
|
||||
:session {:identity (dissoc jwt :exp)
|
||||
:version session-version/current-session-version}}
|
||||
{:status 401
|
||||
:body "Couldn't authenticate"}))
|
||||
(catch Exception e
|
||||
|
||||
@@ -193,8 +193,9 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:request request
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
|
||||
@@ -277,8 +277,8 @@
|
||||
:client-override client
|
||||
:location-override location
|
||||
:vendor-override vendor
|
||||
:source-url (str "http://" (:data-bucket env)
|
||||
".s3-website-us-east-1.amazonaws.com/"
|
||||
:source-url (str "https://" (:data-bucket env)
|
||||
"/"
|
||||
s3-location))))]
|
||||
(import-uploaded-invoice user imports))
|
||||
{:status 200
|
||||
|
||||
34
src/clj/auto_ap/session_version.clj
Normal file
34
src/clj/auto_ap/session_version.clj
Normal file
@@ -0,0 +1,34 @@
|
||||
(ns auto-ap.session-version
|
||||
(:require [bidi.bidi :as bidi]
|
||||
[auto-ap.logging :as alog]))
|
||||
|
||||
;; TODO this should only be done until SSR is complete
|
||||
;; once it is, it should just use redirects based on headers
|
||||
;; no header=use default, mismatch header=redirect to login
|
||||
(def current-session-version 2)
|
||||
(defn wrap-session-version
|
||||
[handler]
|
||||
(fn [request]
|
||||
(let [session (:session request)
|
||||
route (bidi/match-route @(resolve 'auto-ap.handler/all-routes)
|
||||
(:uri request)
|
||||
:request-method (:request-method request))
|
||||
is-normal-route? (or (keyword? route)
|
||||
(keyword? (:handler route)))] ;; TODO SSR icky
|
||||
(if (and (not= (:version session current-session-version) current-session-version)
|
||||
|
||||
is-normal-route?)
|
||||
(cond
|
||||
(or (= :graphql (:handler route))
|
||||
(= :graphql route))
|
||||
{:status 401}
|
||||
|
||||
(get (:headers request) "hx-request")
|
||||
{:session nil
|
||||
:status 200
|
||||
:headers {"hx-redirect" "/login"}}
|
||||
:else
|
||||
{:session nil
|
||||
:status 302
|
||||
:headers {"Location" "/login"}})
|
||||
(handler request)))))
|
||||
@@ -45,7 +45,7 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
pull-many query2]]
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.routes.admin.clients :as route]
|
||||
[auto-ap.routes.indicators :as indicators]
|
||||
[auto-ap.routes.queries :as q]
|
||||
@@ -27,7 +28,7 @@
|
||||
:refer [apply-middleware-to-all-handlers entity-id
|
||||
form-validation-error html-response main-transformer
|
||||
many-entity modal-response ref->enum-schema strip temp-id
|
||||
wrap-entity wrap-schema-enforce]]
|
||||
wrap-entity wrap-schema-enforce wrap-merge-prior-hx]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cheshire.core :as cheshire]
|
||||
@@ -77,7 +78,7 @@
|
||||
(com/text-input {:name "name"
|
||||
:id "name"
|
||||
:class "hot-filter"
|
||||
:value (:name (:parsed-query-params request))
|
||||
:value (:name (:query-params request))
|
||||
:placeholder "Best Restaurant LLC"
|
||||
:size :small}))
|
||||
|
||||
@@ -85,14 +86,14 @@
|
||||
(com/text-input {:name "code"
|
||||
:id "code"
|
||||
:class "hot-filter"
|
||||
:value (:code (:parsed-query-params request))
|
||||
:value (:code (:query-params request))
|
||||
:placeholder "BRLC"
|
||||
:size :small}))
|
||||
(com/field {:label "Group"}
|
||||
(com/text-input {:name "group"
|
||||
:id "group"
|
||||
:class "hot-filter"
|
||||
:value (:group (:parsed-query-params request))
|
||||
:value (:group (:query-params request))
|
||||
:placeholder "NTG"
|
||||
:size :small}))
|
||||
(com/field {:label "Select"}
|
||||
@@ -150,7 +151,7 @@
|
||||
:client/location-matches [:location-match/matches :location-match/location :db/id]}])
|
||||
|
||||
(defn fetch-ids [db request]
|
||||
(let [query-params (:parsed-query-params request)
|
||||
(let [query-params (:query-params request)
|
||||
valid-clients (extract-client-ids #_(:clients request)
|
||||
(map first (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn)))
|
||||
(:client-id query-params)
|
||||
@@ -1842,8 +1843,8 @@
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/page (helper/page-route grid-page)
|
||||
::route/table (helper/table-route grid-page)
|
||||
{::route/page (helper/page-route grid-page :parse-query-params? false)
|
||||
::route/table (helper/table-route grid-page :parse-query-params? false)
|
||||
::route/new-location (add-new-primitive-handler [:step-params :client/locations]
|
||||
""
|
||||
location-row)
|
||||
@@ -1904,7 +1905,10 @@
|
||||
(mm/wrap-wizard client-wizard))}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-admin)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
|
||||
@@ -166,8 +166,9 @@
|
||||
(some-> route-params (get :entity-id) Long/parseLong))]
|
||||
(base-page request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:request request
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns auto-ap.ssr.auth
|
||||
(:require [buddy.sign.jwt :as jwt]
|
||||
(:require [auto-ap.session-version :as session-version]
|
||||
[buddy.sign.jwt :as jwt]
|
||||
[config.core :refer [env]]))
|
||||
|
||||
(defn logout [request]
|
||||
@@ -13,4 +14,5 @@
|
||||
:session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"])
|
||||
(:jwt-secret env)
|
||||
{:alg :hs512})
|
||||
:exp)}})
|
||||
:exp)
|
||||
:version session-version/current-session-version}})
|
||||
|
||||
@@ -131,8 +131,9 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/company-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:company)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
(ns auto-ap.ssr.company-dropdown
|
||||
(:require [auto-ap.datomic :refer [conn pull-attr pull-many]]
|
||||
(:require [auto-ap.datomic :refer [conn pull-many]]
|
||||
[auto-ap.graphql.utils :refer [cleanse-query]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
@@ -10,7 +11,9 @@
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as dc]
|
||||
[hiccup2.core :as hiccup]
|
||||
[iol-ion.query :refer [can-see-client?]]))
|
||||
[iol-ion.query :refer [can-see-client?]]
|
||||
[clojure.data.json :as json]))
|
||||
|
||||
|
||||
(defn dropdown-search-results* [{:keys [options]}]
|
||||
[:ul
|
||||
@@ -25,6 +28,7 @@
|
||||
:request-method :put)
|
||||
:hx-target "#company-dropdown"
|
||||
:hx-headers (hx/json {"x-clients" (pr-str [:group group])})
|
||||
"@click" (format "globalClientSelection={group: %s}" (hx/json group))
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "click"}
|
||||
name]
|
||||
@@ -34,6 +38,7 @@
|
||||
:request-method :put)
|
||||
:hx-target "#company-dropdown"
|
||||
:hx-headers (format "{\"x-clients\": \"[%d]\"}" id)
|
||||
"@click" (format "globalClientSelection={selected: [%d]}" id)
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "click"}
|
||||
name])]])])
|
||||
@@ -64,11 +69,18 @@
|
||||
(dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text"))})))
|
||||
|
||||
(defn dropdown [{:keys [client-selection client identity clients]}]
|
||||
(alog/peek ::clients clients)
|
||||
[:div#company-dropdown
|
||||
[:script
|
||||
(hiccup/raw
|
||||
"localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n"
|
||||
"localStorage.setItem(\"last-selected-clients\", " (pr-str (pr-str client-selection)) ")")]
|
||||
"localStorage.setItem(\"last-selected-clients\", " (json/write-str (json/write-str client-selection))
|
||||
#_(cond (:group client-selection)
|
||||
(:group client-selection)
|
||||
(:selected client-selection)
|
||||
(:selected client-selection)
|
||||
:else
|
||||
client-selection) ")")]
|
||||
[:div
|
||||
[:button#company-dropdown-button {:class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
|
||||
:type "button"}
|
||||
@@ -80,7 +92,7 @@
|
||||
|
||||
(and client
|
||||
(= 1 (count clients)))
|
||||
( :client/name client)
|
||||
(:client/name client)
|
||||
|
||||
:else
|
||||
(str (count clients) " Companies"))
|
||||
@@ -116,6 +128,8 @@
|
||||
:active-client
|
||||
:request-method :put)
|
||||
:hx-target "#company-dropdown"
|
||||
|
||||
"@click" "globalClientSelection=\"mine\""
|
||||
:hx-headers "{\"x-clients\": \":mine\"}"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "click"}
|
||||
@@ -127,6 +141,7 @@
|
||||
:active-client
|
||||
:request-method :put)
|
||||
:hx-target "#company-dropdown"
|
||||
"@click" "globalClientSelection=\"all\""
|
||||
:hx-headers "{\"x-clients\": \":all\"}"
|
||||
:hx-swap "outerHTML"
|
||||
:hx-trigger "click"}
|
||||
@@ -161,7 +176,7 @@ function initCompanyDropdown() {
|
||||
(defn active-client [{:keys [identity params] :as request}]
|
||||
(assoc
|
||||
(html-response
|
||||
(dropdown {:client-selection (:client-selection (:session request))
|
||||
(dropdown {:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity identity}))
|
||||
|
||||
@@ -111,26 +111,26 @@
|
||||
:active? (= "invoices" selected)}
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/all-page)
|
||||
{:date-range "month"})
|
||||
{:date-range "year"})
|
||||
:active? (= ::invoice-route/all-page (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
|
||||
"All")
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/paid-page)
|
||||
{:date-range "month"})
|
||||
{:date-range "year"})
|
||||
:active? (= ::invoice-route/paid-page (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
"Paid")
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/unpaid-page)
|
||||
{:date-range "month"})
|
||||
{:date-range "year"})
|
||||
:active? (= ::invoice-route/unpaid-page (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
"Unpaid")
|
||||
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
|
||||
::invoice-route/voided-page)
|
||||
{:date-range "month"})
|
||||
{:date-range "year"})
|
||||
:active? (= ::invoice-route/voided-page (:matched-route request))
|
||||
:hx-boost "true"}
|
||||
"Voided")
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
(ns auto-ap.ssr.components.date-range
|
||||
(:require [auto-ap.ssr.components :as com]
|
||||
[auto-ap.time :as atime]))
|
||||
[auto-ap.time :as atime]
|
||||
[clj-time.coerce :as c]
|
||||
[clj-time.core :as t]
|
||||
[clj-time.periodic :as per]))
|
||||
|
||||
(defn date-range-field [{:keys [value id] }]
|
||||
(defn date-range-field [{:keys [value id]}]
|
||||
[:div {:id id}
|
||||
(com/field {:label "Date Range"}
|
||||
[:div.space-y-4
|
||||
[:div.space-y-4
|
||||
[:div
|
||||
(com/button-group {:name "date-range"}
|
||||
(com/button-group-button {:size :small :value "all" :hx-trigger "click"} "All")
|
||||
(com/button-group-button {:size :small :value "week" :hx-trigger "click"} "Week")
|
||||
(com/button-group-button {:size :small :value "month" :hx-trigger "click"} "Month")
|
||||
(com/button-group-button {:size :small :value "year" :hx-trigger "click"} "Year"))
|
||||
]
|
||||
(com/button-group-button {:size :small :value "year" :hx-trigger "click"} "Year"))]
|
||||
[:div.flex.space-x-1.items-baseline.w-full.justify-start
|
||||
(com/date-input {:name "start-date"
|
||||
:value (some-> (:start value)
|
||||
(atime/unparse-local atime/normal-date))
|
||||
(atime/unparse-local atime/normal-date))
|
||||
:placeholder "Date"
|
||||
:size :small
|
||||
:class "shrink"})
|
||||
|
||||
|
||||
(com/date-input {:name "end-date"
|
||||
:value (some-> (:end value)
|
||||
(atime/unparse-local atime/normal-date))
|
||||
|
||||
@@ -14,12 +14,12 @@
|
||||
|
||||
(com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"}
|
||||
svg/paperclip
|
||||
(com/badge {} (count links)))
|
||||
(com/badge {:color "blue"} (count links)))
|
||||
[:div.divide-y.divide-gray-200.bg-white.rounded-lg.shadow.z-50 (hx/alpine-appear {:x-ref "tooltip" :x-show "show" :data-key "show"})
|
||||
[:div {:class "p-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}
|
||||
[:div.flex.flex-col.gap-y-2
|
||||
(for [l links]
|
||||
[:div.flex-initial
|
||||
[:a {:href (:link l)}
|
||||
[:a {:href (:link l) :target "_blank"}
|
||||
(com/pill {:color (or (:color l) :primary) :class "truncate block shrink grow-0"}
|
||||
(:content l))]])]]]]))
|
||||
@@ -114,7 +114,7 @@
|
||||
:class "dark:text-blue-500"}
|
||||
"Back"])
|
||||
|
||||
(defn default-next-button [linear-wizard step validation-route]
|
||||
(defn default-next-button [linear-wizard step validation-route & {:keys [next-button-content]}]
|
||||
(let [steps (steps linear-wizard)
|
||||
last? (= (step-key step) (last steps))
|
||||
next-step (when-not last? (->> steps
|
||||
@@ -131,9 +131,10 @@
|
||||
{:from (encode-step-key (step-key step))
|
||||
:to (encode-step-key (step-key next-step))})))
|
||||
|
||||
(if next-step
|
||||
(step-name next-step)
|
||||
"Save")
|
||||
(or next-button-content
|
||||
(if next-step
|
||||
(step-name next-step)
|
||||
"Save"))
|
||||
(when-not last?
|
||||
[:div.w-5.h-5 svg/arrow-right]))))
|
||||
|
||||
@@ -143,7 +144,8 @@
|
||||
|
||||
(defn default-step-footer [linear-wizard step & {:keys [validation-route
|
||||
discard-button
|
||||
next-button]}]
|
||||
next-button
|
||||
next-button-content]}]
|
||||
[:div.flex.justify-end
|
||||
[:div.flex.items-baseline.gap-x-4
|
||||
(com/form-errors {:errors (:errors (:step-params fc/*form-errors*))})
|
||||
@@ -157,7 +159,8 @@
|
||||
next-button
|
||||
|
||||
validation-route
|
||||
(default-next-button linear-wizard step validation-route)
|
||||
(default-next-button linear-wizard step validation-route
|
||||
:next-button-content next-button-content)
|
||||
|
||||
:else
|
||||
[:div "No action possible."])]])
|
||||
|
||||
@@ -21,4 +21,9 @@
|
||||
children))
|
||||
|
||||
(defn badge- [params & children]
|
||||
[:div {:class (hh/add-class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-black text-white bg-red-300 border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900" (:class params))} children])
|
||||
[:div {:class (-> (hh/add-class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-black text-white
|
||||
border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900"
|
||||
(:class params)
|
||||
)
|
||||
(hh/add-class (or (some-> (:color params) (#(str "bg-" % "-300")))
|
||||
"bg-red-300")))} children])
|
||||
|
||||
@@ -27,7 +27,8 @@
|
||||
[:li
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :company), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "My Company"]]
|
||||
(when (= "admin" (:user/role identity))
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"])
|
||||
[:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page),
|
||||
:class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"])
|
||||
[:li
|
||||
[:a {:href "#", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"
|
||||
"_" (hiccup/raw "on click toggle .dark on <body />")}
|
||||
|
||||
@@ -21,6 +21,8 @@
|
||||
[malli.transform :as mt]
|
||||
[taoensso.encore :refer [filter-vals]]))
|
||||
|
||||
|
||||
|
||||
(defn row* [gridspec user entity {:keys [flash? delete-after-settle? request class] :as options}]
|
||||
(let [cells (if (:check-boxes? gridspec)
|
||||
[(com/data-grid-cell {} (com/checkbox {:name "id" :value ((:id-fn gridspec) entity)
|
||||
@@ -75,10 +77,15 @@
|
||||
"default sort"))
|
||||
|
||||
(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}]
|
||||
(alog/info ::TABLE-QP
|
||||
:qp (:query-params request)
|
||||
:pqp (:parsed-query-params request)
|
||||
:sort sort)
|
||||
(let [start (or start 0)
|
||||
per-page (or per-page 25)
|
||||
[entities total] ((:fetch-page grid-spec)
|
||||
request)]
|
||||
[entities total :as page-results] ((:fetch-page grid-spec)
|
||||
request)
|
||||
request (assoc request :page-results page-results)]
|
||||
|
||||
(com/data-grid-card {:id (:id grid-spec)
|
||||
:title (if (string? (:title grid-spec))
|
||||
@@ -206,8 +213,9 @@
|
||||
set)]
|
||||
(handler (assoc request :trimmed-clients valid-clients)))))
|
||||
|
||||
(defn table-route [grid-spec]
|
||||
(-> (fn table [{:keys [identity] :as request}]
|
||||
(defn table-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
|
||||
(cond-> (fn table [{:keys [identity] :as request}]
|
||||
|
||||
(let [unparse-query-params (or (:unparse-query grid-spec)
|
||||
default-unparse-query-params)]
|
||||
(html-response (table*
|
||||
@@ -224,49 +232,53 @@
|
||||
main-transformer))
|
||||
"sort" sort->query)))
|
||||
(update (filter-vals #(not (nil? %))
|
||||
(m/encode (:query-schema grid-spec)
|
||||
(:query-params request)
|
||||
main-transformer))
|
||||
"sort" sort->query))
|
||||
(m/encode (:query-schema grid-spec)
|
||||
(:query-params request)
|
||||
main-transformer))
|
||||
"sort" sort->query))
|
||||
(unparse-query-params (:parsed-query-params request)))
|
||||
"selected" "all-selected")))} ;; TODO seems hacky to special case selected and all-selected here
|
||||
:oob (when-let [oob-render (:oob-render grid-spec)]
|
||||
(oob-render request)))))
|
||||
(wrap-trim-client-ids)
|
||||
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
|
||||
true (wrap-trim-client-ids)
|
||||
parse-query-params? (query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
|
||||
(default-parse-query-params grid-spec)))
|
||||
(wrap-secure)
|
||||
(wrap-client-redirect-unauthenticated)))
|
||||
true (wrap-secure)
|
||||
true (wrap-client-redirect-unauthenticated)))
|
||||
|
||||
(defn page-route [grid-spec]
|
||||
(-> (fn page [{:keys [identity] :as request}]
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav (:nav grid-spec)
|
||||
:page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)]
|
||||
[:div#page-specific-nav (page-specific-nav request)])
|
||||
:client-selection (:client-selection (:session request))
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
|
||||
[:div {:x-data (hx/json {:selected [] :all_selected false})
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
:x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) );
|
||||
(defn page-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
|
||||
|
||||
(cond-> (fn page [{:keys [identity] :as request}]
|
||||
(alog/info ::page-route
|
||||
:pqp (:parsed-query-params request)
|
||||
:qp (:query-params request))
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav (:nav grid-spec)
|
||||
:page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)]
|
||||
[:div#page-specific-nav (page-specific-nav request)])
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
:request request}
|
||||
(apply com/breadcrumbs {} (:breadcrumbs grid-spec))
|
||||
[:div {:x-data (hx/json {:selected [] :all_selected false})
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
:x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) );
|
||||
$watch('all_selected', a=>$dispatch('selectedChanged', {selected: selected, all_selected: a}))"}
|
||||
|
||||
(table* grid-spec
|
||||
identity
|
||||
request)])
|
||||
(if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request))))
|
||||
(wrap-trim-client-ids)
|
||||
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
|
||||
(default-parse-query-params grid-spec)))
|
||||
(wrap-secure)
|
||||
(wrap-client-redirect-unauthenticated)))
|
||||
|
||||
(table* grid-spec
|
||||
identity
|
||||
request)])
|
||||
(if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request))))
|
||||
true (wrap-trim-client-ids)
|
||||
parse-query-params? (query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
|
||||
(default-parse-query-params grid-spec)))
|
||||
true (wrap-secure)
|
||||
true (wrap-client-redirect-unauthenticated)))
|
||||
|
||||
(def request-spec (m/schema [:map]))
|
||||
(def entity-spec (m/schema [:map]))
|
||||
@@ -280,8 +292,8 @@
|
||||
(def grid-spec (m/schema [:map
|
||||
[:id :string]
|
||||
[:nav [:=>
|
||||
[:cat request-spec]
|
||||
vector?]]
|
||||
[:cat request-spec]
|
||||
vector?]]
|
||||
[:page-specific-nav
|
||||
{:optional true
|
||||
:default (fn [request])}
|
||||
@@ -338,8 +350,8 @@
|
||||
(handler (update request :query-params
|
||||
(fn [qp]
|
||||
((comp
|
||||
(query-params/apply-remove-sort)
|
||||
(query-params/apply-toggle-sort grid-spec)
|
||||
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %)))
|
||||
(query-params/apply-remove-sort)
|
||||
(query-params/apply-toggle-sort grid-spec)
|
||||
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %)))
|
||||
qp))))))
|
||||
|
||||
|
||||
@@ -421,8 +421,9 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/admin-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:invoice-glimpse)
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated]]
|
||||
[auto-ap.rule-matching :as rm]
|
||||
[auto-ap.client-routes :as client-routes]
|
||||
[auto-ap.solr :as solr]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
||||
@@ -229,6 +230,14 @@
|
||||
:value (fc/field-value)
|
||||
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))
|
||||
:x-model "vendorId"})]))
|
||||
[:div.mb-4
|
||||
[:span.text-sm.text-gray-500 "Can't find the vendor? "
|
||||
(com/link {:href (bidi.bidi/path-for
|
||||
client-routes/routes
|
||||
:new-vendor)
|
||||
:target "new"}
|
||||
"Add new vendor")
|
||||
" in a new window, then return here."]]
|
||||
|
||||
|
||||
[:div.flex.items-center.gap-2
|
||||
@@ -421,17 +430,36 @@
|
||||
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||
|
||||
(defn invoice-expense-account-total* [request]
|
||||
(format "$%,.2f" (->> (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:invoice/expense-accounts)
|
||||
(map (fnil :invoice-expense-account/amount 0.0))
|
||||
(filter number?)
|
||||
(reduce + 0.0))))
|
||||
(let [total (->> (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:invoice/expense-accounts)
|
||||
(map (fnil :invoice-expense-account/amount 0.0))
|
||||
(filter number?)
|
||||
(reduce + 0.0))]
|
||||
(format "$%,.2f" total)))
|
||||
|
||||
(defn invoice-expense-account-balance* [request]
|
||||
(let [total (->> (-> request
|
||||
:multi-form-state
|
||||
:step-params
|
||||
:invoice/expense-accounts)
|
||||
(map (fnil :invoice-expense-account/amount 0.0))
|
||||
(filter number?)
|
||||
(reduce + 0.0))
|
||||
balance (-
|
||||
(-> request :multi-form-state :snapshot :invoice/total)
|
||||
total)]
|
||||
[:span {:class (when-not (dollars= 0.0 balance)
|
||||
"text-red-300")}
|
||||
(format "$%,.2f" balance)]))
|
||||
|
||||
(defn invoice-expense-account-total [request]
|
||||
(html-response (invoice-expense-account-total* request)))
|
||||
|
||||
(defn invoice-expense-account-balance [request]
|
||||
(html-response (invoice-expense-account-balance* request)))
|
||||
|
||||
(defrecord AccountsStep [linear-wizard]
|
||||
mm/ModalWizardStep
|
||||
(step-name [_]
|
||||
@@ -481,6 +509,19 @@
|
||||
:hx-swap "innerHTML"}
|
||||
(invoice-expense-account-total* request))
|
||||
(com/data-grid-cell {}))
|
||||
|
||||
(com/data-grid-row {}
|
||||
(com/data-grid-cell {})
|
||||
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
|
||||
(com/data-grid-cell {:id "total"
|
||||
:class "text-right"
|
||||
:hx-trigger "change from:closest form target:.amount-field"
|
||||
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-balance)
|
||||
:hx-target "this"
|
||||
:hx-swap "innerHTML"}
|
||||
(invoice-expense-account-balance* request))
|
||||
(com/data-grid-cell {}))
|
||||
|
||||
(com/data-grid-row {}
|
||||
|
||||
(com/data-grid-cell {})
|
||||
@@ -592,12 +633,12 @@
|
||||
(when (seq eas)
|
||||
(let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas)))
|
||||
leftover-beyond-a-single-cent? (or (< leftover -1)
|
||||
(> leftover 1))
|
||||
(> leftover 1))
|
||||
leftover (if leftover-beyond-a-single-cent?
|
||||
0
|
||||
leftover)
|
||||
[first-eas & rest] eas]
|
||||
(cons
|
||||
(cons
|
||||
(update first-eas :invoice-expense-account/amount #(+ % leftover))
|
||||
rest))))
|
||||
|
||||
@@ -813,6 +854,9 @@
|
||||
::route/expense-account-total (-> invoice-expense-account-total
|
||||
(mm/wrap-wizard new-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/expense-account-balance (-> invoice-expense-account-balance
|
||||
(mm/wrap-wizard new-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
::route/location-select (-> location-select
|
||||
(wrap-schema-enforce :query-schema [:map
|
||||
[:name :string]
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[auto-ap.datomic.accounts :as d-accounts]
|
||||
[auto-ap.datomic.bank-accounts :as d-bank-accounts]
|
||||
[auto-ap.datomic.invoices :as d-invoices]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.graphql.checks :as gq-checks :refer [base-payment
|
||||
invoice-payments
|
||||
print-checks-internal
|
||||
@@ -120,8 +121,6 @@
|
||||
(exact-match-id* request)]])
|
||||
|
||||
|
||||
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client-id request)
|
||||
@@ -238,6 +237,7 @@
|
||||
query-params)
|
||||
true
|
||||
(merge-query {:query {:find ['?sort-default '?e]}})))]
|
||||
|
||||
(->> (observable-query query)
|
||||
(apply-sort-3 (assoc query-params :default-asc? false))
|
||||
(apply-pagination query-params))))
|
||||
@@ -251,12 +251,42 @@
|
||||
(map first))]
|
||||
refunds))
|
||||
|
||||
(defn sum-outstanding [ids]
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/outstanding-balance ?o]]}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn sum-total-amount [ids]
|
||||
|
||||
(->>
|
||||
(dc/q {:find ['?id '?o]
|
||||
:in ['$ '[?id ...]]
|
||||
:where ['[?id :invoice/total ?o]]
|
||||
}
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map last)
|
||||
(reduce
|
||||
+
|
||||
0.0)))
|
||||
|
||||
(defn fetch-page [request]
|
||||
(let [db (dc/db conn)
|
||||
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
|
||||
{ids-to-retrieve :ids matching-count :count
|
||||
all-ids :all-ids} (fetch-ids db request)]
|
||||
|
||||
[(->> (hydrate-results ids-to-retrieve db request))
|
||||
matching-count]))
|
||||
matching-count
|
||||
(sum-outstanding all-ids)
|
||||
(sum-total-amount all-ids)]))
|
||||
|
||||
(def query-schema (mc/schema
|
||||
[:maybe [:map {:date-range [:date-range :start-date :end-date]}
|
||||
@@ -378,7 +408,6 @@
|
||||
(:query-params request))})))
|
||||
|
||||
|
||||
|
||||
;; TODO test as a real user
|
||||
(def grid-page
|
||||
(helper/build {:id "entity-table"
|
||||
@@ -392,20 +421,27 @@
|
||||
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
|
||||
:query-schema query-schema
|
||||
:parse-query-params (fn [p]
|
||||
(mc/decode query-schema p main-transformer))
|
||||
(alog/peek ::PARSE
|
||||
(mc/decode query-schema p main-transformer)))
|
||||
:action-buttons (fn [request]
|
||||
[(when (can? (:identity request) {:subject :invoice :activity :bulk-delete})
|
||||
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#invoice-filters"
|
||||
:color :red}
|
||||
"Void selected"))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :pay})
|
||||
(pay-button* {:ids (selected->ids request
|
||||
(:query-params request))}))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :create})
|
||||
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)}
|
||||
"New invoice"))])
|
||||
(let [[_ _ outstanding total] (:page-results request)]
|
||||
[(com/pill {:color :primary} "Outstanding: "
|
||||
(format "$%,.2f" outstanding))
|
||||
(com/pill {:color :secondary} "Total: "
|
||||
(format "$%,.2f" total))
|
||||
|
||||
(when (can? (:identity request) {:subject :invoice :activity :bulk-delete})
|
||||
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
|
||||
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
|
||||
"hx-include" "#invoice-filters"
|
||||
:color :red}
|
||||
"Void selected"))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :pay})
|
||||
(pay-button* {:ids (selected->ids request
|
||||
(:query-params request))}))
|
||||
(when (can? (:identity request) {:subject :invoice :activity :create})
|
||||
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)}
|
||||
"New invoice"))]))
|
||||
:row-buttons (fn [request entity]
|
||||
[(when (and (= :invoice-status/unpaid (:invoice/status entity))
|
||||
(can? (:identity request) {:subject :invoice :activity :delete}))
|
||||
@@ -556,10 +592,10 @@
|
||||
_ (audit-transact tx identity)]
|
||||
(alog/info ::unvoiding-invoice :transaction :tx)
|
||||
(html-response
|
||||
(row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
|
||||
:request request})
|
||||
:headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id)
|
||||
"hx-reswap" "outerHTML"}))))
|
||||
(row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
|
||||
:request request})
|
||||
:headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id)
|
||||
"hx-reswap" "outerHTML"}))))
|
||||
|
||||
(defn delete [{invoice :entity :as request identity :identity}]
|
||||
(exception->notification
|
||||
@@ -716,7 +752,6 @@
|
||||
(map :invoice-id invoices))
|
||||
(into {}))]
|
||||
(every? (fn [%]
|
||||
(println "TEST" (:amount %) (outstanding-balances (:invoice-id %)))
|
||||
(not (does-amount-exceed-outstanding? (:amount %) (outstanding-balances (:invoice-id %)))))
|
||||
invoices)))]]]
|
||||
[:has-warning? :boolean]
|
||||
@@ -968,7 +1003,8 @@
|
||||
:name (fc/field-name)
|
||||
:error? (fc/error?)}))))))))))]])
|
||||
:footer
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/pay-wizard-navigate)
|
||||
(mm/default-step-footer linear-wizard this :validation-route ::route/pay-wizard-navigate
|
||||
:next-button-content "Pay")
|
||||
:validation-route ::route/pay-wizard-navigate)))
|
||||
|
||||
(defn add-handwritten-check [request wizard snapshot]
|
||||
@@ -1137,7 +1173,7 @@
|
||||
(->> (dc/q '[:find ?i
|
||||
:in $ [?i ...]
|
||||
:where [?i :invoice/status :invoice-status/unpaid]
|
||||
[?i :invoice/client ?c] ]
|
||||
[?i :invoice/client ?c]]
|
||||
(dc/db conn)
|
||||
ids)
|
||||
(map first)))
|
||||
@@ -1177,13 +1213,13 @@
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
(->
|
||||
{::route/all-page (-> (helper/page-route grid-page)
|
||||
{::route/all-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status nil))
|
||||
::route/paid-page (-> (helper/page-route grid-page)
|
||||
::route/paid-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status :invoice-status/paid))
|
||||
::route/unpaid-page (-> (helper/page-route grid-page)
|
||||
::route/unpaid-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status :invoice-status/unpaid))
|
||||
::route/voided-page (-> (helper/page-route grid-page)
|
||||
::route/voided-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status :invoice-status/voided))
|
||||
::route/unvoid (-> unvoid-invoice
|
||||
(wrap-entity [:route-params :db/id] default-read)
|
||||
@@ -1212,12 +1248,14 @@
|
||||
(mm/wrap-wizard pay-wizard)
|
||||
(mm/wrap-decode-multi-form-state))
|
||||
|
||||
::route/table (helper/table-route grid-page)}
|
||||
::route/table (helper/table-route grid-page :parse-query-params? false)}
|
||||
(merge new-invoice-wizard/key->handler))
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-status-from-source)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-schema-enforce :query-schema query-schema)
|
||||
(wrap-schema-enforce :hx-schema query-schema)
|
||||
(wrap-client-redirect-unauthenticated)))))
|
||||
@@ -251,7 +251,7 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:clients (:clients request)
|
||||
:client (:client request)
|
||||
:identity (:identity request)
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
[auto-ap.graphql.utils :refer [assert-can-see-client
|
||||
exception->notification
|
||||
extract-client-ids notify-if-locked]]
|
||||
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.permissions :refer [can?]]
|
||||
[auto-ap.routes.invoice :as invoice-route]
|
||||
@@ -38,8 +39,8 @@
|
||||
[malli.transform :as mt]))
|
||||
|
||||
(defn exact-match-id* [request]
|
||||
(if (nat-int? (:exact-match-id (:parsed-query-params request)))
|
||||
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:parsed-query-params request))}) :id "exact-match-id-tag"}
|
||||
(if (nat-int? (:exact-match-id (:query-params request)))
|
||||
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
|
||||
(com/hidden {:name "exact-match-id"
|
||||
"x-model" "exact_match"})
|
||||
(com/pill {:color :primary}
|
||||
@@ -68,7 +69,7 @@
|
||||
:value (:vendor (:query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn :vendor/name}))
|
||||
(date-range-field* request)
|
||||
(date-range-field* (assoc request :parsed-query-params (:query-params request)))
|
||||
(com/field {:label "Check #"}
|
||||
(com/text-input {:name "check-number"
|
||||
:id "check-number"
|
||||
@@ -130,7 +131,7 @@
|
||||
{:transaction/_payment [:db/id :transaction/date]}])
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||
(let [ valid-clients (extract-client-ids (:clients request)
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
(:client request)
|
||||
(:client-id query-params)
|
||||
(when (:client-code query-params)
|
||||
@@ -419,7 +420,8 @@
|
||||
(audit-transact (conj removing-payments updated-payment)
|
||||
identity)
|
||||
|
||||
(html-response (row* (:identity request) updated-payment {:delete-after-settle? true :class "live-removed"})
|
||||
(html-response (row* (:identity request) updated-payment {:delete-after-settle? true :class "live-removed"
|
||||
:request request})
|
||||
:headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id check))})))
|
||||
|
||||
;; TODO use decoding here
|
||||
@@ -529,13 +531,13 @@
|
||||
|
||||
(def key->handler
|
||||
(apply-middleware-to-all-handlers
|
||||
{::route/cleared-page (-> (helper/page-route grid-page)
|
||||
{::route/cleared-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status :payment-status/cleared))
|
||||
::route/pending-page (-> (helper/page-route grid-page)
|
||||
::route/pending-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status :payment-status/pending))
|
||||
::route/voided-page (-> (helper/page-route grid-page)
|
||||
::route/voided-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status :payment-status/voided))
|
||||
::route/all-page (-> (helper/page-route grid-page)
|
||||
::route/all-page (-> (helper/page-route grid-page :parse-query-params? false)
|
||||
(wrap-implied-route-param :status nil))
|
||||
|
||||
::route/delete (-> delete
|
||||
@@ -548,9 +550,10 @@
|
||||
(wrap-admin))
|
||||
|
||||
|
||||
::route/table (helper/table-route grid-page)}
|
||||
::route/table (helper/table-route grid-page :parse-query-params? false)}
|
||||
(fn [h]
|
||||
(-> h
|
||||
(wrap-copy-qp-pqp)
|
||||
(wrap-apply-sort grid-page)
|
||||
(wrap-merge-prior-hx)
|
||||
(wrap-status-from-source)
|
||||
|
||||
@@ -318,8 +318,9 @@
|
||||
(base-page
|
||||
request
|
||||
(com/page {:nav com/main-aside-nav
|
||||
:client-selection (:client-selection (:session request))
|
||||
:client-selection (:client-selection request)
|
||||
:client (:client request)
|
||||
:clients (:clients request)
|
||||
:identity (:identity request)
|
||||
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:transaction-insights)
|
||||
|
||||
@@ -69,9 +69,10 @@ input::-webkit-inner-spin-button {
|
||||
input[type=number] {
|
||||
-moz-appearance:textfield; /* Firefox */
|
||||
} "]
|
||||
|
||||
|
||||
[:body {:hx-ext "disable-submit, class-tools"}
|
||||
[:body {:hx-ext "disable-submit, class-tools"
|
||||
:x-data (hx/json {:globalClientSelection (or (:client-selection request)
|
||||
:all )}) ;; TODO remove once session is used
|
||||
:x-hx-header:x-clients "JSON.stringify(globalClientSelection)"}
|
||||
contents
|
||||
[:script {:src "/js/flowbite.min.js"}]
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
"hx-target" "#user-table"
|
||||
"hx-indicator" "#user-table"}
|
||||
|
||||
[:fieldset.space-y-6
|
||||
[:fieldset.space-y-6
|
||||
(com/field {:label "Name"}
|
||||
(com/text-input {:name "name"
|
||||
:id "name"
|
||||
@@ -57,6 +57,16 @@
|
||||
:placeholder "Johnny Testerson"
|
||||
:size :small}))
|
||||
|
||||
(com/field {:label "Client"}
|
||||
(com/typeahead {:name "client"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)
|
||||
:id (str "client-search")
|
||||
:value (:client (:parsed-query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn :client/name}))
|
||||
|
||||
(com/field {:label "Email"}
|
||||
(com/text-input {:name "email"
|
||||
:id "email"
|
||||
@@ -66,31 +76,22 @@
|
||||
:size :small}))
|
||||
|
||||
(com/field {:label "Role"}
|
||||
(com/radio-card {:size :small
|
||||
:name "role"
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "admin"
|
||||
:content "Admin"}
|
||||
{:value "power-user"
|
||||
:content "Power user"}
|
||||
{:value "manager"
|
||||
:content "Manager"}
|
||||
{:value "user"
|
||||
:content "User"}
|
||||
{:value "read-only"
|
||||
:content "Read Only"}
|
||||
{:value "none"
|
||||
:content "None"}]}))
|
||||
(com/field {:label "Client"}
|
||||
(com/typeahead {:name "client"
|
||||
:placeholder "Search..."
|
||||
:url (bidi/path-for ssr-routes/only-routes
|
||||
:company-search)
|
||||
:id (str "client-search")
|
||||
:value (:client (:parsed-query-params request))
|
||||
:value-fn :db/id
|
||||
:content-fn :client/name}))]])
|
||||
(com/radio-card {:size :small
|
||||
:name "role"
|
||||
:options [{:value ""
|
||||
:content "All"}
|
||||
{:value "admin"
|
||||
:content "Admin"}
|
||||
{:value "power-user"
|
||||
:content "Power user"}
|
||||
{:value "manager"
|
||||
:content "Manager"}
|
||||
{:value "user"
|
||||
:content "User"}
|
||||
{:value "read-only"
|
||||
:content "Read Only"}
|
||||
{:value "none"
|
||||
:content "None"}]}))]])
|
||||
|
||||
(def default-read '[:db/id
|
||||
:user/name
|
||||
|
||||
@@ -107,7 +107,7 @@
|
||||
(mt2/transformer
|
||||
{:decoders
|
||||
{:map (fn [m]
|
||||
(if (not (seq (filter identity (vals m))))
|
||||
(if (and (map? m) (not (seq (filter identity (vals m)))))
|
||||
nil
|
||||
m))
|
||||
:string empty->nil
|
||||
@@ -240,9 +240,10 @@
|
||||
(if date-range-value
|
||||
(-> (condp = date-range-value
|
||||
"week"
|
||||
(assoc m
|
||||
start-date-key (time/plus (time/now) (time/days -7))
|
||||
end-date-key (time/now))
|
||||
(let [last-monday (atime/last-monday)]
|
||||
(assoc m
|
||||
start-date-key (time/plus last-monday (time/days -7))
|
||||
end-date-key last-monday))
|
||||
|
||||
"month"
|
||||
(assoc m
|
||||
@@ -255,7 +256,7 @@
|
||||
end-date-key (time/now))
|
||||
|
||||
"all"
|
||||
(assoc m start-date-key (time/plus (time/now) (time/years -3))
|
||||
(assoc m start-date-key (time/plus (time/now) (time/years -6))
|
||||
end-date-key (time/now))
|
||||
|
||||
m)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
(ns auto-ap.time
|
||||
(:require [clj-time.core :as time]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.format :as f]
|
||||
[auto-ap.logging :as alog]))
|
||||
|
||||
@@ -40,7 +41,7 @@
|
||||
|
||||
(defn unparse-local [v format]
|
||||
(try
|
||||
|
||||
|
||||
(f/unparse (f/with-zone (f/formatter format) (time/time-zone-for-id "America/Los_Angeles")) v)
|
||||
(catch Exception _
|
||||
nil)))
|
||||
@@ -52,3 +53,14 @@
|
||||
d
|
||||
(recur (time/plus d (time/days 1)))))]
|
||||
(iterate #(time/plus % (time/days 7)) next-day)))
|
||||
|
||||
(defn local-today []
|
||||
(coerce/in-time-zone (time/now) (time/time-zone-for-id "America/Los_Angeles")))
|
||||
|
||||
|
||||
(defn last-monday []
|
||||
(loop [current (local-now)]
|
||||
(if (= 1 (time/day-of-week current))
|
||||
current
|
||||
(recur (time/minus current (time/days 1))))))
|
||||
|
||||
@@ -6,7 +6,8 @@
|
||||
"needs-activation/" :needs-activation
|
||||
"needs-activation" :needs-activation
|
||||
"payments/" :payments
|
||||
"admin/" { "vendors" :admin-vendors}
|
||||
"admin/" {"vendors" :admin-vendors}
|
||||
"vendor/" {"new" :new-vendor}
|
||||
"invoices/" {"" :invoices
|
||||
"import" :import-invoices
|
||||
"unpaid" :unpaid-invoices
|
||||
|
||||
@@ -12,7 +12,8 @@
|
||||
"/account/new" ::new-wizard-new-account
|
||||
"/account/location-select" ::location-select
|
||||
"/account/prediction" ::account-prediction
|
||||
"/total" ::expense-account-total}
|
||||
"/total" ::expense-account-total
|
||||
"/balance" ::expense-account-balance}
|
||||
|
||||
"/pay-button" ::pay-button
|
||||
"/pay" {:get ::pay-wizard
|
||||
|
||||
30
src/cljs/auto_ap/client_selection.cljs
Normal file
30
src/cljs/auto_ap/client_selection.cljs
Normal file
@@ -0,0 +1,30 @@
|
||||
(ns auto-ap.client-selection
|
||||
(:require [clojure.string :as str]
|
||||
[malli.core :as mc]
|
||||
[malli.transform :as mt2]))
|
||||
|
||||
;; TODO remove this eventuall
|
||||
(defn str->keyword [s]
|
||||
(if (string? s)
|
||||
(let [[ns k] (str/split s #"/")]
|
||||
(if (and ns k)
|
||||
(keyword ns k)
|
||||
(keyword s)))
|
||||
s))
|
||||
|
||||
(defn keyword->str [k]
|
||||
(subs (str k) 1))
|
||||
(def client-selection-schema
|
||||
(mc/schema
|
||||
[:orn
|
||||
[:global [:enum :all :mine]]
|
||||
[:group-name [:map [:group :string]]]
|
||||
[:specific [:map [:selected [:vector nat-int?]]]]]))
|
||||
|
||||
(def client-selection-transformer
|
||||
(mt2/transformer
|
||||
mt2/json-transformer
|
||||
mt2/string-transformer
|
||||
(mt2/key-transformer {:encode keyword->str :decode str->keyword})))
|
||||
|
||||
;; END TODO
|
||||
@@ -1,27 +1,26 @@
|
||||
(ns auto-ap.effects
|
||||
(:require-macros [cljs.core.async.macros :refer [go]])
|
||||
(:require
|
||||
[auto-ap.history :as p]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.utils :refer [date->str standard]]
|
||||
[cemerick.url :as url]
|
||||
[cljs-http.client :as http]
|
||||
[cljs-time.coerce :as c]
|
||||
[cljs-time.core :as time]
|
||||
[cljs-time.format :as format]
|
||||
[cljs.core.async :refer [<!] :as async]
|
||||
[clojure.string :as str]
|
||||
[clojure.walk :as walk]
|
||||
[pushy.core :as pushy]
|
||||
[re-frame.core :as re-frame]
|
||||
[venia.core :as v]))
|
||||
(:require [auto-ap.client-selection :refer [client-selection-schema]]
|
||||
[auto-ap.history :as p]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.utils :refer [date->str standard]]
|
||||
[cemerick.url :as url]
|
||||
[cljs-http.client :as http]
|
||||
[cljs-time.coerce :as c]
|
||||
[cljs-time.core :as time]
|
||||
[cljs-time.format :as format]
|
||||
[cljs.core.async :refer [<!] :as async]
|
||||
[clojure.string :as str]
|
||||
[clojure.walk :as walk]
|
||||
[malli.core :as mc]
|
||||
[pushy.core :as pushy]
|
||||
[re-frame.core :as re-frame]
|
||||
[venia.core :as v]))
|
||||
|
||||
(defn maybe-add-x-clients [headers]
|
||||
(if (or (and (sequential? (:selected-clients @re-frame.db/app-db)) (every? int? (:selected-clients @re-frame.db/app-db)))
|
||||
(and (sequential? (:selected-clients @re-frame.db/app-db)) (every? string? (:selected-clients @re-frame.db/app-db)))
|
||||
(and (sequential? (:selected-clients @re-frame.db/app-db)) (= :group (first (:selected-clients @re-frame.db/app-db))))
|
||||
(keyword? (:selected-clients @re-frame.db/app-db)))
|
||||
(assoc headers "x-clients" (pr-str (:selected-clients @re-frame.db/app-db)))
|
||||
(if (and (mc/validate client-selection-schema (:selected-clients @re-frame.db/app-db))
|
||||
(not (get headers "x-clients")))
|
||||
(assoc headers "x-clients" (.stringify js/JSON (clj->js (:selected-clients @re-frame.db/app-db))))
|
||||
headers))
|
||||
|
||||
(re-frame/reg-fx
|
||||
@@ -199,27 +198,27 @@
|
||||
"&variables=" (pr-str (or variables {})))}))]
|
||||
|
||||
(cond
|
||||
|
||||
(= (:status response) 401)
|
||||
(re-frame/dispatch [:auto-ap.events/logout "Your session has expired. Please log in again."])
|
||||
|
||||
|
||||
|
||||
(>= (:status response) 400)
|
||||
(let [error (->> response
|
||||
:body
|
||||
:errors
|
||||
(dates->date-times)
|
||||
(map #(assoc % :status (:status response)))
|
||||
)]
|
||||
(map #(assoc % :status (:status response))))]
|
||||
(when (:multi owns-state)
|
||||
(re-frame/dispatch [::status/error-multi (:multi owns-state) (:which owns-state) error]))
|
||||
(when (:single owns-state)
|
||||
(re-frame/dispatch [::status/error (:single owns-state) error]))
|
||||
(when on-error
|
||||
(->> error
|
||||
(->> error
|
||||
(conj on-error)
|
||||
(re-frame/dispatch))))
|
||||
:else
|
||||
(do
|
||||
(do
|
||||
(when (:multi owns-state)
|
||||
(re-frame/dispatch [::status/completed-multi (:multi owns-state) (:which owns-state)]))
|
||||
(when (:single owns-state)
|
||||
|
||||
@@ -1,24 +1,24 @@
|
||||
(ns auto-ap.events
|
||||
(:require
|
||||
[auto-ap.db :as db]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.utils :refer [parse-jwt with-user gunzip]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.string :as str]
|
||||
[clojure.edn :as edn]
|
||||
[goog.crypt.base64 :as b64]
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[cemerick.url :as url]
|
||||
[auto-ap.subs :as subs]
|
||||
[pako]))
|
||||
(:require [auto-ap.client-selection :refer [client-selection-schema
|
||||
client-selection-transformer]]
|
||||
[auto-ap.db :as db]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.utils :refer [gunzip parse-jwt with-user]]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clojure.string :as str]
|
||||
[goog.crypt.base64 :as b64]
|
||||
[malli.core :as mc]
|
||||
[pako]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
|
||||
|
||||
(defn jwt->data [token]
|
||||
(let [raw (js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." )))))
|
||||
(let [raw (js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\.")))))
|
||||
gz-clients (or (:gz-clients raw)
|
||||
(get raw "gz-clients"))]
|
||||
(cond-> raw
|
||||
@@ -29,7 +29,7 @@
|
||||
(defn client-query []
|
||||
(cond-> [:id :name :code :email :locations :feature-flags :groups
|
||||
[:emails [:id :email :description]]
|
||||
[:bank-accounts [:id :code :bank-name :name :type :visible
|
||||
[:bank-accounts [:id :code :bank-name :name :type :visible
|
||||
:locations :include-in-reports :current-balance
|
||||
:sort-order]]]))
|
||||
|
||||
@@ -46,18 +46,28 @@
|
||||
[:plaid-account [:name :id :number]]
|
||||
[:intuit-bank-account [:name :id :external-id]]
|
||||
:use-date-instead-of-post-date
|
||||
:locations :include-in-reports :current-balance :yodlee-balance-old] ]
|
||||
:locations :include-in-reports :current-balance :yodlee-balance-old]]
|
||||
[:address [:id :street1 :street2 :city :state :zip]]
|
||||
[:forecasted-transactions [:id :amount :identifier :day-of-month]]]
|
||||
(= "admin" (or (get (jwt->data token) "role") (get (jwt->data token) "user/role")) ) (into [[:yodlee-provider-accounts [:id [:accounts [:id :name :number :available-balance]]]]
|
||||
[:plaid-items [:id [:accounts [:id :name :number :balance]]]]])))
|
||||
(= "admin" (or (get (jwt->data token) "role") (get (jwt->data token) "user/role"))) (into [[:yodlee-provider-accounts [:id [:accounts [:id :name :number :available-balance]]]]
|
||||
[:plaid-items [:id [:accounts [:id :name :number :balance]]]]])))
|
||||
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::initialize-db
|
||||
(fn [{:keys [_]} [_ token]]
|
||||
(let [handler (:handler (bidi/match-route routes/routes (.. js/window -location -pathname)))
|
||||
last-client-id (.getItem js/localStorage "last-client-id")
|
||||
last-selected-clients (edn/read-string (.getItem js/localStorage "last-selected-clients"))
|
||||
last-selected-clients (try (some->> "last-selected-clients"
|
||||
(.getItem js/localStorage)
|
||||
not-empty
|
||||
(.parse js/JSON)
|
||||
js->clj
|
||||
( #(mc/decode client-selection-schema % client-selection-transformer)))
|
||||
(catch js/Error e
|
||||
:all))
|
||||
jwt-data (some-> token jwt->data)
|
||||
selected-client-assignment (cond (and token
|
||||
(= "admin" (get jwt-data "user/role"))
|
||||
@@ -72,7 +82,7 @@
|
||||
:else
|
||||
nil)]
|
||||
|
||||
|
||||
|
||||
(cond
|
||||
(= :login handler)
|
||||
{:db (cond-> (assoc db/default-db
|
||||
@@ -89,7 +99,7 @@
|
||||
:selected-clients last-selected-clients
|
||||
:user token)}
|
||||
|
||||
(and token (= "none" (or (get jwt-data "role") (get jwt-data "user/role")) ))
|
||||
(and token (= "none" (or (get jwt-data "role") (get jwt-data "user/role"))))
|
||||
{:redirect "/needs-activation"
|
||||
:db (assoc db/default-db
|
||||
:active-route :needs-activation
|
||||
@@ -112,7 +122,7 @@
|
||||
:on-success [::received-initial]
|
||||
:on-error [::failed-initial]}}
|
||||
selected-client-assignment
|
||||
(assoc :set-local-storage ["last-selected-clients" selected-client-assignment]))))))
|
||||
(assoc :set-local-storage ["last-selected-clients" (.stringify js/JSON selected-client-assignment)]))))))
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
@@ -125,13 +135,13 @@
|
||||
::received-initial
|
||||
(fn [{:keys [db]} [_ {clients :client}]]
|
||||
(let [only-one-client (when (= 1 (count clients))
|
||||
(->> clients first :id ))]
|
||||
(->> clients first :id))]
|
||||
(when only-one-client
|
||||
(.setItem js/localStorage "last-client-id" only-one-client)
|
||||
(.setItem js/localStorage "last-selected-clients"
|
||||
(pr-str [(js/parseInt only-one-client)])))
|
||||
{:db (cond-> (-> db
|
||||
(assoc :clients (by :id clients) )
|
||||
{:db (cond-> (-> db
|
||||
(assoc :clients (by :id clients))
|
||||
(assoc :is-initial-loading? false)
|
||||
(assoc :client (or only-one-client
|
||||
(->> clients
|
||||
@@ -167,37 +177,37 @@
|
||||
:active-route :initial-error)))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::swapped-client
|
||||
(fn [{:keys [db]} [_ client client-identifier]]
|
||||
(when (:id client)
|
||||
(.setItem js/localStorage "last-client-id" (:id client)))
|
||||
(.setItem js/localStorage "last-selected-clients"
|
||||
(condp = client-identifier
|
||||
:all
|
||||
:all
|
||||
::swapped-client
|
||||
(fn [{:keys [db]} [_ client client-identifier]]
|
||||
(when (:id client)
|
||||
(.setItem js/localStorage "last-client-id" (:id client)))
|
||||
(.setItem js/localStorage "last-selected-clients"
|
||||
(.stringify js/JSON
|
||||
(clj->js (condp = client-identifier
|
||||
:all
|
||||
:all
|
||||
|
||||
:mine
|
||||
:mine
|
||||
:mine
|
||||
:mine
|
||||
|
||||
(pr-str [(js/parseInt (:id client))])))
|
||||
{:selected [(js/parseInt (:id client))]}))))
|
||||
|
||||
{:db (assoc db :client (:id client)
|
||||
:selected-clients
|
||||
(condp = client-identifier
|
||||
:all
|
||||
:all
|
||||
{:db (assoc db :client (:id client)
|
||||
:selected-clients
|
||||
(condp = client-identifier
|
||||
:all
|
||||
:all
|
||||
|
||||
:mine
|
||||
:mine
|
||||
:mine
|
||||
:mine
|
||||
|
||||
[(js/parseInt (:id client))]))}))
|
||||
{:selected [(js/parseInt (:id client))]}))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::swap-client
|
||||
[with-user]
|
||||
(fn [{:keys [db user]} [_ client]]
|
||||
(let [client-identifier (or (:id client) client)]
|
||||
|
||||
{:http {:token user
|
||||
:method :put
|
||||
:uri (str (bidi/path-for ssr-routes/only-routes
|
||||
@@ -205,12 +215,20 @@
|
||||
:request-method :put)
|
||||
"?"
|
||||
(url/map->query {:search-client client-identifier}))
|
||||
:headers {"x-clients"
|
||||
(.stringify js/JSON
|
||||
(clj->js (cond (= :all client-identifier)
|
||||
"all"
|
||||
(= :mine client-identifier)
|
||||
"mine"
|
||||
:else
|
||||
{:selected [client-identifier]})))}
|
||||
:on-success [::swapped-client client client-identifier]}})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::set-active-route
|
||||
(fn [{:keys [db]} [_ handler params route-params]]
|
||||
(cond
|
||||
(cond
|
||||
(and (not= :login handler) (not (:user db)))
|
||||
{:redirect (bidi/path-for routes/routes :login)
|
||||
:db (assoc db :active-route :login
|
||||
@@ -256,36 +274,36 @@
|
||||
(fn [{:keys [db]} _]
|
||||
{:graphql {:token (:user db)
|
||||
:query-obj {:venia/queries [[:yodlee-merchants
|
||||
[:name :yodlee-id :id]]]}
|
||||
[:name :yodlee-id :id]]]}
|
||||
:on-success [::yodlee-merchants-received]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::vendor-preferences-requested
|
||||
[with-user]
|
||||
(fn [{:keys [user]} [_ {:keys [ client-id vendor-id on-success on-failure owns-state]}]]
|
||||
{:graphql {:token user
|
||||
:query-obj {:venia/queries [[:vendor-by-id
|
||||
{:id vendor-id}
|
||||
[[:automatically-paid-when-due [:id]]
|
||||
[:schedule-payment-dom [[:client [:id]] :dom]]
|
||||
[:default-account [:id]]]]
|
||||
[:account-for-vendor
|
||||
{:vendor-id vendor-id
|
||||
:client-id client-id}
|
||||
[:name :id :numeric-code :location]]]}
|
||||
:owns-state owns-state
|
||||
:on-success (fn [r]
|
||||
(let [schedule-payment-dom (->> r
|
||||
:vendor-by-id
|
||||
:schedule-payment-dom
|
||||
(filter (fn [spd]
|
||||
(= (-> spd :client :id)
|
||||
client-id)))
|
||||
first
|
||||
:dom)
|
||||
automatically-paid-when-due (boolean ((->> r :vendor-by-id :automatically-paid-when-due (map :id) set) client-id))]
|
||||
(conj on-success {:default-account (:account-for-vendor r)
|
||||
:schedule-payment-dom schedule-payment-dom
|
||||
:automatically-paid-when-due automatically-paid-when-due
|
||||
:vendor-autopay? (or automatically-paid-when-due (boolean schedule-payment-dom))})))
|
||||
:on-failure on-failure}}))
|
||||
::vendor-preferences-requested
|
||||
[with-user]
|
||||
(fn [{:keys [user]} [_ {:keys [client-id vendor-id on-success on-failure owns-state]}]]
|
||||
{:graphql {:token user
|
||||
:query-obj {:venia/queries [[:vendor-by-id
|
||||
{:id vendor-id}
|
||||
[[:automatically-paid-when-due [:id]]
|
||||
[:schedule-payment-dom [[:client [:id]] :dom]]
|
||||
[:default-account [:id]]]]
|
||||
[:account-for-vendor
|
||||
{:vendor-id vendor-id
|
||||
:client-id client-id}
|
||||
[:name :id :numeric-code :location]]]}
|
||||
:owns-state owns-state
|
||||
:on-success (fn [r]
|
||||
(let [schedule-payment-dom (->> r
|
||||
:vendor-by-id
|
||||
:schedule-payment-dom
|
||||
(filter (fn [spd]
|
||||
(= (-> spd :client :id)
|
||||
client-id)))
|
||||
first
|
||||
:dom)
|
||||
automatically-paid-when-due (boolean ((->> r :vendor-by-id :automatically-paid-when-due (map :id) set) client-id))]
|
||||
(conj on-success {:default-account (:account-for-vendor r)
|
||||
:schedule-payment-dom schedule-payment-dom
|
||||
:automatically-paid-when-due automatically-paid-when-due
|
||||
:vendor-autopay? (or automatically-paid-when-due (boolean schedule-payment-dom))})))
|
||||
:on-failure on-failure}}))
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
::client
|
||||
:<- [::selected-clients]
|
||||
(fn [selected-clients]
|
||||
(println "SELECTED CLIENTS ARE" selected-clients)
|
||||
(when (= 1 (count selected-clients))
|
||||
(first selected-clients))))
|
||||
|
||||
@@ -44,10 +43,6 @@
|
||||
:<- [::user]
|
||||
:<- [::clients]
|
||||
(fn [[selected-clients user clients]]
|
||||
(println "SELECTED" selected-clients
|
||||
"USER" user
|
||||
"CLIENTS" (count clients))
|
||||
|
||||
(cond (= :mine selected-clients)
|
||||
(sort-by :name
|
||||
(:user/clients user))
|
||||
@@ -58,17 +53,15 @@
|
||||
(nil? selected-clients))
|
||||
clients
|
||||
|
||||
(= :group (and (sequential? selected-clients)
|
||||
(first selected-clients)))
|
||||
(let [group (second selected-clients)]
|
||||
(:group selected-clients)
|
||||
(let [group (:group selected-clients)]
|
||||
(filterv
|
||||
(fn [c]
|
||||
(println "GROUP" group (:groups c))
|
||||
((set (:groups c)) group))
|
||||
clients))
|
||||
|
||||
(sequential? selected-clients)
|
||||
(filter (comp (set (map coerce-string-version selected-clients)) coerce-string-version :id)
|
||||
(:selected selected-clients)
|
||||
(filter (comp (set (:selected selected-clients)) coerce-string-version :id)
|
||||
clients)
|
||||
|
||||
:else
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
[auto-ap.views.pages.ledger.profit-and-loss-detail :refer [profit-and-loss-detail-page]]
|
||||
[auto-ap.views.pages.login :refer [login-page]]
|
||||
[auto-ap.views.pages.payments :refer [payments-page]]
|
||||
[auto-ap.views.pages.home :refer [home-page]]))
|
||||
[auto-ap.views.pages.home :refer [home-page home-page-with-vendor]]))
|
||||
|
||||
(defmulti page (fn [active-page] active-page))
|
||||
(defmethod page :unpaid-invoices [_]
|
||||
@@ -94,6 +94,10 @@
|
||||
(defmethod page :index [_]
|
||||
(home-page))
|
||||
|
||||
(defmethod page :new-vendor [_]
|
||||
(home-page-with-vendor))
|
||||
|
||||
|
||||
(defmethod page :login [_]
|
||||
[login-page])
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
(:require [auto-ap.routes :as routes]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.permissions :as p]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.components.vendor-dialog :as vendor-dialog]
|
||||
[auto-ap.history :refer [history]]
|
||||
[cemerick.url :as url]
|
||||
[auto-ap.views.utils
|
||||
@@ -35,15 +37,14 @@
|
||||
(defn make-pie-chart
|
||||
[{:keys [width height data]}]
|
||||
[pie-chart {:width width
|
||||
:height height}
|
||||
:height height}
|
||||
[pie {:fill "#82ca9d"
|
||||
:data data
|
||||
:dataKey "value"
|
||||
:inner-radius 20}
|
||||
(map (fn [_ y]
|
||||
^{:key y}
|
||||
[cell {:key y :fill (colors y)}]) data (range))
|
||||
]
|
||||
[cell {:key y :fill (colors y)}]) data (range))]
|
||||
[tool-tip]
|
||||
[legend]])
|
||||
|
||||
@@ -56,10 +57,9 @@
|
||||
[y-axis]
|
||||
[legend]])
|
||||
|
||||
(defn make-cash-flow-chart [{:keys [width height data] }]
|
||||
(defn make-cash-flow-chart [{:keys [width height data]}]
|
||||
(let [redirect-fn (fn [x]
|
||||
(pushy/set-token! history (str (bidi/path-for routes/routes :unpaid-invoices) "?" (get (js->clj x) "query-params")))
|
||||
)]
|
||||
(pushy/set-token! history (str (bidi/path-for routes/routes :unpaid-invoices) "?" (get (js->clj x) "query-params"))))]
|
||||
[bar-chart {:width width :height height :data data :fill "#FFFFFF" :stackOffset "sign"}
|
||||
[tool-tip]
|
||||
[bar {:dataKey "effective-balance" :fill (get colors 1) :stackId "a" :name "Effective Balance"
|
||||
@@ -69,13 +69,12 @@
|
||||
[bar {:dataKey "invoices" :fill (get colors 3) :stackId "a" :name "Invoices"
|
||||
:on-click redirect-fn}]
|
||||
[bar {:dataKey "credits" :fill (get colors 2) :stackId "a" :name "Upcoming Credits"
|
||||
:on-click redirect-fn}]
|
||||
:on-click redirect-fn}]
|
||||
[bar {:dataKey "debits" :fill (get colors 4) :stackId "a" :name "Upcoming Debits"
|
||||
:on-click redirect-fn}]
|
||||
[x-axis {:dataKey "name"}]
|
||||
[y-axis]
|
||||
[legend]])
|
||||
)
|
||||
[legend]]))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::received
|
||||
@@ -120,7 +119,7 @@
|
||||
(::top-expense-categories db)))
|
||||
|
||||
(defn sum-by-date [pairs]
|
||||
(reduce
|
||||
(reduce
|
||||
(fn [result [date amount]]
|
||||
(let [due (if (t/before? date (local-now))
|
||||
(local-now)
|
||||
@@ -156,10 +155,10 @@
|
||||
upcoming-debits (sum-by-date (map (fn [i] [(:date i) (:amount i)]) upcoming-debits))
|
||||
start-date (local-now)
|
||||
effective-balance (- beginning-balance outstanding-payments (invoices-due-soon (date->str start-date) 0.0))]
|
||||
|
||||
(reverse
|
||||
|
||||
(reverse
|
||||
(reduce
|
||||
(fn [[{:keys [effective-balance credits-yesterday] } :as acc] day]
|
||||
(fn [[{:keys [effective-balance credits-yesterday]} :as acc] day]
|
||||
(let [invoices-due-today (invoices-due-soon (date->str (t/plus start-date (t/days day))) 0.0)
|
||||
credits-due-today (upcoming-credits (date->str (t/plus start-date (t/days day))) 0.0)
|
||||
debits-due-today (upcoming-debits (date->str (t/plus start-date (t/days day))) 0.0)
|
||||
@@ -167,7 +166,7 @@
|
||||
(conj acc
|
||||
{:name (date->str today)
|
||||
:date today
|
||||
:effective-balance (+ (- effective-balance invoices-due-today )
|
||||
:effective-balance (+ (- effective-balance invoices-due-today)
|
||||
debits-due-today
|
||||
credits-yesterday)
|
||||
:credits-yesterday credits-due-today
|
||||
@@ -175,7 +174,7 @@
|
||||
:debits debits-due-today
|
||||
:invoices (- invoices-due-today)
|
||||
:query-params (url/map->query {:due-range {:start (date->str today standard)
|
||||
:end (date->str today standard)}})})))
|
||||
:end (date->str today standard)}})})))
|
||||
(list {:name (date->str start-date)
|
||||
:date start-date
|
||||
:effective-balance effective-balance
|
||||
@@ -212,7 +211,7 @@
|
||||
:<- [::cash-flow-table-params]
|
||||
:<- [::cash-flow-data]
|
||||
(fn [[params cash-flow-data]]
|
||||
(let [ {:keys [invoices-due-soon upcoming-credits upcoming-debits]} cash-flow-data
|
||||
(let [{:keys [invoices-due-soon upcoming-credits upcoming-debits]} cash-flow-data
|
||||
rows (concat (map (fn [c]
|
||||
{:date (:date c)
|
||||
:days-until (days-until (:date c))
|
||||
@@ -233,7 +232,7 @@
|
||||
:name (str (:name (:vendor c)) " (" (:invoice-number c) ")")
|
||||
:type "Invoice"})
|
||||
invoices-due-soon))]
|
||||
(assoc (grid/virtual-paginate-controls (:start params ) (:per-page params) rows)
|
||||
(assoc (grid/virtual-paginate-controls (:start params) (:per-page params) rows)
|
||||
:data (grid/virtual-paginate (:start params)
|
||||
(:per-page params)
|
||||
(sort-by (comp coerce/to-date :date) rows))))))
|
||||
@@ -243,19 +242,19 @@
|
||||
[(re-frame/inject-cofx ::inject/sub [::subs/client])]
|
||||
(fn [{:keys [db] ::subs/keys [client]} _]
|
||||
(cond->
|
||||
{:db (assoc db ::top-expense-categories nil
|
||||
::cash-flow nil
|
||||
::invoice-stats nil)}
|
||||
{:db (assoc db ::top-expense-categories nil
|
||||
::cash-flow nil
|
||||
::invoice-stats nil)}
|
||||
client (assoc :graphql {:token (-> db :user)
|
||||
:owns-state {:single ::page}
|
||||
:query-obj {:venia/queries [[:expense_account_stats
|
||||
{:client-id (:id client)}
|
||||
{:client-id (:id client)}
|
||||
[[:account [:id :name]] :total]]
|
||||
[:invoice_stats
|
||||
{:client-id (:id client)}
|
||||
{:client-id (:id client)}
|
||||
[:name :paid :unpaid]]
|
||||
[:cash-flow
|
||||
{:client-id (:id client)}
|
||||
{:client-id (:id client)}
|
||||
[:beginning-balance
|
||||
:outstanding-payments
|
||||
[:invoices-due-soon [:due :outstanding-balance [:vendor [:id :name]] :invoice-number]]
|
||||
@@ -287,42 +286,40 @@
|
||||
[grid/header-cell {} "Name"]
|
||||
[grid/header-cell {:class "has-text-right"} "Amount"]]]
|
||||
[grid/body
|
||||
(for [[i {:keys [date days-until type name amount] }] (map vector (range) (:data page))]
|
||||
(for [[i {:keys [date days-until type name amount]}] (map vector (range) (:data page))]
|
||||
^{:key i}
|
||||
[grid/row {}
|
||||
[grid/cell {}
|
||||
(if (> days-until 0)
|
||||
[:span.has-text-success days-until " days"]
|
||||
[:span.has-text-danger days-until " days"])
|
||||
[:i.is-size-7 " (" (date->str date) ")"] ]
|
||||
[:i.is-size-7 " (" (date->str date) ")"]]
|
||||
[grid/cell {} (if (> date 0)
|
||||
"Upcoming "
|
||||
"Due ")
|
||||
type]
|
||||
[grid/cell {} name]
|
||||
[grid/cell {:class "has-text-right"} (->$ amount)]
|
||||
])]]]))
|
||||
[grid/cell {:class "has-text-right"} (->$ amount)]])]]]))
|
||||
|
||||
(defn home-content []
|
||||
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)
|
||||
chart-options @(re-frame/subscribe [::chart-options])
|
||||
state @(re-frame/subscribe [::status/single ::page])]
|
||||
^{:key client-id}
|
||||
[side-bar-layout {:side-bar [:div
|
||||
]
|
||||
[side-bar-layout {:side-bar [:div]
|
||||
:main [:div [:h1.title "Home"]
|
||||
(if client-id
|
||||
(if client-id
|
||||
(if (= :loading (:state state))
|
||||
[:div.loader.is-loading.big.is-centered]
|
||||
|
||||
[:<>
|
||||
[:<>
|
||||
[:h1.title.is-4 "Top expense categories"]
|
||||
(let [expense-categories @(re-frame/subscribe [::top-expense-categories])]
|
||||
(make-pie-chart {:width 800 :height 500 :data (clj->js
|
||||
(map (fn [x] {:name (:name (:account x)) :value (:total x)}) expense-categories))}))
|
||||
(map (fn [x] {:name (:name (:account x)) :value (:total x)}) expense-categories))}))
|
||||
[:h1.title.is-4 "Upcoming Bills"]
|
||||
(make-bar-chart {:width 800 :height 500 :data (clj->js
|
||||
@(re-frame/subscribe [::invoice-stats]))})
|
||||
@(re-frame/subscribe [::invoice-stats]))})
|
||||
|
||||
[:h1.title.is-4 "Cash Flow"]
|
||||
[:div.buttons.has-addons
|
||||
@@ -360,4 +357,16 @@
|
||||
(defn home-page []
|
||||
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)]
|
||||
(re-frame/dispatch [::mounted])
|
||||
^{:key client-id} [home-content]))
|
||||
|
||||
(defn home-page-with-vendor []
|
||||
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)
|
||||
user @(re-frame/subscribe [::subs/user])]
|
||||
(re-frame/dispatch [::mounted])
|
||||
(when (p/can? user {:subject :vendor
|
||||
:activity :create})
|
||||
(re-frame/dispatch [::vendor-dialog/started {}]))
|
||||
|
||||
|
||||
|
||||
^{:key client-id} [home-content]))
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
|
||||
[:img {:src "/img/logo-big.png"}]
|
||||
[:div
|
||||
[:a.button.is-large.is-primary {:href (doto (login-url (get (:query (url/url (.-location js/window))) "redirect-to")) println)} "Login with Google"]]]
|
||||
[:a.button.is-large.is-primary {:href (login-url (get (:query (url/url (.-location js/window))) "redirect-to"))} "Login with Google"]]]
|
||||
[:p.has-text-gray
|
||||
"Copyright Integreat 2018"]]]]]])
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#!/bin/sh
|
||||
ssh -L 2049:172.31.32.90:2049 3.213.115.86 -L 8983:solr-staging.local:8983 -L 4334:integreat-datomic.local:4334 -L9001:integreat-app-staging.local:9000
|
||||
ssh -L 2049:172.31.32.90:2049 3.213.115.86 -L 8984:solr-staging.local:8983 -L 4334:integreat-datomic.local:4334 -L9001:integreat-app-staging.local:9000
|
||||
|
||||
Reference in New Issue
Block a user