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);
})
})
Alpine.directive('hx-header', (el, { value, expression }, { evaluateLater, effect, cleanup, evaluate }) => {
var config = function(evt) {
evt.detail.headers[value] = evaluate(expression); // add a new parameter into the request
}
el.addEventListener('htmx:configRequest', config);
cleanup(() => {
el.removeEventListener('htmx:configRequest', config);
})
})
Alpine.directive('dispatch', (el, { value, expression }, { evaluateLater, effect, cleanup, evaluate }) => {
let dependent_properties = evaluateLater(expression)

File diff suppressed because one or more lines are too long

View File

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

View File

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

View File

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

View File

@@ -273,7 +273,7 @@
(mapcat (fn [k]
(try
(let [invoice-key (copy-readable-version k)
invoice-url (str "http://" bucket-name ".s3-website-us-east-1.amazonaws.com/" invoice-key)]
invoice-url (str "https://" bucket-name "/" invoice-key)]
(with-open [is (-> (s3/get-object {:bucket-name bucket-name
:key k})
:input-stream)]

View File

@@ -134,7 +134,7 @@
(mapcat (fn [k]
(try
(let [invoice-key (str "invoice-files/" (UUID/randomUUID) ".csv") ;
invoice-url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" invoice-key)]
invoice-url (str "https://" (:data-bucket env) "/" invoice-key)]
(s3/copy-object {:source-bucket-name (:data-bucket env)
:destination-bucket-name (:data-bucket env)
:source-key k

View File

@@ -338,7 +338,7 @@
pdf-data (make-pnl args data)
name (pnl-args->name args)
key (str "reports/pnl/" uuid "/" name ".pdf")
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
url (str "https://" (:data-bucket env) "/" key)]
(s3/put-object :bucket-name (:data-bucket env)
:key key
:input-stream (io/make-input-stream pdf-data {})
@@ -359,7 +359,7 @@
pdf-data (make-cash-flows args data)
name (cash-flows-args->name args)
key (str "reports/cash-flows/" uuid "/" name ".pdf")
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
url (str "https://" (:data-bucket env) "/" key)]
(s3/put-object :bucket-name (:data-bucket env)
:key key
:input-stream (io/make-input-stream pdf-data {})
@@ -380,7 +380,7 @@
pdf-data (make-balance-sheet args data)
name (balance-sheet-args->name args)
key (str "reports/balance-sheet/" uuid "/" name ".pdf")
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
url (str "https://" (:data-bucket env) "/" key)]
(s3/put-object :bucket-name (:data-bucket env)
:key key
:input-stream (io/make-input-stream pdf-data {})
@@ -401,7 +401,7 @@
pdf-data (make-journal-detail-report args data)
name (journal-detail-args->name args)
key (str "reports/journal-detail/" uuid "/" name ".pdf")
url (str "http://" (:data-bucket env) ".s3-website-us-east-1.amazonaws.com/" key)]
url (str "https://" (:data-bucket env) "/" key)]
(s3/put-object :bucket-name (:data-bucket env)
:key key
:input-stream (io/make-input-stream pdf-data {})

View File

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

View File

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

View File

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

View File

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

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
request
(com/page {:nav com/admin-aside-nav
:client-selection (:client-selection (:session request))
:client-selection (:client-selection request)
:clients (:clients request)
:client (:client request)
:identity (:identity request)}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
(ns auto-ap.ssr.company-dropdown
(:require [auto-ap.datomic :refer [conn pull-attr pull-many]]
(:require [auto-ap.datomic :refer [conn pull-many]]
[auto-ap.graphql.utils :refer [cleanse-query]]
[auto-ap.logging :as alog]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.hx :as hx]
@@ -10,7 +11,9 @@
[clojure.string :as str]
[datomic.api :as dc]
[hiccup2.core :as hiccup]
[iol-ion.query :refer [can-see-client?]]))
[iol-ion.query :refer [can-see-client?]]
[clojure.data.json :as json]))
(defn dropdown-search-results* [{:keys [options]}]
[:ul
@@ -25,6 +28,7 @@
:request-method :put)
:hx-target "#company-dropdown"
:hx-headers (hx/json {"x-clients" (pr-str [:group group])})
"@click" (format "globalClientSelection={group: %s}" (hx/json group))
:hx-swap "outerHTML"
:hx-trigger "click"}
name]
@@ -34,6 +38,7 @@
:request-method :put)
:hx-target "#company-dropdown"
:hx-headers (format "{\"x-clients\": \"[%d]\"}" id)
"@click" (format "globalClientSelection={selected: [%d]}" id)
:hx-swap "outerHTML"
:hx-trigger "click"}
name])]])])
@@ -64,11 +69,18 @@
(dropdown-search-results* {:options (get-clients identity (get (:query-params request) "search-text"))})))
(defn dropdown [{:keys [client-selection client identity clients]}]
(alog/peek ::clients clients)
[:div#company-dropdown
[:script
(hiccup/raw
"localStorage.setItem(\"last-client-id\", \"" (:db/id client) "\")" "\n"
"localStorage.setItem(\"last-selected-clients\", " (pr-str (pr-str client-selection)) ")")]
"localStorage.setItem(\"last-selected-clients\", " (json/write-str (json/write-str client-selection))
#_(cond (:group client-selection)
(:group client-selection)
(:selected client-selection)
(:selected client-selection)
:else
client-selection) ")")]
[:div
[:button#company-dropdown-button {:class "text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2.5 text-center inline-flex items-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800"
:type "button"}
@@ -80,7 +92,7 @@
(and client
(= 1 (count clients)))
( :client/name client)
(:client/name client)
:else
(str (count clients) " Companies"))
@@ -116,6 +128,8 @@
:active-client
:request-method :put)
:hx-target "#company-dropdown"
"@click" "globalClientSelection=\"mine\""
:hx-headers "{\"x-clients\": \":mine\"}"
:hx-swap "outerHTML"
:hx-trigger "click"}
@@ -127,6 +141,7 @@
:active-client
:request-method :put)
:hx-target "#company-dropdown"
"@click" "globalClientSelection=\"all\""
:hx-headers "{\"x-clients\": \":all\"}"
:hx-swap "outerHTML"
:hx-trigger "click"}
@@ -161,7 +176,7 @@ function initCompanyDropdown() {
(defn active-client [{:keys [identity params] :as request}]
(assoc
(html-response
(dropdown {:client-selection (:client-selection (:session request))
(dropdown {:client-selection (:client-selection request)
:clients (:clients request)
:client (:client request)
:identity identity}))

View File

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

View File

@@ -1,26 +1,28 @@
(ns auto-ap.ssr.components.date-range
(:require [auto-ap.ssr.components :as com]
[auto-ap.time :as atime]))
[auto-ap.time :as atime]
[clj-time.coerce :as c]
[clj-time.core :as t]
[clj-time.periodic :as per]))
(defn date-range-field [{:keys [value id] }]
(defn date-range-field [{:keys [value id]}]
[:div {:id id}
(com/field {:label "Date Range"}
[:div.space-y-4
[:div.space-y-4
[:div
(com/button-group {:name "date-range"}
(com/button-group-button {:size :small :value "all" :hx-trigger "click"} "All")
(com/button-group-button {:size :small :value "week" :hx-trigger "click"} "Week")
(com/button-group-button {:size :small :value "month" :hx-trigger "click"} "Month")
(com/button-group-button {:size :small :value "year" :hx-trigger "click"} "Year"))
]
(com/button-group-button {:size :small :value "year" :hx-trigger "click"} "Year"))]
[:div.flex.space-x-1.items-baseline.w-full.justify-start
(com/date-input {:name "start-date"
:value (some-> (:start value)
(atime/unparse-local atime/normal-date))
(atime/unparse-local atime/normal-date))
:placeholder "Date"
:size :small
:class "shrink"})
(com/date-input {:name "end-date"
:value (some-> (:end value)
(atime/unparse-local atime/normal-date))

View File

@@ -14,12 +14,12 @@
(com/a-icon-button {:x-ref "link" "@click.prevent" "show=!show; $nextTick(() => popper.update());" :class "relative"}
svg/paperclip
(com/badge {} (count links)))
(com/badge {:color "blue"} (count links)))
[:div.divide-y.divide-gray-200.bg-white.rounded-lg.shadow.z-50 (hx/alpine-appear {:x-ref "tooltip" :x-show "show" :data-key "show"})
[:div {:class "p-3 overflow-y-auto text-sm text-gray-700 dark:text-gray-200"}
[:div.flex.flex-col.gap-y-2
(for [l links]
[:div.flex-initial
[:a {:href (:link l)}
[:a {:href (:link l) :target "_blank"}
(com/pill {:color (or (:color l) :primary) :class "truncate block shrink grow-0"}
(:content l))]])]]]]))

View File

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

View File

@@ -21,4 +21,9 @@
children))
(defn badge- [params & children]
[:div {:class (hh/add-class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-black text-white bg-red-300 border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900" (:class params))} children])
[:div {:class (-> (hh/add-class "absolute inline-flex items-center justify-center w-6 h-6 text-xs font-black text-white
border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900"
(:class params)
)
(hh/add-class (or (some-> (:color params) (#(str "bg-" % "-300")))
"bg-red-300")))} children])

View File

@@ -27,7 +27,8 @@
[:li
[:a {:href (bidi/path-for ssr-routes/only-routes :company), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "My Company"]]
(when (= "admin" (:user/role identity))
[:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page), :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"])
[:a {:href (bidi/path-for ssr-routes/only-routes :auto-ap.routes.admin/page),
:class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"} "Admin"])
[:li
[:a {:href "#", :class "block px-4 py-2 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white", :role "menuitem"
"_" (hiccup/raw "on click toggle .dark on <body />")}

View File

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

View File

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

View File

@@ -10,6 +10,7 @@
[auto-ap.routes.utils
:refer [wrap-client-redirect-unauthenticated]]
[auto-ap.rule-matching :as rm]
[auto-ap.client-routes :as client-routes]
[auto-ap.solr :as solr]
[auto-ap.ssr-routes :as ssr-routes]
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
@@ -229,6 +230,14 @@
:value (fc/field-value)
:content-fn (fn [c] (pull-attr (dc/db conn) :vendor/name c))
:x-model "vendorId"})]))
[:div.mb-4
[:span.text-sm.text-gray-500 "Can't find the vendor? "
(com/link {:href (bidi.bidi/path-for
client-routes/routes
:new-vendor)
:target "new"}
"Add new vendor")
" in a new window, then return here."]]
[:div.flex.items-center.gap-2
@@ -421,17 +430,36 @@
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
(defn invoice-expense-account-total* [request]
(format "$%,.2f" (->> (-> request
:multi-form-state
:step-params
:invoice/expense-accounts)
(map (fnil :invoice-expense-account/amount 0.0))
(filter number?)
(reduce + 0.0))))
(let [total (->> (-> request
:multi-form-state
:step-params
:invoice/expense-accounts)
(map (fnil :invoice-expense-account/amount 0.0))
(filter number?)
(reduce + 0.0))]
(format "$%,.2f" total)))
(defn invoice-expense-account-balance* [request]
(let [total (->> (-> request
:multi-form-state
:step-params
:invoice/expense-accounts)
(map (fnil :invoice-expense-account/amount 0.0))
(filter number?)
(reduce + 0.0))
balance (-
(-> request :multi-form-state :snapshot :invoice/total)
total)]
[:span {:class (when-not (dollars= 0.0 balance)
"text-red-300")}
(format "$%,.2f" balance)]))
(defn invoice-expense-account-total [request]
(html-response (invoice-expense-account-total* request)))
(defn invoice-expense-account-balance [request]
(html-response (invoice-expense-account-balance* request)))
(defrecord AccountsStep [linear-wizard]
mm/ModalWizardStep
(step-name [_]
@@ -481,6 +509,19 @@
:hx-swap "innerHTML"}
(invoice-expense-account-total* request))
(com/data-grid-cell {}))
(com/data-grid-row {}
(com/data-grid-cell {})
(com/data-grid-cell {:class "text-right"} [:span.font-bold.text-right "BALANCE"])
(com/data-grid-cell {:id "total"
:class "text-right"
:hx-trigger "change from:closest form target:.amount-field"
:hx-put (bidi.bidi/path-for ssr-routes/only-routes ::route/expense-account-balance)
:hx-target "this"
:hx-swap "innerHTML"}
(invoice-expense-account-balance* request))
(com/data-grid-cell {}))
(com/data-grid-row {}
(com/data-grid-cell {})
@@ -592,12 +633,12 @@
(when (seq eas)
(let [leftover (- invoice-total (reduce + 0 (map :invoice-expense-account/amount eas)))
leftover-beyond-a-single-cent? (or (< leftover -1)
(> leftover 1))
(> leftover 1))
leftover (if leftover-beyond-a-single-cent?
0
leftover)
[first-eas & rest] eas]
(cons
(cons
(update first-eas :invoice-expense-account/amount #(+ % leftover))
rest))))
@@ -813,6 +854,9 @@
::route/expense-account-total (-> invoice-expense-account-total
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state))
::route/expense-account-balance (-> invoice-expense-account-balance
(mm/wrap-wizard new-wizard)
(mm/wrap-decode-multi-form-state))
::route/location-select (-> location-select
(wrap-schema-enforce :query-schema [:map
[:name :string]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,7 @@
"hx-target" "#user-table"
"hx-indicator" "#user-table"}
[:fieldset.space-y-6
[:fieldset.space-y-6
(com/field {:label "Name"}
(com/text-input {:name "name"
:id "name"
@@ -57,6 +57,16 @@
:placeholder "Johnny Testerson"
:size :small}))
(com/field {:label "Client"}
(com/typeahead {:name "client"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)
:id (str "client-search")
:value (:client (:parsed-query-params request))
:value-fn :db/id
:content-fn :client/name}))
(com/field {:label "Email"}
(com/text-input {:name "email"
:id "email"
@@ -66,31 +76,22 @@
:size :small}))
(com/field {:label "Role"}
(com/radio-card {:size :small
:name "role"
:options [{:value ""
:content "All"}
{:value "admin"
:content "Admin"}
{:value "power-user"
:content "Power user"}
{:value "manager"
:content "Manager"}
{:value "user"
:content "User"}
{:value "read-only"
:content "Read Only"}
{:value "none"
:content "None"}]}))
(com/field {:label "Client"}
(com/typeahead {:name "client"
:placeholder "Search..."
:url (bidi/path-for ssr-routes/only-routes
:company-search)
:id (str "client-search")
:value (:client (:parsed-query-params request))
:value-fn :db/id
:content-fn :client/name}))]])
(com/radio-card {:size :small
:name "role"
:options [{:value ""
:content "All"}
{:value "admin"
:content "Admin"}
{:value "power-user"
:content "Power user"}
{:value "manager"
:content "Manager"}
{:value "user"
:content "User"}
{:value "read-only"
:content "Read Only"}
{:value "none"
:content "None"}]}))]])
(def default-read '[:db/id
:user/name

View File

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

View File

@@ -1,5 +1,6 @@
(ns auto-ap.time
(:require [clj-time.core :as time]
[clj-time.coerce :as coerce]
[clj-time.format :as f]
[auto-ap.logging :as alog]))
@@ -40,7 +41,7 @@
(defn unparse-local [v format]
(try
(f/unparse (f/with-zone (f/formatter format) (time/time-zone-for-id "America/Los_Angeles")) v)
(catch Exception _
nil)))
@@ -52,3 +53,14 @@
d
(recur (time/plus d (time/days 1)))))]
(iterate #(time/plus % (time/days 7)) next-day)))
(defn local-today []
(coerce/in-time-zone (time/now) (time/time-zone-for-id "America/Los_Angeles")))
(defn last-monday []
(loop [current (local-now)]
(if (= 1 (time/day-of-week current))
current
(recur (time/minus current (time/days 1))))))

View File

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

View File

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

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

View File

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

View File

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

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

View File

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

View File

@@ -21,6 +21,6 @@
[:img {:src "/img/logo-big.png"}]
[:div
[:a.button.is-large.is-primary {:href (doto (login-url (get (:query (url/url (.-location js/window))) "redirect-to")) println)} "Login with Google"]]]
[:a.button.is-large.is-primary {:href (login-url (get (:query (url/url (.-location js/window))) "redirect-to"))} "Login with Google"]]]
[:p.has-text-gray
"Copyright Integreat 2018"]]]]]])

View File

@@ -1,2 +1,2 @@
#!/bin/sh
ssh -L 2049:172.31.32.90:2049 3.213.115.86 -L 8983:solr-staging.local:8983 -L 4334:integreat-datomic.local:4334 -L9001:integreat-app-staging.local:9000
ssh -L 2049:172.31.32.90:2049 3.213.115.86 -L 8984:solr-staging.local:8983 -L 4334:integreat-datomic.local:4334 -L9001:integreat-app-staging.local:9000