Merge branch 'staging'

This commit is contained in:
2024-04-15 18:36:08 -07:00
47 changed files with 663 additions and 415 deletions

View File

@@ -8,6 +8,15 @@ document.addEventListener('alpine:init', () => {
el.removeEventListener('htmx:configRequest', config); 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 }) => { Alpine.directive('dispatch', (el, { value, expression }, { evaluateLater, effect, cleanup, evaluate }) => {
let dependent_properties = evaluateLater(expression) let dependent_properties = evaluateLater(expression)

File diff suppressed because one or more lines are too long

View File

@@ -601,7 +601,8 @@
(:sort args))) (:sort args)))
(defn apply-sort-3 [args results] (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?) {:sort-key "default" :asc (if (contains? args :default-asc?)
(:default-asc? args) (:default-asc? args)
true)}) true)})
@@ -609,16 +610,17 @@
comparator (fn [xs ys] comparator (fn [xs ys]
(reduce (reduce
(fn [_ i] (fn [_ i]
(let [comparison (if (:asc (nth sort-bys i)) (let [comparison (if (:asc (nth sort-bys i))
(compare (nth xs i) (nth ys i)) (compare (nth xs i) (nth ys i))
(compare (nth ys i) (nth xs i)))] (compare (nth ys i) (nth xs i)))]
(if (not= 0 comparison) (if (not= 0 comparison)
(reduced comparison) (reduced comparison)
0))) 0)))
0 0
(range length)))] (range length)))]
(sort comparator results ))) (sort comparator results)))
(defn apply-pagination-raw [args results] (defn apply-pagination-raw [args results]
{:entries (->> results {:entries (->> results
@@ -633,7 +635,8 @@
(:per-page args) (:per-page args)
default-pagination-size)) default-pagination-size))
(map last)) (map last))
:count (count results)}) :count (count results)
:all-ids (map last results)})
(defn audit-transact-batch [txes id] (defn audit-transact-batch [txes id]
(let [batch-id (.toString (java.util.UUID/randomUUID))] (let [batch-id (.toString (java.util.UUID/randomUUID))]

View File

@@ -2,9 +2,7 @@
(:require [amazonica.core :refer [defcredential]] (:require [amazonica.core :refer [defcredential]]
[auto-ap.client-routes :as client-routes] [auto-ap.client-routes :as client-routes]
[auto-ap.datomic :refer [conn pull-many]] [auto-ap.datomic :refer [conn pull-many]]
[auto-ap.datomic.clients :as d-clients] [auto-ap.graphql.utils :refer [limited-clients]]
[auto-ap.graphql.utils :refer [assert-can-see-client
limited-clients]]
[auto-ap.logging :as alog] [auto-ap.logging :as alog]
[auto-ap.routes.auth :as auth] [auto-ap.routes.auth :as auth]
[auto-ap.routes.exports :as exports] [auto-ap.routes.exports :as exports]
@@ -14,8 +12,10 @@
[auto-ap.routes.invoices :as invoices] [auto-ap.routes.invoices :as invoices]
[auto-ap.routes.queries :as queries] [auto-ap.routes.queries :as queries]
[auto-ap.routes.yodlee2 :as yodlee2] [auto-ap.routes.yodlee2 :as yodlee2]
[auto-ap.session-version :as session-version]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.core :as ssr] [auto-ap.ssr.core :as ssr]
[auto-ap.ssr.utils :refer [entity-id main-transformer]]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[bidi.ring :refer [->ResourcesMaybe make-handler]] [bidi.ring :refer [->ResourcesMaybe make-handler]]
[buddy.auth.backends.session :refer [session-backend]] [buddy.auth.backends.session :refer [session-backend]]
@@ -26,13 +26,14 @@
[cheshire.core :as cheshire] [cheshire.core :as cheshire]
[clj-time.coerce :as coerce] [clj-time.coerce :as coerce]
[clj-time.core :as time] [clj-time.core :as time]
[clojure.edn :as edn] [clojure.data.json :as json]
[clojure.set :as set] [clojure.set :as set]
[clojure.string :as str] [clojure.string :as str]
[com.brunobonacci.mulog :as mu] [com.brunobonacci.mulog :as mu]
[config.core :refer [env]] [config.core :refer [env]]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup] [hiccup2.core :as hiccup]
[malli.core :as mc]
[ring.middleware.edn :refer [wrap-edn-params]] [ring.middleware.edn :refer [wrap-edn-params]]
[ring.middleware.multipart-params :as mp] [ring.middleware.multipart-params :as mp]
[ring.middleware.params :refer [wrap-params]] [ring.middleware.params :refer [wrap-params]]
@@ -110,10 +111,10 @@
uri uri
:request-method request-method)) :request-method request-method))
matched-hx-current-url-route (some->> (get headers "hx-current-url") matched-hx-current-url-route (some->> (get headers "hx-current-url")
url/url url/url
:path :path
(bidi/match-route ssr-routes/only-routes) (bidi/match-route ssr-routes/only-routes)
:handler)] :handler)]
(handler (assoc request (handler (assoc request
:matched-route :matched-route
matched-route matched-route
@@ -135,6 +136,7 @@
(:uri request) (:uri request)
:request-method (:request-method request))) :request-method (:request-method request)))
:client-selection (:client-selection request)
:source "request" :source "request"
:query (:uri request) :query (:uri request)
:request-method (:request-method request) :request-method (:request-method request)
@@ -158,10 +160,12 @@
:exception e) :exception e)
(throw e))))))) (throw e)))))))
(defn wrap-idle-session-timeout (defn wrap-idle-session-timeout
[handler] [handler]
(fn [request] (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))] end-time (coerce/to-date-time (::idle-timeout session))]
(if (and end-time (time/before? end-time (time/now))) (if (and end-time (time/before? end-time (time/now)))
(if (get (:headers request) "hx-request") (if (get (:headers request) "hx-request")
@@ -185,10 +189,17 @@
request (assoc request :hx-query-params query-params)] request (assoc request :hx-query-params query-params)]
(handler request)))) (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 (defn wrap-hydrate-clients
[handler] [handler]
(fn [request] (fn [request]
(let [x-clients (-> request :session :client-selection) (let [x-clients (-> request :client-selection)
identity (or (-> request :identity) identity (or (-> request :identity)
(-> request :session :identity)) (-> request :session :identity))
ideal-ids (set (cond ideal-ids (set (cond
@@ -202,28 +213,21 @@
(= :mine x-clients) (= :mine x-clients)
(map :db/id (:user/clients identity)) (map :db/id (:user/clients identity))
(= :group (first x-clients)) (:group x-clients)
(->> (->>
(dc/q '[:find ?c (dc/q '[:find ?c
:in $ ?g :in $ ?g
:where [?c :client/groups ?g]] :where [?c :client/groups ?g]]
(dc/db conn) (dc/db conn)
(str/upper-case (or (second x-clients) "INVALID"))) (str/upper-case (or (:group x-clients) "INVALID")))
(map first) (map first)
set) set)
(seq x-clients) (seq (:selected x-clients))
(->> x-clients (->> x-clients
(map (fn [c] :selected
(if (string? c)
(try
(Long/parseLong c)
(catch Exception e
nil))
c)))
(filter #(not (nil? %))) (filter #(not (nil? %)))
set))) set)))
limited-clients (some->> (limited-clients identity) limited-clients (some->> (limited-clients identity)
(map :db/id) (map :db/id)
set) set)
@@ -236,12 +240,11 @@
(pull-many (dc/db conn) (pull-many (dc/db conn)
'[:db/id :client/name :client/code :client/locations '[:db/id :client/name :client/code :client/locations
:client/matches :client/feature-flags :client/matches :client/feature-flags
{:client/bank-accounts [:db/id {:client/bank-accounts [:db/id
{:bank-account/type [:db/ident]} {:bank-account/type [:db/ident]}
:bank-account/number :bank-account/number
:bank-account/name :bank-account/name
:bank-account/code]}]))] :bank-account/code]}]))]
(mu/with-context {:clients (take 10 (map :client/code clients))} (mu/with-context {:clients (take 10 (map :client/code clients))}
(handler (assoc request (handler (assoc request
:clients clients :clients clients
@@ -251,33 +254,22 @@
(defn wrap-store-client-in-session (defn wrap-store-client-in-session
[handler] [handler]
(fn [{:keys [headers identity] :as request}] (fn [{:keys [headers identity] :as request}]
(let [x-clients (edn/read-string (get headers "x-clients")) (let [client-selection (try (mc/decode client-selection-schema (some-> (get headers "x-clients") not-empty json/read-str) main-transformer)
x-clients (try (if-let [client-id (and x-clients (catch Exception e
(sequential? x-clients) (alog/warn ::cant-access :error e
(first x-clients) :identity identity
(not= :group (first x-clients)) :x-clients (pr-str (get headers "x-clients")))
(first x-clients))] nil))
(do
(assert-can-see-client identity (cond-> client-id new-request (if client-selection
(string? client-id) (Long/parseLong))) (assoc-in request [:client-selection] client-selection)
[(if (string? client-id) (assoc-in request [:client-selection] (get-in request [:session :client-selection] :all)))]
(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)]
(cond-> (handler new-request) (cond-> (handler new-request)
x-clients (update :session client-selection (update :session
(fn [new-session] (fn [new-session]
(-> (:session request) (-> (:session request)
(into new-session) (into new-session)
(assoc :client-selection x-clients)))))))) (assoc :client-selection client-selection))))))))
(defn wrap-gunzip-jwt (defn wrap-gunzip-jwt
[handler] [handler]
@@ -317,9 +309,9 @@
(-> route-handler (-> route-handler
(wrap-hx-current-url-params) (wrap-hx-current-url-params)
(wrap-guess-route) (wrap-guess-route)
(wrap-logging)
(wrap-hydrate-clients) (wrap-hydrate-clients)
(wrap-store-client-in-session) (wrap-store-client-in-session)
(wrap-logging)
(wrap-gunzip-jwt) (wrap-gunzip-jwt)
(wrap-authorization auth-backend) (wrap-authorization auth-backend)
(wrap-authentication auth-backend (wrap-authentication auth-backend
@@ -327,11 +319,13 @@
(dissoc auth :exp))})) (dissoc auth :exp))}))
#_(wrap-pprint-session) #_(wrap-pprint-session)
(session-version/wrap-session-version)
(wrap-idle-session-timeout) (wrap-idle-session-timeout)
(wrap-session {:store (cookie-store (wrap-session {:store (cookie-store
{:key {:key
(byte-array (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-reload)
(wrap-params) (wrap-params)

View File

@@ -20,9 +20,7 @@
(defn send-email-about-failed-message [mail-bucket mail-key message] (defn send-email-about-failed-message [mail-bucket mail-key message]
(let [target-key (str "failed-emails/" mail-key ".eml") (let [target-key (str "failed-emails/" mail-key ".eml")
target-url (str "http://" (:data-bucket env) target-url (str "https://" (:data-bucket env) "/" target-key)]
".s3-website-us-east-1.amazonaws.com/"
target-key)]
(alog/info ::sending-failure-email :who (:import-failure-destination-emails env)) (alog/info ::sending-failure-email :who (:import-failure-destination-emails env))
(s3/copy-object mail-bucket mail-key (:data-bucket env) target-key) (s3/copy-object mail-bucket mail-key (:data-bucket env) target-key)
(ses/send-email {:destination {:to-addresses (:import-failure-destination-emails env)} (ses/send-email {:destination {:to-addresses (:import-failure-destination-emails env)}
@@ -66,8 +64,8 @@
:content-length (.length (io/file filename))}) :content-length (.length (io/file filename))})
(let [imports (->> (parse/parse-file filename filename) (let [imports (->> (parse/parse-file filename filename)
(map #(assoc % (map #(assoc %
:source-url (str "http://" (:data-bucket env) :source-url (str "https://" (:data-bucket env)
".s3-website-us-east-1.amazonaws.com/" "/"
s3-location) s3-location)
:import-status :import-status/imported)))] :import-status :import-status/imported)))]
(alog/info ::found-imports :imports imports) (alog/info ::found-imports :imports imports)

View File

@@ -273,7 +273,7 @@
(mapcat (fn [k] (mapcat (fn [k]
(try (try
(let [invoice-key (copy-readable-version k) (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 (with-open [is (-> (s3/get-object {:bucket-name bucket-name
:key k}) :key k})
:input-stream)] :input-stream)]

View File

@@ -134,7 +134,7 @@
(mapcat (fn [k] (mapcat (fn [k]
(try (try
(let [invoice-key (str "invoice-files/" (UUID/randomUUID) ".csv") ; (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) (s3/copy-object {:source-bucket-name (:data-bucket env)
:destination-bucket-name (:data-bucket env) :destination-bucket-name (:data-bucket env)
:source-key k :source-key k

View File

@@ -338,7 +338,7 @@
pdf-data (make-pnl args data) pdf-data (make-pnl args data)
name (pnl-args->name args) name (pnl-args->name args)
key (str "reports/pnl/" uuid "/" name ".pdf") 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) (s3/put-object :bucket-name (:data-bucket env)
:key key :key key
:input-stream (io/make-input-stream pdf-data {}) :input-stream (io/make-input-stream pdf-data {})
@@ -359,7 +359,7 @@
pdf-data (make-cash-flows args data) pdf-data (make-cash-flows args data)
name (cash-flows-args->name args) name (cash-flows-args->name args)
key (str "reports/cash-flows/" uuid "/" name ".pdf") 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) (s3/put-object :bucket-name (:data-bucket env)
:key key :key key
:input-stream (io/make-input-stream pdf-data {}) :input-stream (io/make-input-stream pdf-data {})
@@ -380,7 +380,7 @@
pdf-data (make-balance-sheet args data) pdf-data (make-balance-sheet args data)
name (balance-sheet-args->name args) name (balance-sheet-args->name args)
key (str "reports/balance-sheet/" uuid "/" name ".pdf") 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) (s3/put-object :bucket-name (:data-bucket env)
:key key :key key
:input-stream (io/make-input-stream pdf-data {}) :input-stream (io/make-input-stream pdf-data {})
@@ -401,7 +401,7 @@
pdf-data (make-journal-detail-report args data) pdf-data (make-journal-detail-report args data)
name (journal-detail-args->name args) name (journal-detail-args->name args)
key (str "reports/journal-detail/" uuid "/" name ".pdf") 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) (s3/put-object :bucket-name (:data-bucket env)
:key key :key key
:input-stream (io/make-input-stream pdf-data {}) :input-stream (io/make-input-stream pdf-data {})

View File

@@ -4,6 +4,9 @@
[clj-time.core :as time] [clj-time.core :as time]
[clojure.string :as str])) [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] (defn wrap-parse-query-params [handler parser]
(fn parsed-handler [request] (fn parsed-handler [request]
@@ -42,13 +45,13 @@
[])) []))
(defn parse-long [l] (defn parse-long [l]
(try (try
(Long/parseLong l) (Long/parseLong l)
(catch Exception e (catch Exception e
nil))) nil)))
(defn parse-double [l] (defn parse-double [l]
(try (try
(Double/parseDouble l) (Double/parseDouble l)
(catch Exception e (catch Exception e
nil))) nil)))
@@ -58,9 +61,10 @@
(dissoc (dissoc
(condp = (source-key query-params) (condp = (source-key query-params)
"week" "week"
(assoc query-params (let [last-monday (atime/last-monday)]
start-date-key (time/plus (time/now) (time/days -7)) (assoc query-params
end-date-key (time/now)) start-date-key (time/plus last-monday (time/days -7))
end-date-key last-monday))
"month" "month"
(assoc query-params (assoc query-params
@@ -74,7 +78,7 @@
"all" "all"
(assoc query-params (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)) end-date-key (time/now))
query-params) query-params)
@@ -88,18 +92,18 @@
presently-sorted? ((set (map :sort-key current-sort)) key-to-toggle) presently-sorted? ((set (map :sort-key current-sort)) key-to-toggle)
new-sort (if presently-sorted? new-sort (if presently-sorted?
(mapv (mapv
(fn [s] (fn [s]
(if (= (:sort-key s) (if (= (:sort-key s)
key-to-toggle) key-to-toggle)
(-> s (-> s
(update :asc (update :asc
#(boolean (not %))) #(boolean (not %)))
(update :sort-icon (fn [x] (update :sort-icon (fn [x]
(if (= x svg/sort-down) (if (= x svg/sort-down)
svg/sort-up svg/sort-up
svg/sort-down)))) svg/sort-down))))
s)) s))
current-sort) current-sort)
(conj current-sort {:sort-key key-to-toggle (conj current-sort {:sort-key key-to-toggle
:asc true :asc true
:name (:name (first (filter #(= (str key-to-toggle) (:sort-key %)) (:headers grid-spec)))) :name (:name (first (filter #(= (str key-to-toggle) (:sort-key %)) (:headers grid-spec))))

View File

@@ -8,7 +8,8 @@
[config.core :refer [env]] [config.core :refer [env]]
[com.brunobonacci.mulog :as mu] [com.brunobonacci.mulog :as mu]
[clojure.java.io :as io] [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-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com")
(def google-client-secret "OC-WemHurPXYpuIw5cT-B90g") (def google-client-secret "OC-WemHurPXYpuIw5cT-B90g")
@@ -94,7 +95,8 @@
(jwt/sign jwt (jwt/sign jwt
(:jwt-secret env) (:jwt-secret env)
{:alg :hs512}))} {:alg :hs512}))}
:session {:identity (dissoc jwt :exp)}} :session {:identity (dissoc jwt :exp)
:version session-version/current-session-version}}
{:status 401 {:status 401
:body "Couldn't authenticate"})) :body "Couldn't authenticate"}))
(catch Exception e (catch Exception e

View File

@@ -193,8 +193,9 @@
(base-page (base-page
request request
(com/page {:nav com/admin-aside-nav (com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:client (:client request) :client (:client request)
:clients (:clients request)
:identity (:identity request) :identity (:identity request)
:request request :request request
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :app-params {:hx-get (bidi/path-for ssr-routes/only-routes

View File

@@ -277,8 +277,8 @@
:client-override client :client-override client
:location-override location :location-override location
:vendor-override vendor :vendor-override vendor
:source-url (str "http://" (:data-bucket env) :source-url (str "https://" (:data-bucket env)
".s3-website-us-east-1.amazonaws.com/" "/"
s3-location))))] s3-location))))]
(import-uploaded-invoice user imports)) (import-uploaded-invoice user imports))
{:status 200 {:status 200

View 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)))))

View File

@@ -45,7 +45,7 @@
(base-page (base-page
request request
(com/page {:nav com/admin-aside-nav (com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:clients (:clients request) :clients (:clients request)
:client (:client request) :client (:client request)
:identity (:identity request)} :identity (:identity request)}

View File

@@ -5,6 +5,7 @@
pull-many query2]] pull-many query2]]
[auto-ap.graphql.utils :refer [extract-client-ids]] [auto-ap.graphql.utils :refer [extract-client-ids]]
[auto-ap.logging :as alog] [auto-ap.logging :as alog]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.routes.admin.clients :as route] [auto-ap.routes.admin.clients :as route]
[auto-ap.routes.indicators :as indicators] [auto-ap.routes.indicators :as indicators]
[auto-ap.routes.queries :as q] [auto-ap.routes.queries :as q]
@@ -27,7 +28,7 @@
:refer [apply-middleware-to-all-handlers entity-id :refer [apply-middleware-to-all-handlers entity-id
form-validation-error html-response main-transformer form-validation-error html-response main-transformer
many-entity modal-response ref->enum-schema strip temp-id 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] [auto-ap.time :as atime]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[cheshire.core :as cheshire] [cheshire.core :as cheshire]
@@ -77,7 +78,7 @@
(com/text-input {:name "name" (com/text-input {:name "name"
:id "name" :id "name"
:class "hot-filter" :class "hot-filter"
:value (:name (:parsed-query-params request)) :value (:name (:query-params request))
:placeholder "Best Restaurant LLC" :placeholder "Best Restaurant LLC"
:size :small})) :size :small}))
@@ -85,14 +86,14 @@
(com/text-input {:name "code" (com/text-input {:name "code"
:id "code" :id "code"
:class "hot-filter" :class "hot-filter"
:value (:code (:parsed-query-params request)) :value (:code (:query-params request))
:placeholder "BRLC" :placeholder "BRLC"
:size :small})) :size :small}))
(com/field {:label "Group"} (com/field {:label "Group"}
(com/text-input {:name "group" (com/text-input {:name "group"
:id "group" :id "group"
:class "hot-filter" :class "hot-filter"
:value (:group (:parsed-query-params request)) :value (:group (:query-params request))
:placeholder "NTG" :placeholder "NTG"
:size :small})) :size :small}))
(com/field {:label "Select"} (com/field {:label "Select"}
@@ -150,7 +151,7 @@
:client/location-matches [:location-match/matches :location-match/location :db/id]}]) :client/location-matches [:location-match/matches :location-match/location :db/id]}])
(defn fetch-ids [db request] (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) valid-clients (extract-client-ids #_(:clients request)
(map first (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn))) (map first (dc/q '[:find ?c :where [?c :client/code]] (dc/db conn)))
(:client-id query-params) (:client-id query-params)
@@ -1842,8 +1843,8 @@
(def key->handler (def key->handler
(apply-middleware-to-all-handlers (apply-middleware-to-all-handlers
{::route/page (helper/page-route grid-page) {::route/page (helper/page-route grid-page :parse-query-params? false)
::route/table (helper/table-route grid-page) ::route/table (helper/table-route grid-page :parse-query-params? false)
::route/new-location (add-new-primitive-handler [:step-params :client/locations] ::route/new-location (add-new-primitive-handler [:step-params :client/locations]
"" ""
location-row) location-row)
@@ -1904,7 +1905,10 @@
(mm/wrap-wizard client-wizard))} (mm/wrap-wizard client-wizard))}
(fn [h] (fn [h]
(-> h (-> h
(wrap-copy-qp-pqp)
(wrap-apply-sort grid-page) (wrap-apply-sort grid-page)
(wrap-merge-prior-hx)
(wrap-schema-enforce :query-schema query-schema) (wrap-schema-enforce :query-schema query-schema)
(wrap-schema-enforce :hx-schema query-schema)
(wrap-admin) (wrap-admin)
(wrap-client-redirect-unauthenticated))))) (wrap-client-redirect-unauthenticated)))))

View File

@@ -243,7 +243,7 @@
(base-page (base-page
request request
(com/page {:nav com/admin-aside-nav (com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:clients (:clients request) :clients (:clients request)
:client (:client request) :client (:client request)
:identity (:identity request) :identity (:identity request)

View File

@@ -166,8 +166,9 @@
(some-> route-params (get :entity-id) Long/parseLong))] (some-> route-params (get :entity-id) Long/parseLong))]
(base-page request (base-page request
(com/page {:nav com/admin-aside-nav (com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:client (:client request) :client (:client request)
:clients (:clients request)
:identity (:identity request) :identity (:identity request)
:request request :request request
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :app-params {:hx-get (bidi/path-for ssr-routes/only-routes

View File

@@ -1,5 +1,6 @@
(ns auto-ap.ssr.auth (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]])) [config.core :refer [env]]))
(defn logout [request] (defn logout [request]
@@ -13,4 +14,5 @@
:session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"]) :session {:identity (dissoc (jwt/unsign (get-in request [:query-params "jwt"])
(:jwt-secret env) (:jwt-secret env)
{:alg :hs512}) {:alg :hs512})
:exp)}}) :exp)
:version session-version/current-session-version}})

View File

@@ -131,8 +131,9 @@
(base-page (base-page
request request
(com/page {:nav com/company-aside-nav (com/page {:nav com/company-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:client (:client request) :client (:client request)
:clients (:clients request)
:identity (:identity request) :identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:company) :company)

View File

@@ -1,6 +1,7 @@
(ns auto-ap.ssr.company-dropdown (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.graphql.utils :refer [cleanse-query]]
[auto-ap.logging :as alog]
[auto-ap.solr :as solr] [auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.hx :as hx] [auto-ap.ssr.hx :as hx]
@@ -10,7 +11,9 @@
[clojure.string :as str] [clojure.string :as str]
[datomic.api :as dc] [datomic.api :as dc]
[hiccup2.core :as hiccup] [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]}] (defn dropdown-search-results* [{:keys [options]}]
[:ul [:ul
@@ -25,6 +28,7 @@
:request-method :put) :request-method :put)
:hx-target "#company-dropdown" :hx-target "#company-dropdown"
:hx-headers (hx/json {"x-clients" (pr-str [:group group])}) :hx-headers (hx/json {"x-clients" (pr-str [:group group])})
"@click" (format "globalClientSelection={group: %s}" (hx/json group))
:hx-swap "outerHTML" :hx-swap "outerHTML"
:hx-trigger "click"} :hx-trigger "click"}
name] name]
@@ -34,6 +38,7 @@
:request-method :put) :request-method :put)
:hx-target "#company-dropdown" :hx-target "#company-dropdown"
:hx-headers (format "{\"x-clients\": \"[%d]\"}" id) :hx-headers (format "{\"x-clients\": \"[%d]\"}" id)
"@click" (format "globalClientSelection={selected: [%d]}" id)
:hx-swap "outerHTML" :hx-swap "outerHTML"
:hx-trigger "click"} :hx-trigger "click"}
name])]])]) name])]])])
@@ -64,11 +69,18 @@
(dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text"))}))) (dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text"))})))
(defn dropdown [{:keys [client-selection client identity clients]}] (defn dropdown [{:keys [client-selection client identity clients]}]
(alog/peek ::clients clients)
[:div#company-dropdown [:div#company-dropdown
[:script [:script
(hiccup/raw (hiccup/raw
"localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n" "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 [: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" [: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"} :type "button"}
@@ -80,7 +92,7 @@
(and client (and client
(= 1 (count clients))) (= 1 (count clients)))
( :client/name client) (:client/name client)
:else :else
(str (count clients) " Companies")) (str (count clients) " Companies"))
@@ -116,6 +128,8 @@
:active-client :active-client
:request-method :put) :request-method :put)
:hx-target "#company-dropdown" :hx-target "#company-dropdown"
"@click" "globalClientSelection=\"mine\""
:hx-headers "{\"x-clients\": \":mine\"}" :hx-headers "{\"x-clients\": \":mine\"}"
:hx-swap "outerHTML" :hx-swap "outerHTML"
:hx-trigger "click"} :hx-trigger "click"}
@@ -127,6 +141,7 @@
:active-client :active-client
:request-method :put) :request-method :put)
:hx-target "#company-dropdown" :hx-target "#company-dropdown"
"@click" "globalClientSelection=\"all\""
:hx-headers "{\"x-clients\": \":all\"}" :hx-headers "{\"x-clients\": \":all\"}"
:hx-swap "outerHTML" :hx-swap "outerHTML"
:hx-trigger "click"} :hx-trigger "click"}
@@ -161,7 +176,7 @@ function initCompanyDropdown() {
(defn active-client [{:keys [identity params] :as request}] (defn active-client [{:keys [identity params] :as request}]
(assoc (assoc
(html-response (html-response
(dropdown {:client-selection (:client-selection (:session request)) (dropdown {:client-selection (:client-selection request)
:clients (:clients request) :clients (:clients request)
:client (:client request) :client (:client request)
:identity identity})) :identity identity}))

View File

@@ -111,26 +111,26 @@
:active? (= "invoices" selected)} :active? (= "invoices" selected)}
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/all-page) ::invoice-route/all-page)
{:date-range "month"}) {:date-range "year"})
:active? (= ::invoice-route/all-page (:matched-route request)) :active? (= ::invoice-route/all-page (:matched-route request))
:hx-boost "true"} :hx-boost "true"}
"All") "All")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/paid-page) ::invoice-route/paid-page)
{:date-range "month"}) {:date-range "year"})
:active? (= ::invoice-route/paid-page (:matched-route request)) :active? (= ::invoice-route/paid-page (:matched-route request))
:hx-boost "true"} :hx-boost "true"}
"Paid") "Paid")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/unpaid-page) ::invoice-route/unpaid-page)
{:date-range "month"}) {:date-range "year"})
:active? (= ::invoice-route/unpaid-page (:matched-route request)) :active? (= ::invoice-route/unpaid-page (:matched-route request))
:hx-boost "true"} :hx-boost "true"}
"Unpaid") "Unpaid")
(menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes (menu-button- {:href (hu/url (bidi/path-for ssr-routes/only-routes
::invoice-route/voided-page) ::invoice-route/voided-page)
{:date-range "month"}) {:date-range "year"})
:active? (= ::invoice-route/voided-page (:matched-route request)) :active? (= ::invoice-route/voided-page (:matched-route request))
:hx-boost "true"} :hx-boost "true"}
"Voided") "Voided")

View File

@@ -1,26 +1,28 @@
(ns auto-ap.ssr.components.date-range (ns auto-ap.ssr.components.date-range
(:require [auto-ap.ssr.components :as com] (: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} [:div {:id id}
(com/field {:label "Date Range"} (com/field {:label "Date Range"}
[:div.space-y-4 [:div.space-y-4
[:div [:div
(com/button-group {:name "date-range"} (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 "all" :hx-trigger "click"} "All")
(com/button-group-button {:size :small :value "week" :hx-trigger "click"} "Week") (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 "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 [:div.flex.space-x-1.items-baseline.w-full.justify-start
(com/date-input {:name "start-date" (com/date-input {:name "start-date"
:value (some-> (:start value) :value (some-> (:start value)
(atime/unparse-local atime/normal-date)) (atime/unparse-local atime/normal-date))
:placeholder "Date" :placeholder "Date"
:size :small :size :small
:class "shrink"}) :class "shrink"})
(com/date-input {:name "end-date" (com/date-input {:name "end-date"
:value (some-> (:end value) :value (some-> (:end value)
(atime/unparse-local atime/normal-date)) (atime/unparse-local atime/normal-date))

View File

@@ -14,12 +14,12 @@
(com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"} (com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"}
svg/paperclip 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.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 {:class "p-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}
[:div.flex.flex-col.gap-y-2 [:div.flex.flex-col.gap-y-2
(for [l links] (for [l links]
[:div.flex-initial [: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"} (com/pill {:color (or (:color l) :primary) :class "truncate block shrink grow-0"}
(:content l))]])]]]])) (:content l))]])]]]]))

View File

@@ -114,7 +114,7 @@
:class "dark:text-blue-500"} :class "dark:text-blue-500"}
"Back"]) "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) (let [steps (steps linear-wizard)
last? (= (step-key step) (last steps)) last? (= (step-key step) (last steps))
next-step (when-not last? (->> steps next-step (when-not last? (->> steps
@@ -131,9 +131,10 @@
{:from (encode-step-key (step-key step)) {:from (encode-step-key (step-key step))
:to (encode-step-key (step-key next-step))}))) :to (encode-step-key (step-key next-step))})))
(if next-step (or next-button-content
(step-name next-step) (if next-step
"Save") (step-name next-step)
"Save"))
(when-not last? (when-not last?
[:div.w-5.h-5 svg/arrow-right])))) [:div.w-5.h-5 svg/arrow-right]))))
@@ -143,7 +144,8 @@
(defn default-step-footer [linear-wizard step & {:keys [validation-route (defn default-step-footer [linear-wizard step & {:keys [validation-route
discard-button discard-button
next-button]}] next-button
next-button-content]}]
[:div.flex.justify-end [:div.flex.justify-end
[:div.flex.items-baseline.gap-x-4 [:div.flex.items-baseline.gap-x-4
(com/form-errors {:errors (:errors (:step-params fc/*form-errors*))}) (com/form-errors {:errors (:errors (:step-params fc/*form-errors*))})
@@ -157,7 +159,8 @@
next-button next-button
validation-route 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 :else
[:div "No action possible."])]]) [:div "No action possible."])]])

View File

@@ -21,4 +21,9 @@
children)) children))
(defn badge- [params & 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])

View File

@@ -27,7 +27,8 @@
[:li [: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"]] [: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)) (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 [: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" [: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 />")} "_" (hiccup/raw "on click toggle .dark on <body />")}

View File

@@ -21,6 +21,8 @@
[malli.transform :as mt] [malli.transform :as mt]
[taoensso.encore :refer [filter-vals]])) [taoensso.encore :refer [filter-vals]]))
(defn row* [gridspec user entity {:keys [flash? delete-after-settle? request class] :as options}] (defn row* [gridspec user entity {:keys [flash? delete-after-settle? request class] :as options}]
(let [cells (if (:check-boxes? gridspec) (let [cells (if (:check-boxes? gridspec)
[(com/data-grid-cell {} (com/checkbox {:name "id" :value ((:id-fn gridspec) entity) [(com/data-grid-cell {} (com/checkbox {:name "id" :value ((:id-fn gridspec) entity)
@@ -75,10 +77,15 @@
"default sort")) "default sort"))
(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}] (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) (let [start (or start 0)
per-page (or per-page 25) per-page (or per-page 25)
[entities total] ((:fetch-page grid-spec) [entities total :as page-results] ((:fetch-page grid-spec)
request)] request)
request (assoc request :page-results page-results)]
(com/data-grid-card {:id (:id grid-spec) (com/data-grid-card {:id (:id grid-spec)
:title (if (string? (:title grid-spec)) :title (if (string? (:title grid-spec))
@@ -206,8 +213,9 @@
set)] set)]
(handler (assoc request :trimmed-clients valid-clients))))) (handler (assoc request :trimmed-clients valid-clients)))))
(defn table-route [grid-spec] (defn table-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
(-> (fn table [{:keys [identity] :as request}] (cond-> (fn table [{:keys [identity] :as request}]
(let [unparse-query-params (or (:unparse-query grid-spec) (let [unparse-query-params (or (:unparse-query grid-spec)
default-unparse-query-params)] default-unparse-query-params)]
(html-response (table* (html-response (table*
@@ -224,49 +232,53 @@
main-transformer)) main-transformer))
"sort" sort->query))) "sort" sort->query)))
(update (filter-vals #(not (nil? %)) (update (filter-vals #(not (nil? %))
(m/encode (:query-schema grid-spec) (m/encode (:query-schema grid-spec)
(:query-params request) (:query-params request)
main-transformer)) main-transformer))
"sort" sort->query)) "sort" sort->query))
(unparse-query-params (:parsed-query-params request))) (unparse-query-params (:parsed-query-params request)))
"selected" "all-selected")))} ;; TODO seems hacky to special case selected and all-selected here "selected" "all-selected")))} ;; TODO seems hacky to special case selected and all-selected here
:oob (when-let [oob-render (:oob-render grid-spec)] :oob (when-let [oob-render (:oob-render grid-spec)]
(oob-render request))))) (oob-render request)))))
(wrap-trim-client-ids) true (wrap-trim-client-ids)
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec) parse-query-params? (query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
(default-parse-query-params grid-spec))) (default-parse-query-params grid-spec)))
(wrap-secure) true (wrap-secure)
(wrap-client-redirect-unauthenticated))) true (wrap-client-redirect-unauthenticated)))
(defn page-route [grid-spec] (defn page-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
(-> (fn page [{:keys [identity] :as request}]
(base-page (cond-> (fn page [{:keys [identity] :as request}]
request (alog/info ::page-route
(com/page {:nav (:nav grid-spec) :pqp (:parsed-query-params request)
:page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)] :qp (:query-params request))
[:div#page-specific-nav (page-specific-nav request)]) (base-page
:client-selection (:client-selection (:session request)) request
:clients (:clients request) (com/page {:nav (:nav grid-spec)
:client (:client request) :page-specific (when-let [page-specific-nav (:page-specific-nav grid-spec)]
:identity (:identity request) [:div#page-specific-nav (page-specific-nav request)])
:request request} :client-selection (:client-selection request)
(apply com/breadcrumbs {} (:breadcrumbs grid-spec)) :clients (:clients request)
[:div {:x-data (hx/json {:selected [] :all_selected false}) :client (:client request)
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" :identity (:identity request)
:x-init "$watch('selected', s=> $dispatch('selectedChanged', {selected: s, all_selected: all_selected}) ); :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}))"} $watch('all_selected', a=>$dispatch('selectedChanged', {selected: selected, all_selected: a}))"}
(table* grid-spec (table* grid-spec
identity identity
request)]) request)])
(if (string? (:title grid-spec)) (if (string? (:title grid-spec))
(:title grid-spec) (:title grid-spec)
((:title grid-spec) request)))) ((:title grid-spec) request))))
(wrap-trim-client-ids) true (wrap-trim-client-ids)
(query-params/wrap-parse-query-params (or (:parse-query-params grid-spec) parse-query-params? (query-params/wrap-parse-query-params (or (:parse-query-params grid-spec)
(default-parse-query-params grid-spec))) (default-parse-query-params grid-spec)))
(wrap-secure) true (wrap-secure)
(wrap-client-redirect-unauthenticated))) true (wrap-client-redirect-unauthenticated)))
(def request-spec (m/schema [:map])) (def request-spec (m/schema [:map]))
(def entity-spec (m/schema [:map])) (def entity-spec (m/schema [:map]))
@@ -280,8 +292,8 @@
(def grid-spec (m/schema [:map (def grid-spec (m/schema [:map
[:id :string] [:id :string]
[:nav [:=> [:nav [:=>
[:cat request-spec] [:cat request-spec]
vector?]] vector?]]
[:page-specific-nav [:page-specific-nav
{:optional true {:optional true
:default (fn [request])} :default (fn [request])}
@@ -338,8 +350,8 @@
(handler (update request :query-params (handler (update request :query-params
(fn [qp] (fn [qp]
((comp ((comp
(query-params/apply-remove-sort) (query-params/apply-remove-sort)
(query-params/apply-toggle-sort grid-spec) (query-params/apply-toggle-sort grid-spec)
(query-params/parse-key :sort #(query-params/parse-sort grid-spec %))) (query-params/parse-key :sort #(query-params/parse-sort grid-spec %)))
qp)))))) qp))))))

View File

@@ -421,8 +421,9 @@ invoice_dropzone = new Dropzone(\"#invoice\", {
(base-page (base-page
request request
(com/page {:nav com/admin-aside-nav (com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:client (:client request) :client (:client request)
:clients (:clients request)
:identity (:identity request) :identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:invoice-glimpse) :invoice-glimpse)

View File

@@ -10,6 +10,7 @@
[auto-ap.routes.utils [auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated]] :refer [wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm] [auto-ap.rule-matching :as rm]
[auto-ap.client-routes :as client-routes]
[auto-ap.solr :as solr] [auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]] [auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
@@ -229,6 +230,14 @@
:value (fc/field-value) :value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c)) :content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))
:x-model "vendorId"})])) :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 [: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)))) (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(defn invoice-expense-account-total* [request] (defn invoice-expense-account-total* [request]
(format "$%,.2f" (->> (-> request (let [total (->> (-> request
:multi-form-state :multi-form-state
:step-params :step-params
:invoice/expense-accounts) :invoice/expense-accounts)
(map (fnil :invoice-expense-account/amount 0.0)) (map (fnil :invoice-expense-account/amount 0.0))
(filter number?) (filter number?)
(reduce + 0.0)))) (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] (defn invoice-expense-account-total [request]
(html-response (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] (defrecord AccountsStep [linear-wizard]
mm/ModalWizardStep mm/ModalWizardStep
(step-name [_] (step-name [_]
@@ -481,6 +509,19 @@
:hx-swap "innerHTML"} :hx-swap "innerHTML"}
(invoice-expense-account-total* request)) (invoice-expense-account-total* request))
(com/data-grid-cell {})) (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-row {}
(com/data-grid-cell {}) (com/data-grid-cell {})
@@ -592,12 +633,12 @@
(when (seq eas) (when (seq eas)
(let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas))) (let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas)))
leftover-beyond-a-single-cent? (or (< leftover -1) leftover-beyond-a-single-cent? (or (< leftover -1)
(> leftover 1)) (> leftover 1))
leftover (if leftover-beyond-a-single-cent? leftover (if leftover-beyond-a-single-cent?
0 0
leftover) leftover)
[first-eas & rest] eas] [first-eas & rest] eas]
(cons (cons
(update first-eas :invoice-expense-account/amount #(+ % leftover)) (update first-eas :invoice-expense-account/amount #(+ % leftover))
rest)))) rest))))
@@ -813,6 +854,9 @@
::route/expense-account-total (-> invoice-expense-account-total ::route/expense-account-total (-> invoice-expense-account-total
(mm/wrap-wizard new-wizard) (mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state)) (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 ::route/location-select (-> location-select
(wrap-schema-enforce :query-schema [:map (wrap-schema-enforce :query-schema [:map
[:name :string] [:name :string]

View File

@@ -7,6 +7,7 @@
[auto-ap.datomic.accounts :as d-accounts] [auto-ap.datomic.accounts :as d-accounts]
[auto-ap.datomic.bank-accounts :as d-bank-accounts] [auto-ap.datomic.bank-accounts :as d-bank-accounts]
[auto-ap.datomic.invoices :as d-invoices] [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 [auto-ap.graphql.checks :as gq-checks :refer [base-payment
invoice-payments invoice-payments
print-checks-internal print-checks-internal
@@ -120,8 +121,6 @@
(exact-match-id* request)]]) (exact-match-id* request)]])
(defn fetch-ids [db {:keys [query-params route-params] :as request}] (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-id request) (:client-id request)
@@ -238,6 +237,7 @@
query-params) query-params)
true true
(merge-query {:query {:find ['?sort-default '?e]}})))] (merge-query {:query {:find ['?sort-default '?e]}})))]
(->> (observable-query query) (->> (observable-query query)
(apply-sort-3 (assoc query-params :default-asc? false)) (apply-sort-3 (assoc query-params :default-asc? false))
(apply-pagination query-params)))) (apply-pagination query-params))))
@@ -251,12 +251,42 @@
(map first))] (map first))]
refunds)) 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] (defn fetch-page [request]
(let [db (dc/db conn) (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)) [(->> (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 (def query-schema (mc/schema
[:maybe [:map {:date-range [:date-range :start-date :end-date]} [:maybe [:map {:date-range [:date-range :start-date :end-date]}
@@ -378,7 +408,6 @@
(:query-params request))}))) (:query-params request))})))
;; TODO test as a real user ;; TODO test as a real user
(def grid-page (def grid-page
(helper/build {:id "entity-table" (helper/build {:id "entity-table"
@@ -392,20 +421,27 @@
(assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)]) (assoc-in (exact-match-id* request) [1 :hx-swap-oob] true)])
:query-schema query-schema :query-schema query-schema
:parse-query-params (fn [p] :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] :action-buttons (fn [request]
[(when (can? (:identity request) {:subject :invoice :activity :bulk-delete}) (let [[_ _ outstanding total] (:page-results request)]
(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete)) [(com/pill {:color :primary} "Outstanding: "
"x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})" (format "$%,.2f" outstanding))
"hx-include" "#invoice-filters" (com/pill {:color :secondary} "Total: "
:color :red} (format "$%,.2f" total))
"Void selected"))
(when (can? (:identity request) {:subject :invoice :activity :pay}) (when (can? (:identity request) {:subject :invoice :activity :bulk-delete})
(pay-button* {:ids (selected->ids request (com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes ::route/bulk-delete))
(:query-params request))})) "x-bind:hx-vals" "JSON.stringify({selected: $data.selected, 'all-selected': $data.all_selected})"
(when (can? (:identity request) {:subject :invoice :activity :create}) "hx-include" "#invoice-filters"
(com/button {:hx-get (bidi/path-for ssr-routes/only-routes ::route/new-wizard)} :color :red}
"New invoice"))]) "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] :row-buttons (fn [request entity]
[(when (and (= :invoice-status/unpaid (:invoice/status entity)) [(when (and (= :invoice-status/unpaid (:invoice/status entity))
(can? (:identity request) {:subject :invoice :activity :delete})) (can? (:identity request) {:subject :invoice :activity :delete}))
@@ -556,10 +592,10 @@
_ (audit-transact tx identity)] _ (audit-transact tx identity)]
(alog/info ::unvoiding-invoice :transaction :tx) (alog/info ::unvoiding-invoice :transaction :tx)
(html-response (html-response
(row* identity (dc/pull (dc/db conn) default-read id) {:flash? true (row* identity (dc/pull (dc/db conn) default-read id) {:flash? true
:request request}) :request request})
:headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id) :headers (cond-> {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" id)
"hx-reswap" "outerHTML"})))) "hx-reswap" "outerHTML"}))))
(defn delete [{invoice :entity :as request identity :identity}] (defn delete [{invoice :entity :as request identity :identity}]
(exception->notification (exception->notification
@@ -716,7 +752,6 @@
(map :invoice-id invoices)) (map :invoice-id invoices))
(into {}))] (into {}))]
(every? (fn [%] (every? (fn [%]
(println "TEST" (:amount %) (outstanding-balances (:invoice-id %)))
(not (does-amount-exceed-outstanding? (:amount %) (outstanding-balances (:invoice-id %))))) (not (does-amount-exceed-outstanding? (:amount %) (outstanding-balances (:invoice-id %)))))
invoices)))]]] invoices)))]]]
[:has-warning? :boolean] [:has-warning? :boolean]
@@ -968,7 +1003,8 @@
:name (fc/field-name) :name (fc/field-name)
:error? (fc/error?)}))))))))))]]) :error? (fc/error?)}))))))))))]])
:footer :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))) :validation-route ::route/pay-wizard-navigate)))
(defn add-handwritten-check [request wizard snapshot] (defn add-handwritten-check [request wizard snapshot]
@@ -1137,7 +1173,7 @@
(->> (dc/q '[:find ?i (->> (dc/q '[:find ?i
:in $ [?i ...] :in $ [?i ...]
:where [?i :invoice/status :invoice-status/unpaid] :where [?i :invoice/status :invoice-status/unpaid]
[?i :invoice/client ?c] ] [?i :invoice/client ?c]]
(dc/db conn) (dc/db conn)
ids) ids)
(map first))) (map first)))
@@ -1177,13 +1213,13 @@
(def key->handler (def key->handler
(apply-middleware-to-all-handlers (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)) (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)) (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)) (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)) (wrap-implied-route-param :status :invoice-status/voided))
::route/unvoid (-> unvoid-invoice ::route/unvoid (-> unvoid-invoice
(wrap-entity [:route-params :db/id] default-read) (wrap-entity [:route-params :db/id] default-read)
@@ -1212,12 +1248,14 @@
(mm/wrap-wizard pay-wizard) (mm/wrap-wizard pay-wizard)
(mm/wrap-decode-multi-form-state)) (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)) (merge new-invoice-wizard/key->handler))
(fn [h] (fn [h]
(-> h (-> h
(wrap-copy-qp-pqp)
(wrap-status-from-source) (wrap-status-from-source)
(wrap-apply-sort grid-page) (wrap-apply-sort grid-page)
(wrap-schema-enforce :query-schema query-schema)
(wrap-merge-prior-hx) (wrap-merge-prior-hx)
(wrap-schema-enforce :query-schema query-schema)
(wrap-schema-enforce :hx-schema query-schema)
(wrap-client-redirect-unauthenticated))))) (wrap-client-redirect-unauthenticated)))))

View File

@@ -251,7 +251,7 @@
(base-page (base-page
request request
(com/page {:nav com/main-aside-nav (com/page {:nav com/main-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:clients (:clients request) :clients (:clients request)
:client (:client request) :client (:client request)
:identity (:identity request) :identity (:identity request)

View File

@@ -7,6 +7,7 @@
[auto-ap.graphql.utils :refer [assert-can-see-client [auto-ap.graphql.utils :refer [assert-can-see-client
exception->notification exception->notification
extract-client-ids notify-if-locked]] extract-client-ids notify-if-locked]]
[auto-ap.query-params :refer [wrap-copy-qp-pqp]]
[auto-ap.logging :as alog] [auto-ap.logging :as alog]
[auto-ap.permissions :refer [can?]] [auto-ap.permissions :refer [can?]]
[auto-ap.routes.invoice :as invoice-route] [auto-ap.routes.invoice :as invoice-route]
@@ -38,8 +39,8 @@
[malli.transform :as mt])) [malli.transform :as mt]))
(defn exact-match-id* [request] (defn exact-match-id* [request]
(if (nat-int? (:exact-match-id (:parsed-query-params request))) (if (nat-int? (:exact-match-id (:query-params request)))
[:div {:x-data (hx/json {:exact_match (:exact-match-id (:parsed-query-params request))}) :id "exact-match-id-tag"} [:div {:x-data (hx/json {:exact_match (:exact-match-id (:query-params request))}) :id "exact-match-id-tag"}
(com/hidden {:name "exact-match-id" (com/hidden {:name "exact-match-id"
"x-model" "exact_match"}) "x-model" "exact_match"})
(com/pill {:color :primary} (com/pill {:color :primary}
@@ -68,7 +69,7 @@
:value (:vendor (:query-params request)) :value (:vendor (:query-params request))
:value-fn :db/id :value-fn :db/id
:content-fn :vendor/name})) :content-fn :vendor/name}))
(date-range-field* request) (date-range-field* (assoc request :parsed-query-params (:query-params request)))
(com/field {:label "Check #"} (com/field {:label "Check #"}
(com/text-input {:name "check-number" (com/text-input {:name "check-number"
:id "check-number" :id "check-number"
@@ -130,7 +131,7 @@
{:transaction/_payment [:db/id :transaction/date]}]) {:transaction/_payment [:db/id :transaction/date]}])
(defn fetch-ids [db {:keys [query-params route-params] :as request}] (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 request)
(:client-id query-params) (:client-id query-params)
(when (:client-code query-params) (when (:client-code query-params)
@@ -419,7 +420,8 @@
(audit-transact (conj removing-payments updated-payment) (audit-transact (conj removing-payments updated-payment)
identity) 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))}))) :headers {"hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id check))})))
;; TODO use decoding here ;; TODO use decoding here
@@ -529,13 +531,13 @@
(def key->handler (def key->handler
(apply-middleware-to-all-handlers (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)) (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)) (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)) (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)) (wrap-implied-route-param :status nil))
::route/delete (-> delete ::route/delete (-> delete
@@ -548,9 +550,10 @@
(wrap-admin)) (wrap-admin))
::route/table (helper/table-route grid-page)} ::route/table (helper/table-route grid-page :parse-query-params? false)}
(fn [h] (fn [h]
(-> h (-> h
(wrap-copy-qp-pqp)
(wrap-apply-sort grid-page) (wrap-apply-sort grid-page)
(wrap-merge-prior-hx) (wrap-merge-prior-hx)
(wrap-status-from-source) (wrap-status-from-source)

View File

@@ -318,8 +318,9 @@
(base-page (base-page
request request
(com/page {:nav com/main-aside-nav (com/page {:nav com/main-aside-nav
:client-selection (:client-selection (:session request)) :client-selection (:client-selection request)
:client (:client request) :client (:client request)
:clients (:clients request)
:identity (:identity request) :identity (:identity request)
:app-params {:hx-get (bidi/path-for ssr-routes/only-routes :app-params {:hx-get (bidi/path-for ssr-routes/only-routes
:transaction-insights) :transaction-insights)

View File

@@ -69,9 +69,10 @@ input::-webkit-inner-spin-button {
input[type=number] { input[type=number] {
-moz-appearance:textfield; /* Firefox */ -moz-appearance:textfield; /* Firefox */
} "] } "]
[:body {:hx-ext "disable-submit, class-tools"
:x-data (hx/json {:globalClientSelection (or (:client-selection request)
[:body {:hx-ext "disable-submit, class-tools"} :all )}) ;; TODO remove once session is used
:x-hx-header:x-clients "JSON.stringify(globalClientSelection)"}
contents contents
[:script {:src "/js/flowbite.min.js"}] [:script {:src "/js/flowbite.min.js"}]

View File

@@ -48,7 +48,7 @@
"hx-target" "#user-table" "hx-target" "#user-table"
"hx-indicator" "#user-table"} "hx-indicator" "#user-table"}
[:fieldset.space-y-6 [:fieldset.space-y-6
(com/field {:label "Name"} (com/field {:label "Name"}
(com/text-input {:name "name" (com/text-input {:name "name"
:id "name" :id "name"
@@ -57,6 +57,16 @@
:placeholder "Johnny Testerson" :placeholder "Johnny Testerson"
:size :small})) :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/field {:label "Email"}
(com/text-input {:name "email" (com/text-input {:name "email"
:id "email" :id "email"
@@ -66,31 +76,22 @@
:size :small})) :size :small}))
(com/field {:label "Role"} (com/field {:label "Role"}
(com/radio-card {:size :small (com/radio-card {:size :small
:name "role" :name "role"
:options [{:value "" :options [{:value ""
:content "All"} :content "All"}
{:value "admin" {:value "admin"
:content "Admin"} :content "Admin"}
{:value "power-user" {:value "power-user"
:content "Power user"} :content "Power user"}
{:value "manager" {:value "manager"
:content "Manager"} :content "Manager"}
{:value "user" {:value "user"
:content "User"} :content "User"}
{:value "read-only" {:value "read-only"
:content "Read Only"} :content "Read Only"}
{:value "none" {:value "none"
:content "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}))]])
(def default-read '[:db/id (def default-read '[:db/id
:user/name :user/name

View File

@@ -107,7 +107,7 @@
(mt2/transformer (mt2/transformer
{:decoders {:decoders
{:map (fn [m] {:map (fn [m]
(if (not (seq (filter identity (vals m)))) (if (and (map? m) (not (seq (filter identity (vals m)))))
nil nil
m)) m))
:string empty->nil :string empty->nil
@@ -240,9 +240,10 @@
(if date-range-value (if date-range-value
(-> (condp = date-range-value (-> (condp = date-range-value
"week" "week"
(assoc m (let [last-monday (atime/last-monday)]
start-date-key (time/plus (time/now) (time/days -7)) (assoc m
end-date-key (time/now)) start-date-key (time/plus last-monday (time/days -7))
end-date-key last-monday))
"month" "month"
(assoc m (assoc m
@@ -255,7 +256,7 @@
end-date-key (time/now)) end-date-key (time/now))
"all" "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)) end-date-key (time/now))
m) m)

View File

@@ -1,5 +1,6 @@
(ns auto-ap.time (ns auto-ap.time
(:require [clj-time.core :as time] (:require [clj-time.core :as time]
[clj-time.coerce :as coerce]
[clj-time.format :as f] [clj-time.format :as f]
[auto-ap.logging :as alog])) [auto-ap.logging :as alog]))
@@ -40,7 +41,7 @@
(defn unparse-local [v format] (defn unparse-local [v format]
(try (try
(f/unparse (f/with-zone (f/formatter format) (time/time-zone-for-id "America/Los_Angeles")) v) (f/unparse (f/with-zone (f/formatter format) (time/time-zone-for-id "America/Los_Angeles")) v)
(catch Exception _ (catch Exception _
nil))) nil)))
@@ -52,3 +53,14 @@
d d
(recur (time/plus d (time/days 1)))))] (recur (time/plus d (time/days 1)))))]
(iterate #(time/plus % (time/days 7)) next-day))) (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))))))

View File

@@ -6,7 +6,8 @@
"needs-activation/" :needs-activation "needs-activation/" :needs-activation
"needs-activation" :needs-activation "needs-activation" :needs-activation
"payments/" :payments "payments/" :payments
"admin/" { "vendors" :admin-vendors} "admin/" {"vendors" :admin-vendors}
"vendor/" {"new" :new-vendor}
"invoices/" {"" :invoices "invoices/" {"" :invoices
"import" :import-invoices "import" :import-invoices
"unpaid" :unpaid-invoices "unpaid" :unpaid-invoices

View File

@@ -12,7 +12,8 @@
"/account/new" ::new-wizard-new-account "/account/new" ::new-wizard-new-account
"/account/location-select" ::location-select "/account/location-select" ::location-select
"/account/prediction" ::account-prediction "/account/prediction" ::account-prediction
"/total" ::expense-account-total} "/total" ::expense-account-total
"/balance" ::expense-account-balance}
"/pay-button" ::pay-button "/pay-button" ::pay-button
"/pay" {:get ::pay-wizard "/pay" {:get ::pay-wizard

View 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

View File

@@ -1,27 +1,26 @@
(ns auto-ap.effects (ns auto-ap.effects
(:require-macros [cljs.core.async.macros :refer [go]]) (:require-macros [cljs.core.async.macros :refer [go]])
(:require (:require [auto-ap.client-selection :refer [client-selection-schema]]
[auto-ap.history :as p] [auto-ap.history :as p]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.views.utils :refer [date->str standard]] [auto-ap.views.utils :refer [date->str standard]]
[cemerick.url :as url] [cemerick.url :as url]
[cljs-http.client :as http] [cljs-http.client :as http]
[cljs-time.coerce :as c] [cljs-time.coerce :as c]
[cljs-time.core :as time] [cljs-time.core :as time]
[cljs-time.format :as format] [cljs-time.format :as format]
[cljs.core.async :refer [<!] :as async] [cljs.core.async :refer [<!] :as async]
[clojure.string :as str] [clojure.string :as str]
[clojure.walk :as walk] [clojure.walk :as walk]
[pushy.core :as pushy] [malli.core :as mc]
[re-frame.core :as re-frame] [pushy.core :as pushy]
[venia.core :as v])) [re-frame.core :as re-frame]
[venia.core :as v]))
(defn maybe-add-x-clients [headers] (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))) (if (and (mc/validate client-selection-schema (: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))) (not (get headers "x-clients")))
(and (sequential? (:selected-clients @re-frame.db/app-db)) (= :group (first (:selected-clients @re-frame.db/app-db)))) (assoc headers "x-clients" (.stringify js/JSON (clj->js (: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)))
headers)) headers))
(re-frame/reg-fx (re-frame/reg-fx
@@ -199,27 +198,27 @@
"&variables=" (pr-str (or variables {})))}))] "&variables=" (pr-str (or variables {})))}))]
(cond (cond
(= (:status response) 401) (= (:status response) 401)
(re-frame/dispatch [:auto-ap.events/logout "Your session has expired. Please log in again."]) (re-frame/dispatch [:auto-ap.events/logout "Your session has expired. Please log in again."])
(>= (:status response) 400) (>= (:status response) 400)
(let [error (->> response (let [error (->> response
:body :body
:errors :errors
(dates->date-times) (dates->date-times)
(map #(assoc % :status (:status response))) (map #(assoc % :status (:status response))))]
)]
(when (:multi owns-state) (when (:multi owns-state)
(re-frame/dispatch [::status/error-multi (:multi owns-state) (:which owns-state) error])) (re-frame/dispatch [::status/error-multi (:multi owns-state) (:which owns-state) error]))
(when (:single owns-state) (when (:single owns-state)
(re-frame/dispatch [::status/error (:single owns-state) error])) (re-frame/dispatch [::status/error (:single owns-state) error]))
(when on-error (when on-error
(->> error (->> error
(conj on-error) (conj on-error)
(re-frame/dispatch)))) (re-frame/dispatch))))
:else :else
(do (do
(when (:multi owns-state) (when (:multi owns-state)
(re-frame/dispatch [::status/completed-multi (:multi owns-state) (:which owns-state)])) (re-frame/dispatch [::status/completed-multi (:multi owns-state) (:which owns-state)]))
(when (:single owns-state) (when (:single owns-state)

View File

@@ -1,24 +1,24 @@
(ns auto-ap.events (ns auto-ap.events
(:require (:require [auto-ap.client-selection :refer [client-selection-schema
[auto-ap.db :as db] client-selection-transformer]]
[auto-ap.routes :as routes] [auto-ap.db :as db]
[auto-ap.utils :refer [by]] [auto-ap.routes :as routes]
[auto-ap.views.pages.data-page :as data-page] [auto-ap.ssr-routes :as ssr-routes]
[auto-ap.views.utils :refer [parse-jwt with-user gunzip]] [auto-ap.utils :refer [by]]
[bidi.bidi :as bidi] [auto-ap.views.pages.data-page :as data-page]
[clojure.string :as str] [auto-ap.views.utils :refer [gunzip parse-jwt with-user]]
[clojure.edn :as edn] [bidi.bidi :as bidi]
[goog.crypt.base64 :as b64] [cemerick.url :as url]
[re-frame.core :as re-frame] [clojure.string :as str]
[auto-ap.ssr-routes :as ssr-routes] [goog.crypt.base64 :as b64]
[cemerick.url :as url] [malli.core :as mc]
[auto-ap.subs :as subs] [pako]
[pako])) [re-frame.core :as re-frame]))
(defn jwt->data [token] (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) gz-clients (or (:gz-clients raw)
(get raw "gz-clients"))] (get raw "gz-clients"))]
(cond-> raw (cond-> raw
@@ -29,7 +29,7 @@
(defn client-query [] (defn client-query []
(cond-> [:id :name :code :email :locations :feature-flags :groups (cond-> [:id :name :code :email :locations :feature-flags :groups
[:emails [:id :email :description]] [: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 :locations :include-in-reports :current-balance
:sort-order]]])) :sort-order]]]))
@@ -46,18 +46,28 @@
[:plaid-account [:name :id :number]] [:plaid-account [:name :id :number]]
[:intuit-bank-account [:name :id :external-id]] [:intuit-bank-account [:name :id :external-id]]
:use-date-instead-of-post-date :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]] [:address [:id :street1 :street2 :city :state :zip]]
[:forecasted-transactions [:id :amount :identifier :day-of-month]]] [: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]]]] (= "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]]]]]))) [:plaid-items [:id [:accounts [:id :name :number :balance]]]]])))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::initialize-db ::initialize-db
(fn [{:keys [_]} [_ token]] (fn [{:keys [_]} [_ token]]
(let [handler (:handler (bidi/match-route routes/routes (.. js/window -location -pathname))) (let [handler (:handler (bidi/match-route routes/routes (.. js/window -location -pathname)))
last-client-id (.getItem js/localStorage "last-client-id") 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) jwt-data (some-> token jwt->data)
selected-client-assignment (cond (and token selected-client-assignment (cond (and token
(= "admin" (get jwt-data "user/role")) (= "admin" (get jwt-data "user/role"))
@@ -72,7 +82,7 @@
:else :else
nil)] nil)]
(cond (cond
(= :login handler) (= :login handler)
{:db (cond-> (assoc db/default-db {:db (cond-> (assoc db/default-db
@@ -89,7 +99,7 @@
:selected-clients last-selected-clients :selected-clients last-selected-clients
:user token)} :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" {:redirect "/needs-activation"
:db (assoc db/default-db :db (assoc db/default-db
:active-route :needs-activation :active-route :needs-activation
@@ -112,7 +122,7 @@
:on-success [::received-initial] :on-success [::received-initial]
:on-error [::failed-initial]}} :on-error [::failed-initial]}}
selected-client-assignment 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 (re-frame/reg-event-db
@@ -125,13 +135,13 @@
::received-initial ::received-initial
(fn [{:keys [db]} [_ {clients :client}]] (fn [{:keys [db]} [_ {clients :client}]]
(let [only-one-client (when (= 1 (count clients)) (let [only-one-client (when (= 1 (count clients))
(->> clients first :id ))] (->> clients first :id))]
(when only-one-client (when only-one-client
(.setItem js/localStorage "last-client-id" only-one-client) (.setItem js/localStorage "last-client-id" only-one-client)
(.setItem js/localStorage "last-selected-clients" (.setItem js/localStorage "last-selected-clients"
(pr-str [(js/parseInt only-one-client)]))) (pr-str [(js/parseInt only-one-client)])))
{:db (cond-> (-> db {:db (cond-> (-> db
(assoc :clients (by :id clients) ) (assoc :clients (by :id clients))
(assoc :is-initial-loading? false) (assoc :is-initial-loading? false)
(assoc :client (or only-one-client (assoc :client (or only-one-client
(->> clients (->> clients
@@ -167,37 +177,37 @@
:active-route :initial-error))) :active-route :initial-error)))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::swapped-client ::swapped-client
(fn [{:keys [db]} [_ client client-identifier]] (fn [{:keys [db]} [_ client client-identifier]]
(when (:id client) (when (:id client)
(.setItem js/localStorage "last-client-id" (:id client))) (.setItem js/localStorage "last-client-id" (:id client)))
(.setItem js/localStorage "last-selected-clients" (.setItem js/localStorage "last-selected-clients"
(condp = client-identifier (.stringify js/JSON
:all (clj->js (condp = client-identifier
:all :all
:all
:mine :mine
:mine :mine
(pr-str [(js/parseInt (:id client))]))) {:selected [(js/parseInt (:id client))]}))))
{:db (assoc db :client (:id client) {:db (assoc db :client (:id client)
:selected-clients :selected-clients
(condp = client-identifier (condp = client-identifier
:all :all
:all :all
:mine :mine
:mine :mine
[(js/parseInt (:id client))]))})) {:selected [(js/parseInt (:id client))]}))}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::swap-client ::swap-client
[with-user] [with-user]
(fn [{:keys [db user]} [_ client]] (fn [{:keys [db user]} [_ client]]
(let [client-identifier (or (:id client) client)] (let [client-identifier (or (:id client) client)]
{:http {:token user {:http {:token user
:method :put :method :put
:uri (str (bidi/path-for ssr-routes/only-routes :uri (str (bidi/path-for ssr-routes/only-routes
@@ -205,12 +215,20 @@
:request-method :put) :request-method :put)
"?" "?"
(url/map->query {:search-client client-identifier})) (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]}}))) :on-success [::swapped-client client client-identifier]}})))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::set-active-route ::set-active-route
(fn [{:keys [db]} [_ handler params route-params]] (fn [{:keys [db]} [_ handler params route-params]]
(cond (cond
(and (not= :login handler) (not (:user db))) (and (not= :login handler) (not (:user db)))
{:redirect (bidi/path-for routes/routes :login) {:redirect (bidi/path-for routes/routes :login)
:db (assoc db :active-route :login :db (assoc db :active-route :login
@@ -256,36 +274,36 @@
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
{:graphql {:token (:user db) {:graphql {:token (:user db)
:query-obj {:venia/queries [[:yodlee-merchants :query-obj {:venia/queries [[:yodlee-merchants
[:name :yodlee-id :id]]]} [:name :yodlee-id :id]]]}
:on-success [::yodlee-merchants-received]}})) :on-success [::yodlee-merchants-received]}}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::vendor-preferences-requested ::vendor-preferences-requested
[with-user] [with-user]
(fn [{:keys [user]} [_ {:keys [ client-id vendor-id on-success on-failure owns-state]}]] (fn [{:keys [user]} [_ {:keys [client-id vendor-id on-success on-failure owns-state]}]]
{:graphql {:token user {:graphql {:token user
:query-obj {:venia/queries [[:vendor-by-id :query-obj {:venia/queries [[:vendor-by-id
{:id vendor-id} {:id vendor-id}
[[:automatically-paid-when-due [:id]] [[:automatically-paid-when-due [:id]]
[:schedule-payment-dom [[:client [:id]] :dom]] [:schedule-payment-dom [[:client [:id]] :dom]]
[:default-account [:id]]]] [:default-account [:id]]]]
[:account-for-vendor [:account-for-vendor
{:vendor-id vendor-id {:vendor-id vendor-id
:client-id client-id} :client-id client-id}
[:name :id :numeric-code :location]]]} [:name :id :numeric-code :location]]]}
:owns-state owns-state :owns-state owns-state
:on-success (fn [r] :on-success (fn [r]
(let [schedule-payment-dom (->> r (let [schedule-payment-dom (->> r
:vendor-by-id :vendor-by-id
:schedule-payment-dom :schedule-payment-dom
(filter (fn [spd] (filter (fn [spd]
(= (-> spd :client :id) (= (-> spd :client :id)
client-id))) client-id)))
first first
:dom) :dom)
automatically-paid-when-due (boolean ((->> r :vendor-by-id :automatically-paid-when-due (map :id) set) client-id))] 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) (conj on-success {:default-account (:account-for-vendor r)
:schedule-payment-dom schedule-payment-dom :schedule-payment-dom schedule-payment-dom
:automatically-paid-when-due automatically-paid-when-due :automatically-paid-when-due automatically-paid-when-due
:vendor-autopay? (or automatically-paid-when-due (boolean schedule-payment-dom))}))) :vendor-autopay? (or automatically-paid-when-due (boolean schedule-payment-dom))})))
:on-failure on-failure}})) :on-failure on-failure}}))

View File

@@ -17,7 +17,6 @@
::client ::client
:<- [::selected-clients] :<- [::selected-clients]
(fn [selected-clients] (fn [selected-clients]
(println "SELECTED CLIENTS ARE" selected-clients)
(when (= 1 (count selected-clients)) (when (= 1 (count selected-clients))
(first selected-clients)))) (first selected-clients))))
@@ -44,10 +43,6 @@
:<- [::user] :<- [::user]
:<- [::clients] :<- [::clients]
(fn [[selected-clients user clients]] (fn [[selected-clients user clients]]
(println "SELECTED" selected-clients
"USER" user
"CLIENTS" (count clients))
(cond (= :mine selected-clients) (cond (= :mine selected-clients)
(sort-by :name (sort-by :name
(:user/clients user)) (:user/clients user))
@@ -58,17 +53,15 @@
(nil? selected-clients)) (nil? selected-clients))
clients clients
(= :group (and (sequential? selected-clients) (:group selected-clients)
(first selected-clients))) (let [group (:group selected-clients)]
(let [group (second selected-clients)]
(filterv (filterv
(fn [c] (fn [c]
(println "GROUP" group (:groups c))
((set (:groups c)) group)) ((set (:groups c)) group))
clients)) clients))
(sequential? selected-clients) (:selected selected-clients)
(filter (comp (set (map coerce-string-version selected-clients)) coerce-string-version :id) (filter (comp (set (:selected selected-clients)) coerce-string-version :id)
clients) clients)
:else :else

View File

@@ -18,7 +18,7 @@
[auto-ap.views.pages.ledger.profit-and-loss-detail :refer [profit-and-loss-detail-page]] [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.login :refer [login-page]]
[auto-ap.views.pages.payments :refer [payments-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)) (defmulti page (fn [active-page] active-page))
(defmethod page :unpaid-invoices [_] (defmethod page :unpaid-invoices [_]
@@ -94,6 +94,10 @@
(defmethod page :index [_] (defmethod page :index [_]
(home-page)) (home-page))
(defmethod page :new-vendor [_]
(home-page-with-vendor))
(defmethod page :login [_] (defmethod page :login [_]
[login-page]) [login-page])

View File

@@ -2,7 +2,9 @@
(:require [auto-ap.routes :as routes] (:require [auto-ap.routes :as routes]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components.grid :as grid] [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.layouts :refer [side-bar-layout]]
[auto-ap.views.components.vendor-dialog :as vendor-dialog]
[auto-ap.history :refer [history]] [auto-ap.history :refer [history]]
[cemerick.url :as url] [cemerick.url :as url]
[auto-ap.views.utils [auto-ap.views.utils
@@ -35,15 +37,14 @@
(defn make-pie-chart (defn make-pie-chart
[{:keys [width height data]}] [{:keys [width height data]}]
[pie-chart {:width width [pie-chart {:width width
:height height} :height height}
[pie {:fill "#82ca9d" [pie {:fill "#82ca9d"
:data data :data data
:dataKey "value" :dataKey "value"
:inner-radius 20} :inner-radius 20}
(map (fn [_ y] (map (fn [_ y]
^{:key y} ^{:key y}
[cell {:key y :fill (colors y)}]) data (range)) [cell {:key y :fill (colors y)}]) data (range))]
]
[tool-tip] [tool-tip]
[legend]]) [legend]])
@@ -56,10 +57,9 @@
[y-axis] [y-axis]
[legend]]) [legend]])
(defn make-cash-flow-chart [{:keys [width height data] }] (defn make-cash-flow-chart [{:keys [width height data]}]
(let [redirect-fn (fn [x] (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"} [bar-chart {:width width :height height :data data :fill "#FFFFFF" :stackOffset "sign"}
[tool-tip] [tool-tip]
[bar {:dataKey "effective-balance" :fill (get colors 1) :stackId "a" :name "Effective Balance" [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" [bar {:dataKey "invoices" :fill (get colors 3) :stackId "a" :name "Invoices"
:on-click redirect-fn}] :on-click redirect-fn}]
[bar {:dataKey "credits" :fill (get colors 2) :stackId "a" :name "Upcoming Credits" [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" [bar {:dataKey "debits" :fill (get colors 4) :stackId "a" :name "Upcoming Debits"
:on-click redirect-fn}] :on-click redirect-fn}]
[x-axis {:dataKey "name"}] [x-axis {:dataKey "name"}]
[y-axis] [y-axis]
[legend]]) [legend]]))
)
(re-frame/reg-event-db (re-frame/reg-event-db
::received ::received
@@ -120,7 +119,7 @@
(::top-expense-categories db))) (::top-expense-categories db)))
(defn sum-by-date [pairs] (defn sum-by-date [pairs]
(reduce (reduce
(fn [result [date amount]] (fn [result [date amount]]
(let [due (if (t/before? date (local-now)) (let [due (if (t/before? date (local-now))
(local-now) (local-now)
@@ -156,10 +155,10 @@
upcoming-debits (sum-by-date (map (fn [i] [(:date i) (:amount i)]) upcoming-debits)) upcoming-debits (sum-by-date (map (fn [i] [(:date i) (:amount i)]) upcoming-debits))
start-date (local-now) start-date (local-now)
effective-balance (- beginning-balance outstanding-payments (invoices-due-soon (date->str start-date) 0.0))] effective-balance (- beginning-balance outstanding-payments (invoices-due-soon (date->str start-date) 0.0))]
(reverse (reverse
(reduce (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) (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) 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) debits-due-today (upcoming-debits (date->str (t/plus start-date (t/days day))) 0.0)
@@ -167,7 +166,7 @@
(conj acc (conj acc
{:name (date->str today) {:name (date->str today)
:date today :date today
:effective-balance (+ (- effective-balance invoices-due-today ) :effective-balance (+ (- effective-balance invoices-due-today)
debits-due-today debits-due-today
credits-yesterday) credits-yesterday)
:credits-yesterday credits-due-today :credits-yesterday credits-due-today
@@ -175,7 +174,7 @@
:debits debits-due-today :debits debits-due-today
:invoices (- invoices-due-today) :invoices (- invoices-due-today)
:query-params (url/map->query {:due-range {:start (date->str today standard) :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) (list {:name (date->str start-date)
:date start-date :date start-date
:effective-balance effective-balance :effective-balance effective-balance
@@ -212,7 +211,7 @@
:<- [::cash-flow-table-params] :<- [::cash-flow-table-params]
:<- [::cash-flow-data] :<- [::cash-flow-data]
(fn [[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] rows (concat (map (fn [c]
{:date (:date c) {:date (:date c)
:days-until (days-until (:date c)) :days-until (days-until (:date c))
@@ -233,7 +232,7 @@
:name (str (:name (:vendor c)) " (" (:invoice-number c) ")") :name (str (:name (:vendor c)) " (" (:invoice-number c) ")")
:type "Invoice"}) :type "Invoice"})
invoices-due-soon))] 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) :data (grid/virtual-paginate (:start params)
(:per-page params) (:per-page params)
(sort-by (comp coerce/to-date :date) rows)))))) (sort-by (comp coerce/to-date :date) rows))))))
@@ -243,19 +242,19 @@
[(re-frame/inject-cofx ::inject/sub [::subs/client])] [(re-frame/inject-cofx ::inject/sub [::subs/client])]
(fn [{:keys [db] ::subs/keys [client]} _] (fn [{:keys [db] ::subs/keys [client]} _]
(cond-> (cond->
{:db (assoc db ::top-expense-categories nil {:db (assoc db ::top-expense-categories nil
::cash-flow nil ::cash-flow nil
::invoice-stats nil)} ::invoice-stats nil)}
client (assoc :graphql {:token (-> db :user) client (assoc :graphql {:token (-> db :user)
:owns-state {:single ::page} :owns-state {:single ::page}
:query-obj {:venia/queries [[:expense_account_stats :query-obj {:venia/queries [[:expense_account_stats
{:client-id (:id client)} {:client-id (:id client)}
[[:account [:id :name]] :total]] [[:account [:id :name]] :total]]
[:invoice_stats [:invoice_stats
{:client-id (:id client)} {:client-id (:id client)}
[:name :paid :unpaid]] [:name :paid :unpaid]]
[:cash-flow [:cash-flow
{:client-id (:id client)} {:client-id (:id client)}
[:beginning-balance [:beginning-balance
:outstanding-payments :outstanding-payments
[:invoices-due-soon [:due :outstanding-balance [:vendor [:id :name]] :invoice-number]] [:invoices-due-soon [:due :outstanding-balance [:vendor [:id :name]] :invoice-number]]
@@ -287,42 +286,40 @@
[grid/header-cell {} "Name"] [grid/header-cell {} "Name"]
[grid/header-cell {:class "has-text-right"} "Amount"]]] [grid/header-cell {:class "has-text-right"} "Amount"]]]
[grid/body [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} ^{:key i}
[grid/row {} [grid/row {}
[grid/cell {} [grid/cell {}
(if (> days-until 0) (if (> days-until 0)
[:span.has-text-success days-until " days"] [:span.has-text-success days-until " days"]
[:span.has-text-danger 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) [grid/cell {} (if (> date 0)
"Upcoming " "Upcoming "
"Due ") "Due ")
type] type]
[grid/cell {} name] [grid/cell {} name]
[grid/cell {:class "has-text-right"} (->$ amount)] [grid/cell {:class "has-text-right"} (->$ amount)]])]]]))
])]]]))
(defn home-content [] (defn home-content []
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id) (let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)
chart-options @(re-frame/subscribe [::chart-options]) chart-options @(re-frame/subscribe [::chart-options])
state @(re-frame/subscribe [::status/single ::page])] state @(re-frame/subscribe [::status/single ::page])]
^{:key client-id} ^{:key client-id}
[side-bar-layout {:side-bar [:div [side-bar-layout {:side-bar [:div]
]
:main [:div [:h1.title "Home"] :main [:div [:h1.title "Home"]
(if client-id (if client-id
(if (= :loading (:state state)) (if (= :loading (:state state))
[:div.loader.is-loading.big.is-centered] [:div.loader.is-loading.big.is-centered]
[:<> [:<>
[:h1.title.is-4 "Top expense categories"] [:h1.title.is-4 "Top expense categories"]
(let [expense-categories @(re-frame/subscribe [::top-expense-categories])] (let [expense-categories @(re-frame/subscribe [::top-expense-categories])]
(make-pie-chart {:width 800 :height 500 :data (clj->js (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"] [:h1.title.is-4 "Upcoming Bills"]
(make-bar-chart {:width 800 :height 500 :data (clj->js (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"] [:h1.title.is-4 "Cash Flow"]
[:div.buttons.has-addons [:div.buttons.has-addons
@@ -360,4 +357,16 @@
(defn home-page [] (defn home-page []
(let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)] (let [client-id (-> @(re-frame/subscribe [::subs/client]) :id)]
(re-frame/dispatch [::mounted]) (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])) ^{:key client-id} [home-content]))

View File

@@ -21,6 +21,6 @@
[:img {:src "/img/logo-big.png"}] [:img {:src "/img/logo-big.png"}]
[:div [: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 [:p.has-text-gray
"Copyright Integreat 2018"]]]]]]) "Copyright Integreat 2018"]]]]]])

View File

@@ -1,2 +1,2 @@
#!/bin/sh #!/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