merged many changes.
This commit is contained in:
@@ -247,6 +247,7 @@
|
||||
:transaction/bank-account [:bank-account/name :bank-account/code :bank-account/yodlee-account-id :db/id :bank-account/locations :bank-account/current-balance]
|
||||
:transaction/vendor [:db/id :vendor/name]
|
||||
:transaction/matched-rule [:db/id :transaction-rule/note]
|
||||
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
|
||||
:transaction/accounts [:transaction-account/amount
|
||||
:db/id
|
||||
:transaction-account/location
|
||||
|
||||
@@ -234,7 +234,7 @@
|
||||
:user
|
||||
{:fields {:id {:type :id}
|
||||
:name {:type 'String}
|
||||
:role {:type 'String}
|
||||
:role {:type :role}
|
||||
:clients {:type '(list :client)}}}
|
||||
|
||||
:account_client_override
|
||||
@@ -415,7 +415,7 @@
|
||||
:edit_user
|
||||
{:fields {:id {:type :id}
|
||||
:name {:type 'String}
|
||||
:role {:type 'String}
|
||||
:role {:type :role}
|
||||
:clients {:type '(list String)}}}
|
||||
|
||||
:add_contact
|
||||
@@ -520,6 +520,11 @@
|
||||
:applicability {:values [{:enum-value :global}
|
||||
{:enum-value :optional}
|
||||
{:enum-value :customized}]}
|
||||
:role {:values [{:enum-value :none}
|
||||
{:enum-value :user}
|
||||
{:enum-value :manager}
|
||||
{:enum-value :power_user}
|
||||
{:enum-value :admin}]}
|
||||
:account_type {:values [{:enum-value :dividend}
|
||||
{:enum-value :expense}
|
||||
{:enum-value :asset}
|
||||
|
||||
@@ -2,25 +2,34 @@
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query]]
|
||||
[auto-ap.graphql.utils :refer [assert-admin assert-present <-graphql ->graphql]]
|
||||
[auto-ap.graphql.utils
|
||||
:refer [->graphql
|
||||
<-graphql
|
||||
assert-admin
|
||||
assert-can-see-client
|
||||
assert-present
|
||||
limited-clients]]
|
||||
[auto-ap.plaid.core :as p]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clj-time.core :as time]
|
||||
[clojure.tools.logging :as log]
|
||||
[com.walmartlabs.lacinia.util :refer [attach-resolvers]]
|
||||
[datomic.api :as d]))
|
||||
|
||||
(defn plaid-link-token [context value args]
|
||||
(assert-admin (:id context))
|
||||
(when-not (:client_id value)
|
||||
(throw (ex-info "Client ID is required" {:validation-error "Client ID is required"})))
|
||||
(assert-can-see-client (:id context) (:client_id value))
|
||||
(let [client-code (:client/code (d/pull (d/db conn) [:client/code] (:client_id value)))]
|
||||
{:token (p/get-link-token client-code)}))
|
||||
|
||||
(defn link-plaid [context value args]
|
||||
(assert-admin (:id context))
|
||||
(when-not (:client_code value)
|
||||
(throw (ex-info "Client not provided" {:validation-error "Client not provided."})))
|
||||
(when-not (:public_token value)
|
||||
(throw (ex-info "Public token not provided" {:validation-error "public token not provided"})))
|
||||
|
||||
(log/info (:id context) (:db/id (d/pull (d/db conn) [:db/id] [:client/code (:client_code value)])))
|
||||
(assert-can-see-client (:id context) (:db/id (d/pull (d/db conn) [:db/id] [:client/code (:client_code value)])))
|
||||
(let [access-token (:access_token (p/exchange-public-token (:public_token value) (:client_code value)))
|
||||
account-result (p/get-accounts access-token )
|
||||
item {:plaid-item/client [:client/code (:client_code value)]
|
||||
@@ -40,7 +49,8 @@
|
||||
:plaid-item/_accounts "plaid-item"}
|
||||
balance (assoc :plaid-account/balance balance)))))
|
||||
(into [item])))
|
||||
{:message (str "Plaid linked successfully. Access Token: " access-token)}))
|
||||
(log/info "Access token was " access-token)
|
||||
{:message (str "Plaid linked successfully.")}))
|
||||
|
||||
|
||||
(def default-read '[:db/id
|
||||
@@ -54,7 +64,6 @@
|
||||
:plaid-account/name]}])
|
||||
|
||||
(defn raw-graphql-ids [db args]
|
||||
(println args)
|
||||
(let [query (cond-> {:query {:find []
|
||||
:in ['$]
|
||||
:where []}
|
||||
@@ -63,6 +72,11 @@
|
||||
(:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]}
|
||||
args)
|
||||
|
||||
(limited-clients (:id args))
|
||||
(merge-query {:query {:in ['[?xx ...]]
|
||||
:where ['[?e :plaid-item/client ?xx]]}
|
||||
:args [ (set (map :db/id (limited-clients (:id args))))]})
|
||||
|
||||
(:client-id args)
|
||||
(merge-query {:query {:in '[?client-id]
|
||||
:where ['[?e :plaid-item/client ?client-id]]}
|
||||
@@ -93,7 +107,7 @@
|
||||
|
||||
|
||||
(defn get-plaid-item-page [context args value]
|
||||
(assert-admin (:id context))
|
||||
|
||||
(let [args (assoc args :id (:id context))
|
||||
[plaid-items cnt] (get-graphql (<-graphql (assoc args :id (:id context))))]
|
||||
{:plaid_items (->> plaid-items
|
||||
|
||||
@@ -1,59 +1,49 @@
|
||||
(ns auto-ap.graphql.transaction-rules
|
||||
(:require [auto-ap.datomic
|
||||
:refer
|
||||
[audit-transact merge-query remove-nils replace-nils-with-retract uri conn]]
|
||||
[auto-ap.datomic.transaction-rules :as tr]
|
||||
[auto-ap.datomic.transactions :as d-transactions]
|
||||
[auto-ap.graphql.utils
|
||||
:refer
|
||||
[->graphql
|
||||
<-graphql
|
||||
assert-admin
|
||||
ident->enum-f
|
||||
limited-clients
|
||||
result->page
|
||||
snake->kebab]]
|
||||
[auto-ap.rule-matching :as rm]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[clj-time.coerce :as coerce]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[clojure.tools.logging :as log]
|
||||
[datomic.api :as d]
|
||||
[clj-time.coerce :as c])
|
||||
(:import java.time.temporal.ChronoField))
|
||||
(:require
|
||||
[auto-ap.datomic
|
||||
:refer [audit-transact
|
||||
conn
|
||||
merge-query
|
||||
remove-nils
|
||||
replace-nils-with-retract]]
|
||||
[auto-ap.datomic.transaction-rules :as tr]
|
||||
[auto-ap.datomic.transactions :as d-transactions]
|
||||
[auto-ap.graphql.utils
|
||||
:refer [->graphql
|
||||
<-graphql
|
||||
assert-admin
|
||||
ident->enum-f
|
||||
limited-clients
|
||||
result->page
|
||||
snake->kebab]]
|
||||
[auto-ap.rule-matching :as rm]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[clj-time.coerce :as c]
|
||||
[clojure.set :as set]
|
||||
[clojure.string :as str]
|
||||
[datomic.api :as d]))
|
||||
|
||||
(defn get-transaction-rule-page [context args value]
|
||||
(defn get-transaction-rule-page [context args _]
|
||||
(let [args (assoc args :id (:id context))
|
||||
[journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))]
|
||||
(result->page (->> journal-entries
|
||||
(map (ident->enum-f :transaction-rule/transaction-approval-status)))
|
||||
journal-entries-count :transaction_rules args)))
|
||||
|
||||
(defn get-transaction-rule-matches [context args value]
|
||||
(defn get-transaction-rule-matches [context args _]
|
||||
(if (= "admin" (:user/role (:id context)))
|
||||
(let [all-rules (tr/get-all)
|
||||
transaction (update (d-transactions/get-by-id (:transaction_id args)) :transaction/date coerce/to-date)]
|
||||
transaction (update (d-transactions/get-by-id (:transaction_id args)) :transaction/date c/to-date)]
|
||||
(map ->graphql (rm/get-matching-rules transaction all-rules)))
|
||||
nil))
|
||||
|
||||
(defn deleted-accounts [transaction accounts]
|
||||
(let [current-accounts (:transaction-rule/accounts transaction)
|
||||
specified-ids (->> accounts
|
||||
(map :id)
|
||||
set)
|
||||
existing-ids (->> current-accounts
|
||||
(map :db/id)
|
||||
set)]
|
||||
(set/difference existing-ids specified-ids)))
|
||||
|
||||
(defn transaction-rule-account->entity [{:keys [id account_id percentage location]}]
|
||||
(remove-nils #:transaction-rule-account {:percentage percentage
|
||||
:db/id id
|
||||
:account account_id
|
||||
:location location}))
|
||||
|
||||
(defn delete-transaction-rule [context {:keys [transaction_rule_id ]} value]
|
||||
(defn delete-transaction-rule [context {:keys [transaction_rule_id ]} _]
|
||||
(assert-admin (:id context))
|
||||
(let [existing-transaction-rule (tr/get-by-id transaction_rule_id)]
|
||||
(when-not (:transaction-rule/description existing-transaction-rule)
|
||||
@@ -62,10 +52,9 @@
|
||||
(audit-transact [[:db/retractEntity transaction_rule_id]] (:id context))
|
||||
transaction_rule_id))
|
||||
|
||||
(defn upsert-transaction-rule [context {{:keys [id description yodlee_merchant_id note client_id bank_account_id amount_lte amount_gte vendor_id accounts transaction_approval_status dom_gte dom_lte]} :transaction_rule :as z} value]
|
||||
(defn upsert-transaction-rule [context {{:keys [id description yodlee_merchant_id note client_id bank_account_id amount_lte amount_gte vendor_id accounts transaction_approval_status dom_gte dom_lte]} :transaction_rule} _]
|
||||
(assert-admin (:id context))
|
||||
(let [existing-transaction (tr/get-by-id id)
|
||||
deleted (deleted-accounts existing-transaction accounts)
|
||||
account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts))
|
||||
_ (when-not (dollars= 1.0 account-total)
|
||||
(let [error (str "Account total (" account-total ") does not reach 100%")]
|
||||
@@ -75,7 +64,7 @@
|
||||
(let [error (str "You must provide a description or a yodlee merchant")]
|
||||
(throw (ex-info error {:validation-error error}))))
|
||||
_ (doseq [a accounts
|
||||
:let [{:keys [:account/location :account/name] :as account} (d/entity (d/db conn) (:account_id a))
|
||||
:let [{:keys [:account/location :account/name]} (d/entity (d/db conn) (:account_id a))
|
||||
client (d/entity (d/db conn) client_id)
|
||||
]]
|
||||
(when (and location (not= location (:location a)))
|
||||
@@ -120,7 +109,7 @@
|
||||
(defn tr [z x]
|
||||
(re-find (re-pattern z) x))
|
||||
|
||||
(defn -test-transaction-rule [id {:keys [:transaction-rule/description :transaction-rule/note :transaction-rule/client :transaction-rule/bank-account :transaction-rule/amount-lte :transaction-rule/amount-gte :transaction-rule/dom-lte :transaction-rule/dom-gte :transaction-rule/yodlee-merchant]} include-coded? count]
|
||||
(defn -test-transaction-rule [id {:keys [:transaction-rule/description :transaction-rule/client :transaction-rule/bank-account :transaction-rule/amount-lte :transaction-rule/amount-gte :transaction-rule/dom-lte :transaction-rule/dom-gte :transaction-rule/yodlee-merchant]} include-coded? count]
|
||||
(->>
|
||||
(d/query
|
||||
(cond-> {:query {:find ['(pull ?e [* {:transaction/client [:client/name]
|
||||
@@ -204,7 +193,7 @@
|
||||
(map ->graphql))
|
||||
conj [])))
|
||||
|
||||
(defn test-transaction-rule [{:keys [id]} {{:keys [description note client_id bank_account_id amount_lte amount_gte dom_lte dom_gte yodlee_merchant_id]} :transaction_rule :as z} value]
|
||||
(defn test-transaction-rule [{:keys [id]} {{:keys [description client_id bank_account_id amount_lte amount_gte dom_lte dom_gte yodlee_merchant_id]} :transaction_rule} _]
|
||||
(assert-admin id)
|
||||
(-test-transaction-rule id #:transaction-rule {:description description
|
||||
:client (when client_id {:db/id client_id})
|
||||
@@ -217,6 +206,6 @@
|
||||
true 15))
|
||||
|
||||
|
||||
(defn run-transaction-rule [{:keys [id]} {:keys [transaction_rule_id count]} value]
|
||||
(defn run-transaction-rule [{:keys [id]} {:keys [transaction_rule_id count]} _]
|
||||
(assert-admin id)
|
||||
(-test-transaction-rule id (tr/get-by-id transaction_rule_id) false count))
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
[auto-ap.datomic.users :as d-users]
|
||||
[auto-ap.graphql.utils :refer [->graphql assert-admin]]))
|
||||
|
||||
(def role->datomic-role {":none" :user-role/none
|
||||
":admin" :user-role/admin
|
||||
":power_user" :user-role/power-user
|
||||
":manager" :user-role/manager
|
||||
":user" :user-role/user})
|
||||
(def role->datomic-role {:none :user-role/none
|
||||
:admin :user-role/admin
|
||||
:power_user :user-role/power-user
|
||||
:manager :user-role/manager
|
||||
:user :user-role/user})
|
||||
|
||||
(defn edit-user [context {:keys [edit_user] :as args} value]
|
||||
(assert-admin (:id context))
|
||||
|
||||
@@ -128,7 +128,7 @@
|
||||
(if (str/includes? q "&")
|
||||
(str "\"" q "\"~0.8")
|
||||
(let [parts (-> q
|
||||
(str/replace #"[\[\]\+\*]" "")
|
||||
(str/replace #"[\[\]\+\*\-]" "")
|
||||
(str/split #"\s+"))
|
||||
exacts (butlast parts)
|
||||
partial (last parts)]
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
|
||||
(add-shutdown-hook! shutdown-mount)
|
||||
(start-server :port 9000 :bind "0.0.0.0" #_#_:handler (cider-nrepl-handler))
|
||||
(alter-var-root #'nrepl.middleware.print/*print-fn* (constantly clojure.pprint/pprint))
|
||||
#_(alter-var-root #'nrepl.middleware.print/*print-fn* (constantly clojure.pprint/pprint))
|
||||
(apply mount/start-without without)))
|
||||
|
||||
(comment
|
||||
|
||||
@@ -14,11 +14,9 @@
|
||||
"rules" :admin-rules
|
||||
"accounts" :admin-accounts
|
||||
"import-batches" :admin-import-batches
|
||||
"reminders" :admin-reminders
|
||||
"vendors" :admin-vendors
|
||||
"excel-import" :admin-excel-import
|
||||
"yodlee2" :admin-yodlee2
|
||||
"plaid" :admin-plaid}
|
||||
"yodlee2" :admin-yodlee2}
|
||||
"invoices/" {"" :invoices
|
||||
"import" :import-invoices
|
||||
"unpaid" :unpaid-invoices
|
||||
@@ -33,6 +31,7 @@
|
||||
"requires-feedback" :requires-feedback-transactions
|
||||
"excluded" :excluded-transactions}
|
||||
"reports/" {"" :reports}
|
||||
"plaid" :plaid
|
||||
"ledger/" {"" :ledger
|
||||
"profit-and-loss" :profit-and-loss
|
||||
"balance-sheet" :balance-sheet
|
||||
|
||||
@@ -2,21 +2,4 @@
|
||||
(:require [clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.shared :as shared]))
|
||||
|
||||
(s/def ::vendor map?)
|
||||
(s/def ::vendor-name string?)
|
||||
(s/def ::client map?)
|
||||
(s/def ::invoice-number ::shared/required-identifier)
|
||||
(s/def ::date ::shared/date)
|
||||
(s/def ::due (s/nilable ::shared/date))
|
||||
(s/def ::scheduled-payment (s/nilable ::shared/date))
|
||||
(s/def ::total ::shared/money)
|
||||
|
||||
(s/def ::invoice (s/keys :req-un [::client
|
||||
::invoice-number
|
||||
::date
|
||||
::vendor
|
||||
::total]
|
||||
:opt-un [::vendor-name
|
||||
::due
|
||||
::scheduled-payment
|
||||
]))
|
||||
|
||||
@@ -19,6 +19,9 @@
|
||||
:ldt? #(instance? org.joda.time.LocalDate %)
|
||||
:str? (s/and string? #(re-matches date-regex %)))))
|
||||
|
||||
(s/def ::required some?)
|
||||
(s/def ::has-id (s/and map?
|
||||
#(:id %)))
|
||||
(s/def ::required-identifier (s/and string?
|
||||
#(not (str/blank? %))))
|
||||
|
||||
|
||||
@@ -530,8 +530,8 @@
|
||||
[title a b (and (:value a) (:value b)
|
||||
{:border (:border b)
|
||||
:format :dollar
|
||||
:value (- (:value a)
|
||||
(:value b))})]))))
|
||||
:value (- (or (:value a) 0.0)
|
||||
(or (:value b) 0.0))})]))))
|
||||
|
||||
(defn summarize-balance-sheet [pnl-data]
|
||||
(let [pnl-datas (map (fn [p]
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
(re-frame/reg-fx
|
||||
:redirect
|
||||
(fn [uri]
|
||||
(pushy/set-token! p/history uri)))
|
||||
(pushy/set-token! p/history uri)
|
||||
(p/dispatch-route (p/parse-url uri))))
|
||||
|
||||
(re-frame/reg-fx
|
||||
:set-uri-params
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
(:require
|
||||
[auto-ap.db :as db]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.utils :refer [with-user]]
|
||||
[auto-ap.views.utils :refer [with-user parse-jwt]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.string :as str]
|
||||
[goog.crypt.base64 :as b64]
|
||||
[re-frame.core :as re-frame]))
|
||||
[re-frame.core :as re-frame]
|
||||
[goog.crypt.base64 :as base64]))
|
||||
|
||||
(defn jwt->data [token]
|
||||
(js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." ))))))
|
||||
@@ -142,15 +142,26 @@
|
||||
(re-frame/reg-event-fx
|
||||
::set-active-route
|
||||
(fn [{:keys [db]} [_ handler params route-params]]
|
||||
|
||||
(cond
|
||||
(and (not= :login handler) (not (:user db)))
|
||||
{:redirect (bidi/path-for routes/routes :login)
|
||||
:db (assoc db :active-route :login
|
||||
:active-page :login
|
||||
:menu nil
|
||||
:page-failure nil)}
|
||||
|
||||
(if (and (not= :login handler) (not (:user db)))
|
||||
{:redirect "/login"
|
||||
:db (assoc db :active-route :login
|
||||
:page-failure nil)}
|
||||
(and (not= "admin" (:user/role (parse-jwt (:user db))))
|
||||
(str/includes? (name handler) "admin"))
|
||||
{:redirect (bidi/path-for routes/routes :index)
|
||||
:db (assoc db :active-route :index
|
||||
:active-page :index
|
||||
:menu nil
|
||||
:page-failure nil)}
|
||||
:else
|
||||
{:db (-> db
|
||||
(assoc :active-route handler
|
||||
:page-failure nil
|
||||
:menu nil
|
||||
:query-params params
|
||||
:route-params route-params)
|
||||
(auto-ap.views.pages.data-page/dispose-all))})))
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
(ns auto-ap.events.admin.reminders
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[auto-ap.db :as db]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.effects :as effects]))
|
||||
|
||||
|
||||
@@ -2,13 +2,16 @@
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[re-frame.interceptor :as i]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.utils :refer [dispatch-event bind-field]]))
|
||||
[auto-ap.views.utils :refer [dispatch-event]]
|
||||
[malli.core :as m]))
|
||||
|
||||
|
||||
(re-frame/reg-sub
|
||||
::form
|
||||
(fn [db [_ x]]
|
||||
(get (-> db ::forms) x)))
|
||||
(update (get (-> db ::forms) x)
|
||||
:visited (fn [v]
|
||||
(or v #{})))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::field
|
||||
@@ -28,12 +31,20 @@
|
||||
([db form data]
|
||||
(start-form db form data nil))
|
||||
([db form data complete-listener]
|
||||
(assoc-in db [::forms form] {:error nil
|
||||
:active? true
|
||||
:id (random-uuid)
|
||||
:status nil
|
||||
:data data
|
||||
:complete-listener complete-listener})))
|
||||
(-> db
|
||||
(assoc-in [::forms form] {:error nil
|
||||
:active? true
|
||||
:id (random-uuid)
|
||||
:visited #{}
|
||||
:status nil
|
||||
:data data
|
||||
:complete-listener complete-listener})
|
||||
(assoc-in [::status/status form] nil))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::start-form
|
||||
(fn [db [_ id data]]
|
||||
(start-form db id data)))
|
||||
|
||||
(defn triggers-saved [form data-key]
|
||||
(i/->interceptor
|
||||
@@ -75,6 +86,28 @@
|
||||
db
|
||||
(partition 2 path-pairs))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::reset
|
||||
(fn [db [_ form v]]
|
||||
(assoc-in db [::forms form :data] v)))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::visited
|
||||
(fn [db [_ form & paths]]
|
||||
(update-in db [::forms form :visited] (fn [v]
|
||||
(set (into v paths))))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::check-problems
|
||||
(fn [db [_ form schema]]
|
||||
(assoc-in db [::forms form :problems]
|
||||
(when schema (m/explain schema (get-in db [::forms form :data]))))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::attempted-submit
|
||||
(fn [db [_ form & paths]]
|
||||
(assoc-in db [::forms form :attempted-submit?] true)))
|
||||
|
||||
(defn change-handler [form customize-fn]
|
||||
(fn [db [_ & path-pairs]]
|
||||
|
||||
@@ -92,6 +125,7 @@
|
||||
(re-frame/reg-event-db
|
||||
::save-error
|
||||
(fn [db [_ form result]]
|
||||
(println result)
|
||||
(-> db
|
||||
(assoc-in [::forms form :status] :error)
|
||||
(assoc-in [::forms form :error] (or (:message (first result))
|
||||
@@ -138,68 +172,3 @@
|
||||
(assoc-in [::forms id :error] nil)))
|
||||
|
||||
|
||||
(defn vertical-form [{:keys [can-submit id change-event submit-event fullwidth?] :or {fullwidth? true}}]
|
||||
{:form
|
||||
(fn [{:keys [title] :as params} & children]
|
||||
(let [{:keys [data active? error]} @(re-frame/subscribe [::form id])
|
||||
can-submit @(re-frame/subscribe can-submit)]
|
||||
|
||||
[:form { :on-submit (fn [e]
|
||||
(when (.-stopPropagation e)
|
||||
(.stopPropagation e)
|
||||
(.preventDefault e))
|
||||
(when can-submit
|
||||
(re-frame/dispatch-sync (vec (conj submit-event params)))))}
|
||||
[:h1.title.is-2 title]
|
||||
[:<>
|
||||
children]]))
|
||||
|
||||
:form-inline
|
||||
(fn [{:keys [title] :as params} children]
|
||||
(let [{:keys [data active? error]} @(re-frame/subscribe [::form id])
|
||||
can-submit @(re-frame/subscribe can-submit)]
|
||||
|
||||
[:form { :on-submit (fn [e]
|
||||
(when (.-stopPropagation e)
|
||||
(.stopPropagation e)
|
||||
(.preventDefault e))
|
||||
(when can-submit
|
||||
(re-frame/dispatch-sync (vec (conj submit-event params)))))}
|
||||
(when title
|
||||
[:h1.title.is-2 title])
|
||||
children]))
|
||||
:raw-field (fn [control]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::form id])]
|
||||
[bind-field (-> control
|
||||
(assoc-in [1 :subscription] data)
|
||||
(assoc-in [1 :event] change-event))]))
|
||||
:field-holder (fn [label control]
|
||||
[:div.field
|
||||
(when label (if fullwidth? [:p.help label]
|
||||
[:label.label label]))
|
||||
[:div.control control]])
|
||||
:field ^{:key "field"}
|
||||
(fn [label control]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::form id])]
|
||||
[:div.field
|
||||
(when label (if fullwidth? [:p.help label]
|
||||
[:label.label label]))
|
||||
[:div.control [bind-field (-> control
|
||||
(assoc-in [1 :subscription] data)
|
||||
(assoc-in [1 :event] change-event))]]]))
|
||||
|
||||
:error-notification
|
||||
(fn []
|
||||
(when-let [error (:error @(re-frame/subscribe [::form id]))]
|
||||
^{:key error}
|
||||
[:div.has-text-danger.animated.fadeInUp {} error]))
|
||||
:submit-button (fn [child]
|
||||
(let [error (:error @(re-frame/subscribe [::form id]))
|
||||
status @(re-frame/subscribe [::status/single id])
|
||||
can-submit @(re-frame/subscribe can-submit)]
|
||||
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
|
||||
(not can-submit))
|
||||
:class (cond-> (status/class-for status)
|
||||
fullwidth? (conj "is-fullwidth")) }
|
||||
child]))})
|
||||
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
(ns auto-ap.forms.builder
|
||||
(:require
|
||||
[auto-ap.views.utils :refer [bind-field]]
|
||||
[re-frame.core :as re-frame]
|
||||
[react :as react]
|
||||
[reagent.core :as r]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]))
|
||||
[auto-ap.status :as status]
|
||||
[malli.core :as m]
|
||||
[malli.error :as me]))
|
||||
|
||||
(defonce ^js/React.Context form-context (react/createContext "default"))
|
||||
(def ^js/React.Provider Provider (. form-context -Provider))
|
||||
@@ -15,104 +16,231 @@
|
||||
(def ^js/React.Provider FormScopeProvider (. form-scope-context -Provider))
|
||||
(def ^js/React.Consumer FormScopeConsumer (. form-scope-context -Consumer))
|
||||
|
||||
(defn builder [{:keys [can-submit data-sub change-event submit-event id fullwidth?] :as z}]
|
||||
(let [data-sub (or data-sub [::forms/form id])
|
||||
change-event (or change-event [::forms/change id])
|
||||
{:keys [data error] form-key :id} @(re-frame/subscribe data-sub)
|
||||
status @(re-frame/subscribe [::status/single id])]
|
||||
(r/create-element Provider #js {:value #js {:can-submit @(re-frame/subscribe can-submit)
|
||||
:change-event change-event
|
||||
:submit-event submit-event
|
||||
:error error
|
||||
:status status
|
||||
:id id
|
||||
:data data
|
||||
:fullwidth? fullwidth?}}
|
||||
(defn valid-field? [problems field-path]
|
||||
(not (get-in (me/humanize problems) field-path)))
|
||||
|
||||
(defn spec-error-message [problems field-path error-messages]
|
||||
(-> (me/humanize problems
|
||||
{:errors (merge (-> me/default-errors
|
||||
(assoc ::m/missing-key {:error/message "Required"}
|
||||
::m/invalid-type {:error/fn
|
||||
(fn [a b]
|
||||
(if (nil? (:value a))
|
||||
"Required"
|
||||
"Invalid"))}))
|
||||
error-messages)})
|
||||
(get-in field-path)
|
||||
first))
|
||||
|
||||
(defn consume [consumer-component fields f]
|
||||
[:> consumer-component {}
|
||||
(fn [consumed]
|
||||
(r/as-element
|
||||
(apply f (for [field fields]
|
||||
(aget consumed field)))))])
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::blurred
|
||||
(fn [_ [_ schema id field]]
|
||||
{:dispatch-n [[::forms/check-problems id schema]
|
||||
[::forms/visited id field]]}))
|
||||
|
||||
(defn builder [{:keys [value on-change can-submit data-sub error-messages change-event submit-event id fullwidth? schema validation-error-string]}]
|
||||
(when (and change-event on-change)
|
||||
(throw "Either the form is to be managed by ::forms, or it should have value and on-change passed in"))
|
||||
(let [data-sub (or data-sub [::forms/form id])
|
||||
change-event (when-not on-change
|
||||
(or change-event [::forms/change id]))
|
||||
{:keys [data visited attempted-submit? problems error] form-key :id} @(re-frame/subscribe data-sub)
|
||||
data (or value data)
|
||||
status @(re-frame/subscribe [::status/single id])
|
||||
can-submit (if can-submit @(re-frame/subscribe can-submit)
|
||||
true)]
|
||||
(r/create-element Provider #js {:value #js {:can-submit can-submit
|
||||
:error-messages (or error-messages
|
||||
nil)
|
||||
:on-change on-change
|
||||
:change-event change-event
|
||||
:blur-event [::blurred schema id]
|
||||
:visited visited
|
||||
:submit-event submit-event
|
||||
:problems problems
|
||||
:attempted-submit? attempted-submit?
|
||||
:error (or error (-> status :error first :message))
|
||||
:status status
|
||||
:id id
|
||||
:data data
|
||||
:fullwidth? fullwidth?}}
|
||||
(r/as-element
|
||||
^{:key form-key}
|
||||
[:form {:on-submit (fn [e]
|
||||
(when (.-stopPropagation e)
|
||||
(.stopPropagation e)
|
||||
(.preventDefault e))
|
||||
(when can-submit
|
||||
(re-frame/dispatch-sync (vec (conj submit-event {})))))}
|
||||
(if (and schema (not (m/validate schema data)))
|
||||
(do
|
||||
(re-frame/dispatch-sync [::status/dispose-single id])
|
||||
(re-frame/dispatch [::status/error id [{:message (or validation-error-string "Please fix the errors and try again.")}]])
|
||||
(re-frame/dispatch [::forms/attempted-submit id]))
|
||||
(when can-submit
|
||||
(re-frame/dispatch-sync (vec (conj submit-event {}))))))}
|
||||
(into [:fieldset {:disabled (boolean (= :loading (:state status)))}]
|
||||
(r/children (r/current-component)))]
|
||||
(r/children (r/current-component)))]
|
||||
))))
|
||||
;; TODO make virtual builder operate as a cursor and an input instead of a whole new thing
|
||||
;; make it inherit the outer form, avoiding creating new forms
|
||||
(defn virtual-builder []
|
||||
(let [starting-key (random-uuid)
|
||||
key (r/atom starting-key)]
|
||||
(re-frame/dispatch [::forms/start-form starting-key []])
|
||||
(fn [{:keys [value on-change can-submit error-messages fullwidth? schema attempted-submit?]}]
|
||||
(let [data-sub [::forms/form @key]
|
||||
{:keys [data error problems visited]} @(re-frame/subscribe data-sub)
|
||||
data (or value data)]
|
||||
(r/create-element Provider #js {:value #js {:can-submit can-submit
|
||||
:error-messages (or error-messages
|
||||
nil)
|
||||
;; wrap to make sure raw form updates too
|
||||
:on-change (fn [v o]
|
||||
(re-frame/dispatch-sync [::forms/reset @key v])
|
||||
(on-change v o))
|
||||
:blur-event [::blurred schema @key ]
|
||||
:problems problems
|
||||
:attempted-submit? attempted-submit?
|
||||
:visited visited
|
||||
:error error
|
||||
:id @key
|
||||
:data data
|
||||
:fullwidth? fullwidth?}}
|
||||
(r/as-element
|
||||
^{:key @key}
|
||||
(into [:<>]
|
||||
(r/children (r/current-component)))))))))
|
||||
|
||||
(defn raw-field []
|
||||
|
||||
(defn change-handler [path re-frame-change-event event-or-value]
|
||||
(re-frame/dispatch (-> re-frame-change-event
|
||||
(conj path)
|
||||
(conj (if-let [target (some-> event-or-value (aget "target"))]
|
||||
(aget target "value")
|
||||
event-or-value)))))
|
||||
|
||||
(defn form-change-handler [data path on-change event-or-value]
|
||||
(on-change (assoc-in data path (if-let [target (some-> event-or-value (aget "target"))]
|
||||
(aget target "value")
|
||||
event-or-value))
|
||||
data))
|
||||
|
||||
(defn blur-handler [path re-frame-blur-event original-on-blur e]
|
||||
(when original-on-blur
|
||||
(original-on-blur e))
|
||||
|
||||
(re-frame/dispatch (-> re-frame-blur-event
|
||||
(conj path))))
|
||||
|
||||
(defn raw-error-v2 [{:keys [field]}]
|
||||
(consume Consumer
|
||||
["visited" "attempted-submit?" "problems" "error-messages"]
|
||||
(fn [visited attempted-submit? problems error-messages]
|
||||
(consume FormScopeConsumer
|
||||
["scope"]
|
||||
(fn [scope]
|
||||
(let [scope (or scope [])
|
||||
full-field-path (cond
|
||||
(sequential? field)
|
||||
(into scope field)
|
||||
|
||||
field
|
||||
(conj scope field)
|
||||
|
||||
:else
|
||||
nil)
|
||||
visited? (get visited full-field-path)]
|
||||
(when-let [error-message (and
|
||||
(or visited? attempted-submit?)
|
||||
(spec-error-message problems full-field-path error-messages))]
|
||||
[:div
|
||||
[:p.help.has-text-danger error-message]])))))))
|
||||
|
||||
(defn raw-field-v2 [{:keys [field] :as props}]
|
||||
(when-not field
|
||||
(throw (ex-info (str "Missing field") (clj->js {:props props}))))
|
||||
(let [[child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume-form]
|
||||
(r/as-element
|
||||
[:> FormScopeConsumer {}
|
||||
(fn [form-scope]
|
||||
(r/as-element
|
||||
[bind-field (-> child
|
||||
(update-in [1 :field] (fn [f]
|
||||
(cond
|
||||
(sequential? f)
|
||||
(into form-scope f)
|
||||
(consume Consumer
|
||||
["visited" "attempted-submit?" "data" "on-change" "change-event" "blur-event" "problems"]
|
||||
(fn [visited attempted-submit? data on-change change-event blur-event problems]
|
||||
(consume FormScopeConsumer
|
||||
["scope"]
|
||||
(fn [scope]
|
||||
(update child 1 (fn [child-props]
|
||||
(let [scope (or scope [])
|
||||
full-field-path (cond
|
||||
(sequential? field)
|
||||
(into scope field)
|
||||
|
||||
f
|
||||
(conj form-scope f)
|
||||
field
|
||||
(conj scope field)
|
||||
|
||||
:else
|
||||
nil)))
|
||||
|
||||
(assoc-in [1 :subscription] (aget consume-form "data"))
|
||||
(assoc-in [1 :event] (aget consume-form "change-event")))]))]))]))
|
||||
:else
|
||||
nil)
|
||||
visited? (get visited full-field-path)
|
||||
value (get-in data full-field-path)]
|
||||
(-> child-props
|
||||
(assoc :on-change
|
||||
(if on-change
|
||||
(partial form-change-handler data full-field-path on-change)
|
||||
(partial change-handler full-field-path change-event))
|
||||
:on-blur (partial blur-handler full-field-path blur-event (:on-blur child-props))
|
||||
:value value)
|
||||
(update :class (fn [class]
|
||||
(str class
|
||||
(cond
|
||||
(and (not visited?) (not attempted-submit?))
|
||||
""
|
||||
(not (valid-field? problems full-field-path))
|
||||
" is-danger"
|
||||
|
||||
value
|
||||
" is-success"
|
||||
|
||||
:else
|
||||
""))))))))))))))
|
||||
(defn with-scope [{:keys [scope]}]
|
||||
(r/create-element FormScopeProvider #js {:value scope}
|
||||
(r/create-element FormScopeProvider #js {:value #js {:scope scope}}
|
||||
(r/as-element (into [:<>]
|
||||
(r/children (r/current-component))))))
|
||||
|
||||
(defn vertical-control [{:keys [is-small? required?]}]
|
||||
(let [[label & children] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(r/as-element
|
||||
[:div.field
|
||||
(if (aget consume "fullwidth?")
|
||||
[:p.help label]
|
||||
[:label.label
|
||||
(if required?
|
||||
[:span label [:span.has-text-danger " *"]]
|
||||
label)])
|
||||
(into [:div.control ] children)]))]))
|
||||
(consume Consumer
|
||||
["fullwidth?"]
|
||||
(fn [fullwidth?]
|
||||
[:div.field
|
||||
(if fullwidth?
|
||||
[:p.help label]
|
||||
[:label.label
|
||||
(if required?
|
||||
[:span label [:span.has-text-danger " *"]]
|
||||
label)])
|
||||
(into [:div.control ] children)]))))
|
||||
|
||||
(defn field []
|
||||
(let [props (r/props (r/current-component))
|
||||
(defn field-v2 []
|
||||
(let [props (r/props (r/current-component))
|
||||
[label child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(r/as-element
|
||||
[:div.field
|
||||
(when label
|
||||
(if (aget consume "fullwidth?")
|
||||
[:p.help label]
|
||||
[:label.label
|
||||
(if (:required? props)
|
||||
[:span label [:span.has-text-danger " *"]]
|
||||
label)]))
|
||||
[:div.control [raw-field {} child]]]))]))
|
||||
|
||||
(defn horizontal-control []
|
||||
(let [[label & children] (r/children (r/current-component))]
|
||||
[:div.field.is-horizontal
|
||||
(when label
|
||||
[:div.field-label [:label.label label]])
|
||||
[:div.field-body
|
||||
(for [[i child] (map vector (range) children)]
|
||||
^{:key i}
|
||||
[:div.field
|
||||
child])]]))
|
||||
|
||||
(defn horizontal-field []
|
||||
(let [[label child] (r/children (r/current-component))]
|
||||
[horizontal-control
|
||||
label
|
||||
[raw-field {} child]]))
|
||||
(consume Consumer
|
||||
["fullwidth?"]
|
||||
(fn [fullwidth?]
|
||||
[:div.field
|
||||
(when label
|
||||
(if fullwidth?
|
||||
[:p.help label]
|
||||
[:label.label
|
||||
(if (:required? props)
|
||||
[:span label [:span.has-text-danger " *"]]
|
||||
label)]))
|
||||
[:div.control [raw-field-v2 props child]]
|
||||
[:div
|
||||
[raw-error-v2 {:field (:field props)}]]]))))
|
||||
|
||||
(defn section [{:keys [title]}]
|
||||
[:<>
|
||||
@@ -123,36 +251,37 @@
|
||||
|
||||
(defn submit-button [{:keys [class]}]
|
||||
(let [[child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(let [status (aget consume "status")
|
||||
can-submit (aget consume "can-submit")
|
||||
fullwidth? (aget consume "fullwidth?")]
|
||||
(r/as-element
|
||||
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
|
||||
(not can-submit))
|
||||
:class (cond-> (or class [])
|
||||
(status/class-for status) (conj (status/class-for status))
|
||||
fullwidth? (conj "is-fullwidth")) }
|
||||
child])))]))
|
||||
(consume
|
||||
Consumer
|
||||
["status" "can-submit" "fullwidth?"]
|
||||
(fn [status can-submit fullwidth?]
|
||||
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
|
||||
(not can-submit))
|
||||
:class (cond-> (or class [])
|
||||
(status/class-for status) (into (status/class-for status))
|
||||
fullwidth? (conj "is-fullwidth")) }
|
||||
child]))))
|
||||
|
||||
(defn hidden-submit-button []
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(let [status (aget consume "status")
|
||||
can-submit (aget consume "can-submit")]
|
||||
(r/as-element
|
||||
[:div {:style {:display "none"}}
|
||||
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
|
||||
(not can-submit))}]])))])
|
||||
(consume Consumer ["status" "can-submit"]
|
||||
(fn [status can-submit]
|
||||
[:div {:style {:display "none"}}
|
||||
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
|
||||
(not can-submit))}]])))
|
||||
|
||||
(defn error-notification []
|
||||
(let [[child] (r/children (r/current-component))]
|
||||
[:> Consumer {}
|
||||
(fn [consume]
|
||||
(r/as-element
|
||||
(when-let [error (aget consume "error")]
|
||||
^{:key error}
|
||||
[:div.has-text-danger.animated.fadeInUp {} error])))]))
|
||||
(consume Consumer ["error" "status"]
|
||||
(fn [error status]
|
||||
(println status)
|
||||
(cond error
|
||||
^{:key error}
|
||||
[:div.has-text-danger.animated.fadeInUp {} error]
|
||||
|
||||
(-> status :error first :message)
|
||||
[:div.has-text-danger.animated.fadeInUp {} (-> status :error first :message)]
|
||||
|
||||
(-> status :error)
|
||||
[:div.has-text-danger.animated.fadeInUp {} (-> status :error str)]
|
||||
|
||||
:else
|
||||
nil))))
|
||||
|
||||
@@ -6,11 +6,11 @@
|
||||
[cemerick.url :refer [url]]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(defn- parse-url [url]
|
||||
(defn parse-url [url]
|
||||
(println "parsing url" url)
|
||||
(bidi/match-route routes/routes url))
|
||||
|
||||
(defn- dispatch-route [matched-route]
|
||||
(defn dispatch-route [matched-route]
|
||||
(println "Matched route" matched-route)
|
||||
(re-frame/dispatch [:auto-ap.events/set-active-route (:handler matched-route) (u/query-params) (:route-params matched-route)]))
|
||||
|
||||
|
||||
26
src/cljs/auto_ap/schema.cljs
Normal file
26
src/cljs/auto_ap/schema.cljs
Normal file
@@ -0,0 +1,26 @@
|
||||
(ns auto-ap.schema
|
||||
(:require [malli.core :as m]))
|
||||
|
||||
(def reference (m/schema [:map [:id :string]]))
|
||||
(def date (m/schema [:fn
|
||||
(fn [d]
|
||||
(if-not (or (instance? goog.date.DateTime d)
|
||||
(instance? goog.date.Date d))
|
||||
(throw (ex-info "Invalid Date" {:type ::m/invalid-type}))
|
||||
true))]))
|
||||
|
||||
(def money (m/schema [float? {:error/message "Invalid money"}]))
|
||||
(def not-empty-string (m/schema [:re {:error/message "Required"} #"\S+"]))
|
||||
(def code-string (m/schema [:re #"[A-Z0-9\-]+"]))
|
||||
|
||||
(def positive-integer (m/schema [:int {:min 1}]))
|
||||
(def integer-code (m/schema [:int {:min 10000 :max 99999}]))
|
||||
|
||||
(def expense-account (m/schema [:map
|
||||
[:id :string]
|
||||
[:account reference]
|
||||
[:location :string]
|
||||
[:amount money]]))
|
||||
|
||||
|
||||
(def approval-status (m/schema [:enum :unapproved :requires-feedback :approved :excluded]))
|
||||
@@ -2,6 +2,7 @@
|
||||
(ns auto-ap.subs
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.utils :refer [parse-jwt]]
|
||||
[clojure.string :as str]
|
||||
[goog.crypt.base64 :as base64]
|
||||
[minisearch :as ms]))
|
||||
@@ -22,6 +23,12 @@
|
||||
(when (:user db)
|
||||
(sort-by :name (vals (:clients db))))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::client-refs
|
||||
:<- [::clients]
|
||||
(fn [c]
|
||||
(map #(select-keys % [:id :name]) c)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::all-accounts
|
||||
(fn [db]
|
||||
@@ -135,8 +142,7 @@
|
||||
(re-frame/reg-sub
|
||||
::user
|
||||
(fn [db]
|
||||
(when (:user db)
|
||||
(js->clj (.parse js/JSON (base64/decodeString (second (str/split (:user db) #"\.")))) :keywordize-keys true))))
|
||||
(parse-jwt (:user db))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::active-route
|
||||
|
||||
72
src/cljs/auto_ap/views/components.cljs
Normal file
72
src/cljs/auto_ap/views/components.cljs
Normal file
@@ -0,0 +1,72 @@
|
||||
(ns auto-ap.views.components
|
||||
(:require [reagent.core :as r]
|
||||
[clojure.string :as str]
|
||||
[auto-ap.views.components.multi :as multi]
|
||||
[auto-ap.views.components.money-field :as money]
|
||||
[auto-ap.views.components.number :as number]
|
||||
[auto-ap.views.components.typeahead.vendor :as typeahead]
|
||||
[auto-ap.views.components.button-radio :as br]))
|
||||
|
||||
|
||||
(defn checkbox [{:keys [on-change
|
||||
value
|
||||
label]
|
||||
:as props}]
|
||||
(into [:label.checkbox
|
||||
[:input (-> props
|
||||
(assoc
|
||||
:type "checkbox"
|
||||
:on-change (fn []
|
||||
(on-change (not value)))
|
||||
:checked value)
|
||||
(dissoc :value))]
|
||||
" " label
|
||||
]
|
||||
(r/children (r/current-component))))
|
||||
|
||||
(defn select-field [{:keys [options allow-nil? class on-change keywordize?] :as props}]
|
||||
[:div.select {:class class}
|
||||
[:select (-> props
|
||||
(dissoc :allow-nil? :class :options)
|
||||
(update :value (fn [v]
|
||||
(cond (str/blank? v)
|
||||
""
|
||||
|
||||
keywordize?
|
||||
(name v)
|
||||
|
||||
:else
|
||||
v)))
|
||||
(assoc :on-change
|
||||
(fn [e]
|
||||
(println "VALUE IS" (keyword (.. e -target -value)))
|
||||
(if keywordize?
|
||||
(on-change (keyword (.. e -target -value)))
|
||||
(on-change e))))
|
||||
(dissoc :keywordize?))
|
||||
[:<>
|
||||
(when allow-nil?
|
||||
[:option {:value nil}])
|
||||
(for [[k v] options]
|
||||
^{:key k} [:option {:value k} v])]]])
|
||||
|
||||
(defn switch-input [{:keys [id label on-change value class]}]
|
||||
[:<>
|
||||
[:input.switch {:type "checkbox"
|
||||
:id id
|
||||
:on-change (fn []
|
||||
(on-change (not value)))
|
||||
:checked (boolean value)
|
||||
:class class}]
|
||||
[:label {:for id} label]])
|
||||
|
||||
(def multi-field-v2 multi/multi-field-v2)
|
||||
|
||||
(def number-input number/number-input)
|
||||
|
||||
(def money-input money/field)
|
||||
|
||||
(def search-backed-typeahead typeahead/search-backed-typeahead)
|
||||
(def entity-typeahead typeahead/typeahead-v3)
|
||||
|
||||
(def button-radio-input br/button-radio)
|
||||
@@ -1,108 +1,34 @@
|
||||
(ns auto-ap.views.components.address
|
||||
(:require [auto-ap.entities.address :as address]
|
||||
[auto-ap.views.utils :refer [dispatch-value-change dispatch-event bind-field horizontal-field]]
|
||||
[auto-ap.forms.builder :as form-builder]))
|
||||
(:require
|
||||
[auto-ap.entities.address :as address]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.views.components.level :as level]))
|
||||
|
||||
(defn address-field [{:keys [event field subscription]}]
|
||||
[:span
|
||||
[horizontal-field
|
||||
nil
|
||||
[:div.control
|
||||
[:p.help "Address"]
|
||||
[bind-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "1700 Pennsylvania Ave"
|
||||
:field (conj field :street1)
|
||||
:spec ::address/street1
|
||||
:event event
|
||||
:subscription subscription}]]]]
|
||||
|
||||
[horizontal-field
|
||||
nil
|
||||
[:div.control
|
||||
[bind-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "Suite 400"
|
||||
:field (conj field :street2)
|
||||
:spec ::address/street2
|
||||
:event event
|
||||
:subscription subscription}]]]]
|
||||
|
||||
[horizontal-field
|
||||
nil
|
||||
[:div.control
|
||||
[:p.help "City"]
|
||||
[bind-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "Cupertino"
|
||||
:field (conj field :city)
|
||||
:spec ::address/city
|
||||
:event event
|
||||
:subscription subscription}]]]
|
||||
[:div.control
|
||||
[:p.help "State"]
|
||||
[bind-field
|
||||
[:input.input {:type "text"
|
||||
:placeholder "CA"
|
||||
:field (conj field :state)
|
||||
:spec ::address/state
|
||||
:size 2
|
||||
:max-length "2"
|
||||
:event event
|
||||
:subscription subscription}]]]
|
||||
[:div.control
|
||||
[:p.help "Zip"]
|
||||
[bind-field
|
||||
[:input.input {:type "text"
|
||||
:field (conj field :zip)
|
||||
:spec ::address/zip
|
||||
:event event
|
||||
:subscription subscription
|
||||
:placeholder "95014"}]]]]])
|
||||
|
||||
(defn address2-field []
|
||||
[:span
|
||||
[horizontal-field
|
||||
nil
|
||||
[:div.control
|
||||
(defn address2-field [{:keys [value on-change]}]
|
||||
[form-builder/virtual-builder {:value (or value {})
|
||||
:on-change on-change}
|
||||
[:div
|
||||
[form-builder/field-v2 {:field :street1}
|
||||
[:p.help "Street Address"]
|
||||
[form-builder/raw-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "1700 Pennsylvania Ave"
|
||||
:field [:street1]
|
||||
:spec ::address/street1}]]]]
|
||||
|
||||
[horizontal-field
|
||||
nil
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "Suite 400"
|
||||
:field [:street2]
|
||||
:spec ::address/street2}]]]]
|
||||
|
||||
[horizontal-field
|
||||
nil
|
||||
[:div.control
|
||||
[:p.help "City"]
|
||||
[form-builder/raw-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "1700 Pennsylvania Ave"}]]
|
||||
[form-builder/raw-field-v2 {:field :street2}
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "Suite 400"}]]
|
||||
[level/left-stack
|
||||
[form-builder/field-v2 {:field :city}
|
||||
[:p.help "City"]
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:placeholder "Cupertino"
|
||||
:field [:city]
|
||||
:spec ::address/city}]]]
|
||||
[:div.control
|
||||
[:p.help "State"]
|
||||
[form-builder/raw-field
|
||||
[:input.input {:type "text"
|
||||
:placeholder "CA"
|
||||
:field [:state]
|
||||
:spec ::address/state
|
||||
:size 2
|
||||
:max-length "2"}]]]
|
||||
[:div.control
|
||||
[:p.help "Zip"]
|
||||
[form-builder/raw-field
|
||||
[:input.input {:type "text"
|
||||
:field [:zip]
|
||||
:spec ::address/zip
|
||||
:placeholder "95014"}]]]]])
|
||||
:field [:city]
|
||||
:spec ::address/city}]]
|
||||
[form-builder/field-v2 {:field :state}
|
||||
[:p.help "State"]
|
||||
[:input.input {:type "text"
|
||||
:placeholder "CA"
|
||||
:size 2
|
||||
:max-length "2"}]]
|
||||
[form-builder/field-v2 {:field :zip}
|
||||
[:p.help "Zip"]
|
||||
[:input.input {:type "text"
|
||||
:placeholder "95014"}]]]]])
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
(ns auto-ap.views.components.admin.side-bar
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[reagent.core :as r]
|
||||
[clojure.string :as str]
|
||||
[clojure.spec.alpha :as s]
|
||||
[cljs-time.core :as c]
|
||||
[goog.string :as gstring]
|
||||
[bidi.bidi :as bidi]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.views.utils :refer [active-when dispatch-event bind-field horizontal-field date->str str->date pretty standard]]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.events :as events]))
|
||||
(:require
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.utils :refer [active-when]]
|
||||
[bidi.bidi :as bidi]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]))
|
||||
|
||||
(defn admin-side-bar [params ]
|
||||
(let [ap @(re-frame/subscribe [::subs/active-page])]
|
||||
@@ -56,20 +52,9 @@
|
||||
[:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}]
|
||||
[:span {:class "name"} "Yodlee 2 Link"]]]
|
||||
|
||||
[:li.menu-item
|
||||
[:a {:href (bidi/path-for routes/routes :admin-plaid), :class (str "item" (active-when ap = :admin-plaid))}
|
||||
[:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}]
|
||||
[:span {:class "name"} "Plaid Link"]]]
|
||||
|
||||
|
||||
[:ul ]]
|
||||
[:p.menu-label "History"]
|
||||
[:ul.menu-list
|
||||
[:li.menu-item
|
||||
[:a {:href (bidi/path-for routes/routes :admin-reminders) , :class (str "item" (active-when ap = :admin-reminders))}
|
||||
[:span {:class "icon"}
|
||||
[:i {:class "fa fa-star-o"}]]
|
||||
|
||||
[:span {:class "name"} "Reminders"]]]]
|
||||
[:p.menu-label "Import"]
|
||||
[:ul.menu-list
|
||||
[:li.menu-item
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
(ns auto-ap.views.components.bank-account-filter
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.views.utils :refer [bind-field ->$]]
|
||||
[auto-ap.views.utils :refer [->$]]
|
||||
[auto-ap.subs :as subs]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
|
||||
@@ -1,57 +1,51 @@
|
||||
(ns auto-ap.views.components.date-range-filter
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.views.utils :refer [bind-field date-picker-optional date->str local-now standard]]
|
||||
[auto-ap.views.utils :refer [date-picker date->str local-now standard]]
|
||||
[cljs-time.core :as t]
|
||||
[re-frame.core :as re-frame]))
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.forms.builder :as form-builder]))
|
||||
|
||||
(defn dispatch-change [on-change-event start end]
|
||||
(fn [_]
|
||||
(re-frame/dispatch (into on-change-event [[:start] start]) )
|
||||
(re-frame/dispatch (into on-change-event [[:end] end]))))
|
||||
|
||||
(defn set-value [on-change-event v]
|
||||
(re-frame/dispatch (conj on-change-event v)))
|
||||
|
||||
(defn date-range-filter [{:keys [value on-change-event]}]
|
||||
[:div
|
||||
[:div.field.has-addons
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
(dispatch-change on-change-event
|
||||
(date->str (t/minus (local-now) (t/period :days 7)) standard)
|
||||
(date->str (local-now) standard))}
|
||||
"Week" ]]
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
(dispatch-change on-change-event
|
||||
(date->str (t/minus (local-now) (t/period :months 1)) standard)
|
||||
(date->str (local-now) standard))}
|
||||
"Month" ]]
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
(dispatch-change on-change-event
|
||||
(date->str (t/minus (local-now) (t/period :years 1)) standard)
|
||||
(date->str (local-now) standard))}
|
||||
"Year"]]
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
(dispatch-change on-change-event
|
||||
nil
|
||||
nil)}
|
||||
"All"]]]
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[bind-field
|
||||
[date-picker-optional
|
||||
{:event on-change-event
|
||||
:type "date2"
|
||||
:placeholder "Start"
|
||||
:class "is-small"
|
||||
:field [:start]
|
||||
:subscription value
|
||||
:output :text}]]]
|
||||
[:div.control
|
||||
[bind-field
|
||||
[date-picker-optional
|
||||
{:event on-change-event
|
||||
:type "date2"
|
||||
:class "is-small"
|
||||
:placeholder "End"
|
||||
:field [:end]
|
||||
:subscription value
|
||||
:output :text}]]]]])
|
||||
[form-builder/virtual-builder {:value (or value {})
|
||||
:on-change (fn [v]
|
||||
(set-value on-change-event v))}
|
||||
|
||||
[:div
|
||||
[:div.field.has-addons
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
#(set-value on-change-event
|
||||
{:start (date->str (t/minus (local-now) (t/period :days 7)) standard)
|
||||
:end (date->str (local-now) standard)})}
|
||||
"Week" ]]
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
#(set-value on-change-event
|
||||
{:start (date->str (t/minus (local-now) (t/period :months 1)) standard)
|
||||
:end (date->str (local-now) standard)})}
|
||||
"Month" ]]
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
|
||||
#(set-value on-change-event
|
||||
{:start (date->str (t/minus (local-now) (t/period :years 1)) standard)
|
||||
:end (date->str (local-now) standard)})}
|
||||
"Year"]]
|
||||
[:p.control [:a.button.is-small {:on-click
|
||||
#(set-value on-change-event nil)}
|
||||
"All"]]]
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
|
||||
[form-builder/raw-field-v2 {:field :start}
|
||||
[date-picker
|
||||
{:placeholder "Start"
|
||||
:class "is-small"
|
||||
:output :text}]]]
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field :end}
|
||||
[date-picker
|
||||
{:class "is-small"
|
||||
:placeholder "End"
|
||||
:output :text}]]]]]])
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]]
|
||||
@@ -10,17 +9,14 @@
|
||||
[auto-ap.views.utils :refer [dispatch-event with-user]]
|
||||
[clojure.string :as str]
|
||||
[goog.string :as gstring]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
(fn [db]
|
||||
true))
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::try-save
|
||||
[(forms/in-form ::form)]
|
||||
(fn [{:keys [db]} [_ id ]]
|
||||
(fn [{:keys [db]} _]
|
||||
(let [{{:keys [ total]} :invoice
|
||||
:keys [expense-accounts]} (:data db)
|
||||
expense-accounts (vals expense-accounts)
|
||||
@@ -33,6 +29,7 @@
|
||||
%))
|
||||
(reduce + 0))
|
||||
does-add-up? (< (Math/abs (- expense-accounts-total (js/parseFloat total))) 0.001)]
|
||||
|
||||
(if (and does-add-up?
|
||||
(every? :new-amount expense-accounts))
|
||||
|
||||
@@ -44,7 +41,7 @@
|
||||
(re-frame/reg-event-fx
|
||||
::save
|
||||
[with-user (forms/in-form ::form)]
|
||||
(fn [{:keys [db user] } [_ id]]
|
||||
(fn [{:keys [db user] } _]
|
||||
(let [{{:keys [id]} :invoice
|
||||
:keys [expense-accounts]} (:data db)
|
||||
expense-accounts (vals expense-accounts)]
|
||||
@@ -85,16 +82,10 @@
|
||||
(fn [db [_ x]]
|
||||
(update-in db [:data :expense-accounts] dissoc x)))
|
||||
|
||||
(def change-expense-accounts-form (forms/vertical-form {:submit-event [::try-save]
|
||||
:change-event [::forms/change ::form]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
|
||||
(defn form []
|
||||
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
expense-accounts (:expense-accounts data)
|
||||
{:keys [total] :or {total 0} {:keys [locations] :as client} :client} (:invoice data)
|
||||
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} change-expense-accounts-form
|
||||
multi-location? (> (count locations) 1)
|
||||
expense-accounts-total (->> expense-accounts
|
||||
vals
|
||||
@@ -108,72 +99,71 @@
|
||||
[:div
|
||||
[:div
|
||||
[:a.button.is-outlined {:on-click (dispatch-event [::add-split])} "Add split"]]
|
||||
(form-inline {}
|
||||
[:table.table
|
||||
[:thead
|
||||
[:tr
|
||||
[:th {:style {:width "500px"}} "Expense Account"]
|
||||
(when multi-location?
|
||||
[:th {:style {:width "200px"}} "Location"])
|
||||
[:th {:style {:width "200px"}} "Original Amount"]
|
||||
[:th {:style {:width "300px"}} "Amount"]
|
||||
[:th {:style {:width "5em"}}]]]
|
||||
[:tbody
|
||||
(doall (for [[id expense-account] expense-accounts]
|
||||
^{:key id}
|
||||
[:tr
|
||||
[:td.expandable [:div.control
|
||||
(raw-field
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i
|
||||
:client-id (:id client)}
|
||||
[:name :id :location]])
|
||||
:type "typeahead-v3"
|
||||
:field [:expense-accounts id :account]}])]]
|
||||
[form-builder/builder {:submit-event [::try-save]
|
||||
:id ::form}
|
||||
[:table.table
|
||||
[:thead
|
||||
[:tr
|
||||
[:th {:style {:width "500px"}} "Expense Account"]
|
||||
(when multi-location?
|
||||
[:th {:style {:width "200px"}} "Location"])
|
||||
[:th {:style {:width "200px"}} "Original Amount"]
|
||||
[:th {:style {:width "300px"}} "Amount"]
|
||||
[:th {:style {:width "5em"}}]]]
|
||||
[:tbody
|
||||
(doall (for [[id _] expense-accounts]
|
||||
^{:key id}
|
||||
[:tr
|
||||
[:td.expandable [:div.control
|
||||
[form-builder/raw-field-v2 {:field [:expense-accounts id :account]}
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i
|
||||
:client-id (:id client)}
|
||||
[:name :id :location]])}]]]]
|
||||
|
||||
(when multi-location?
|
||||
[:td
|
||||
(if-let [forced-location (get-in expense-accounts [id :account :location])]
|
||||
[:div.select
|
||||
[:select {:disabled "disabled" :value forced-location} [:option {:value forced-location} forced-location]]]
|
||||
[:div.select
|
||||
(raw-field
|
||||
[:select {:type "select"
|
||||
:field [:expense-accounts id :location]
|
||||
:spec (set locations)}
|
||||
(map (fn [l] ^{:key l} [:option {:value l} l]) locations)])])])
|
||||
|
||||
[:td
|
||||
(str "$" (get-in expense-accounts [id :amount]))]
|
||||
[:td
|
||||
[:div.control
|
||||
[:div.field.has-addons.is-extended
|
||||
[:p.control [:a.button.is-static "$"]]
|
||||
[:p.control
|
||||
(raw-field
|
||||
[:input.input {:type "number"
|
||||
:field [:expense-accounts id :new-amount-temp]
|
||||
:style {:text-align "right"}
|
||||
:on-blur (dispatch-event [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])])
|
||||
:on-key-down (fn [e ]
|
||||
(if (= 13 (.-keyCode e))
|
||||
(do
|
||||
(re-frame/dispatch [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])])
|
||||
true)
|
||||
false))
|
||||
:max (:total data)
|
||||
:step "0.01"}])]]]]
|
||||
[:td [:a.button {:on-click (dispatch-event [::remove-expense-account-split id])} [:i.fa.fa-times]]]]))
|
||||
[:tr
|
||||
[:td.no-border { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Invoice total: "]
|
||||
[:td.no-border { :style { :text-align "right"} } (str (gstring/format "$%.2f" total ) )]]
|
||||
[:tr
|
||||
[:td { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Account total: "]
|
||||
[:td { :style { :text-align "right"} } (str (gstring/format "$%.2f" expense-accounts-total ) )]]
|
||||
[:tr
|
||||
[:td.no-border { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Difference: "]
|
||||
[:td.no-border { :style { :text-align "right"} } (str (gstring/format "$%.2f" (- total expense-accounts-total) ) )]]]])]))
|
||||
(when multi-location?
|
||||
[:td
|
||||
(if-let [forced-location (get-in expense-accounts [id :account :location])]
|
||||
[:div.select
|
||||
[:select {:disabled "disabled" :value forced-location} [:option {:value forced-location} forced-location]]]
|
||||
[:div.select
|
||||
|
||||
[form-builder/raw-field-v2 {:field [:expense-accounts id :location]}
|
||||
[com/select-field {:options (map (fn [l] [l l])
|
||||
locations)
|
||||
:allow-nil? true}]]])])
|
||||
|
||||
[:td
|
||||
(str "$" (get-in expense-accounts [id :amount]))]
|
||||
[:td
|
||||
[:div.control
|
||||
[:div.field.has-addons.is-extended
|
||||
[:p.control [:a.button.is-static "$"]]
|
||||
[:p.control
|
||||
[form-builder/raw-field-v2 {:field [:expense-accounts id :new-amount-temp]}
|
||||
[:input.input {:type "number"
|
||||
:style {:text-align "right"}
|
||||
:on-blur (dispatch-event [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])])
|
||||
:on-key-down (fn [e ]
|
||||
(if (= 13 (.-keyCode e))
|
||||
(do
|
||||
(re-frame/dispatch [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])])
|
||||
true)
|
||||
false))
|
||||
:max (:total data)
|
||||
:step "0.01"}]]]]]]
|
||||
[:td [:a.button {:on-click (dispatch-event [::remove-expense-account-split id])} [:i.fa.fa-times]]]]))
|
||||
[:tr
|
||||
[:td.no-border { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Invoice total: "]
|
||||
[:td.no-border { :style { :text-align "right"} } (str (gstring/format "$%.2f" total ) )]]
|
||||
[:tr
|
||||
[:td { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Account total: "]
|
||||
[:td { :style { :text-align "right"} } (str (gstring/format "$%.2f" expense-accounts-total ) )]]
|
||||
[:tr
|
||||
[:td.no-border { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Difference: "]
|
||||
[:td.no-border { :style { :text-align "right"} } (str (gstring/format "$%.2f" (- total expense-accounts-total) ) )]]]]
|
||||
[form-builder/hidden-submit-button]]]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::show
|
||||
@@ -184,11 +174,10 @@
|
||||
:status-from [::status/single ::form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::try-save])
|
||||
:can-submit [::can-submit]
|
||||
:close-event [::status/completed ::form]}}]
|
||||
:db (-> db
|
||||
(forms/start-form ::form
|
||||
(forms/start-form ::form
|
||||
|
||||
{:expense-accounts (by :id
|
||||
(:expense-accounts i))
|
||||
:invoice i}))}))
|
||||
{:expense-accounts (by :id
|
||||
(:expense-accounts i))
|
||||
:invoice i}))}))
|
||||
|
||||
@@ -1,10 +1,18 @@
|
||||
(ns auto-ap.views.components.expense-accounts-field
|
||||
(:require
|
||||
[auto-ap.views.utils :refer [->$ bind-field dispatch-event]]
|
||||
[auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]]
|
||||
[clojure.string :as str]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.utils :refer [dollars-0?]]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.button-radio :as button-radio]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.views.components.money-field :refer [money-field]]
|
||||
[auto-ap.views.components.percentage-field :refer [percentage-field]]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.utils :refer [->$ appearing-group]]
|
||||
[goog.string :as gstring]
|
||||
[re-frame.core :as re-frame]))
|
||||
[malli.core :as m]))
|
||||
|
||||
(defn can-replace-with-default? [accounts]
|
||||
(and (or (not (seq accounts))
|
||||
@@ -46,27 +54,6 @@
|
||||
|
||||
;; EVENTS
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::add-expense-account
|
||||
(fn [_ [_ event expense-accounts locations]]
|
||||
{:dispatch (conj event (conj expense-accounts
|
||||
{:amount 0 :id (str "new-" (random-uuid))
|
||||
:amount-mode "%"
|
||||
:amount-percentage 0
|
||||
:location (if (= 1 (count locations))
|
||||
(first locations)
|
||||
nil)}))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::remove-expense-account
|
||||
(fn [_ [_ event expense-accounts id]]
|
||||
{:dispatch (conj event (transduce (filter
|
||||
(fn [ea]
|
||||
(not= (:id ea) id)) )
|
||||
conj
|
||||
[]
|
||||
expense-accounts))}))
|
||||
|
||||
(defn recalculate-amounts [expense-accounts total]
|
||||
(mapv
|
||||
(fn [ea]
|
||||
@@ -76,143 +63,116 @@
|
||||
(* (/ (js/parseFloat (:amount-percentage ea)) 100.0) total)))))
|
||||
expense-accounts))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::spread-evenly
|
||||
(fn [_ [_ event expense-accounts max-value]]
|
||||
{:dispatch (into event [(recalculate-amounts (mapv
|
||||
(fn [ea]
|
||||
(assoc ea :amount-percentage (js/parseFloat
|
||||
(goog.string/format "%.2f"
|
||||
(* 100 (/ 1 (count expense-accounts)))))))
|
||||
expense-accounts)
|
||||
max-value)])}))
|
||||
(re-frame/reg-event-fx
|
||||
::expense-account-changed
|
||||
(fn [_ [_ event expense-accounts max-value field value]]
|
||||
(let [updated-accounts (cond-> expense-accounts
|
||||
true (assoc-in field value)
|
||||
(= (list :account) (drop 1 field)) (assoc-in [(first field) :location] nil)
|
||||
|
||||
(= (list :amount-percentage) (drop 1 field)) (assoc-in [(first field) :amount]
|
||||
(js/parseFloat
|
||||
(goog.string/format "%.2f"
|
||||
(* (/ (cond-> value
|
||||
(not (float? value)) (js/parseFloat )) 100.0)
|
||||
(cond-> max-value
|
||||
(not (float? max-value)) (js/parseFloat)))))))
|
||||
updated-accounts (if-let [location (get-in updated-accounts [(first field) :account :location])]
|
||||
(assoc-in updated-accounts [(first field) :location] location)
|
||||
updated-accounts)]
|
||||
{:dispatch (into event [updated-accounts])})))
|
||||
|
||||
|
||||
;; VIEWS
|
||||
(defn expense-accounts-field [{expense-accounts :value client :client max-value :max locations :locations event :event descriptor :descriptor disabled :disabled percentage-only? :percentage-only? :or {percentage-only? false}}]
|
||||
[:div
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:h1.subtitle.is-4.is-inline (str/capitalize descriptor) "s"]
|
||||
|
||||
|
||||
(def schema (m/schema [:sequential [:map
|
||||
[:id :string]
|
||||
[:account schema/reference]
|
||||
[:location schema/not-empty-string]
|
||||
[:amount schema/money]]]))
|
||||
|
||||
|
||||
|
||||
(defn expense-accounts-field-v2 [{value :value on-change :on-change expense-accounts :value client :client max-value :max locations :locations disabled :disabled percentage-only? :percentage-only? :or {percentage-only? false}}]
|
||||
[form-builder/virtual-builder {:value value
|
||||
:schema schema
|
||||
:on-change (fn [expense-accounts original-expense-accounts]
|
||||
(let [updated-expense-accounts
|
||||
(for [[before-account after-account] (map vector (concat original-expense-accounts
|
||||
(repeat nil)) expense-accounts)]
|
||||
(cond-> after-account
|
||||
(not= (:id (:account before-account))
|
||||
(:id (:account after-account)))
|
||||
(assoc :location nil)
|
||||
|
||||
(not= (:amount-percentage before-account)
|
||||
(:amount-percentage after-account))
|
||||
(assoc :amount (* (/ (:amount-percentage after-account) 100.0)
|
||||
max-value))
|
||||
|
||||
(:location (:account after-account))
|
||||
(assoc :location (:location (:account after-account)))))]
|
||||
(on-change (into [] updated-expense-accounts))))}
|
||||
[:div
|
||||
[:div.tags
|
||||
(when max-value
|
||||
[:div.tag "To Allocate: " (->$ max-value)])
|
||||
|
||||
(when-not percentage-only?
|
||||
[:p.help "Remaining " (->$ (- max-value (reduce + 0 (map (comp js/parseFloat :amount) expense-accounts))))])]
|
||||
[:div.column.is-narrow
|
||||
(when-not disabled
|
||||
[:p.buttons
|
||||
[:a.button {:on-click (dispatch-event [::spread-evenly event expense-accounts max-value])} "Spread evenly"]
|
||||
[:a.button {:on-click (dispatch-event [::add-expense-account event expense-accounts locations])} "Add"]])]]
|
||||
(let [total (reduce + 0 (map (or :amount 0.0) expense-accounts))]
|
||||
[:<>
|
||||
[:div.tag "Total: " (->$ total) ]
|
||||
[:div.tag {:class (if (dollars-0? (- max-value total))
|
||||
["is-primary" "is-light"]
|
||||
["is-danger" "is-light"])}
|
||||
"Remaining: " (->$ (- max-value total))]]))]
|
||||
|
||||
(for [[index {:keys [account id location amount amount-percentage amount-mode] :as expense-account}] (map vector (range) expense-accounts)]
|
||||
^{:key id}
|
||||
[:div.box
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:h1.subtitle.is-6 (cond (and account (not percentage-only?))
|
||||
(str (:name account) " - "
|
||||
location ": "
|
||||
(gstring/format "$%.2f" (or amount 0) ))
|
||||
|
||||
account
|
||||
(str (:name account) " - "
|
||||
location ": %"
|
||||
amount-percentage)
|
||||
|
||||
:else
|
||||
[:i "New " descriptor])]]
|
||||
[:div.column.is-narrow
|
||||
(when-not disabled
|
||||
[:a.delete {:on-click (dispatch-event [::remove-expense-account event expense-accounts id])}])]]
|
||||
|
||||
[:div.field
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:p.help "Account"]
|
||||
[:div.control.is-fullwidth
|
||||
[bind-field
|
||||
^{:key (:id client)}
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i
|
||||
:client-id (:id client)}
|
||||
[:name :id :location]])
|
||||
:type "typeahead-v3"
|
||||
:field [index :account]
|
||||
|
||||
:disabled disabled
|
||||
:event [::expense-account-changed event expense-accounts max-value]
|
||||
:subscription expense-accounts}]]]]
|
||||
[:div.column.is-narrow
|
||||
[:p.help "Location"]
|
||||
[:div.control
|
||||
(if-let [forced-location (:location account)]
|
||||
[:div.select
|
||||
[:select {:disabled "disabled" :style {:width "5em"} :value forced-location} [:option {:value forced-location} forced-location]]]
|
||||
[:div.select
|
||||
[bind-field
|
||||
[:select {:type "select"
|
||||
:disabled (boolean (or (:location account)
|
||||
disabled))
|
||||
:style {:width "5em"}
|
||||
:field [index :location]
|
||||
:allow-nil? true
|
||||
:spec (set locations)
|
||||
:event [::expense-account-changed event expense-accounts max-value]
|
||||
:subscription expense-accounts}
|
||||
(map (fn [l] ^{:key l} [:option {:value l} l]) locations)]]])]]]]
|
||||
|
||||
[:div.field
|
||||
[:p.help "Amount"]
|
||||
[:div.control
|
||||
[:div.field.has-addons.is-extended
|
||||
[:p.control [:span.select
|
||||
[bind-field
|
||||
[:select {:type "select"
|
||||
:disabled (or disabled percentage-only?)
|
||||
:field [index :amount-mode]
|
||||
:allow-nil? false
|
||||
:event [::expense-account-changed event expense-accounts max-value]
|
||||
:subscription expense-accounts}
|
||||
[:option "$"]
|
||||
[:option "%"]]]]]
|
||||
[:p.control
|
||||
(if (= "$" amount-mode)
|
||||
[bind-field
|
||||
[:input.input {:type "number"
|
||||
:field [index :amount]
|
||||
:style {:text-align "right" :width "7em"}
|
||||
:event [::expense-account-changed event expense-accounts max-value]
|
||||
:disabled disabled
|
||||
:subscription expense-accounts
|
||||
:precision 2
|
||||
:value (get-in expense-account [:amount])
|
||||
:max max-value
|
||||
:step "0.01"}]]
|
||||
[bind-field
|
||||
[:input.input {:type "number"
|
||||
:field [index :amount-percentage]
|
||||
:style {:text-align "right" :width "7em"}
|
||||
:disabled disabled
|
||||
:event [::expense-account-changed event expense-accounts max-value]
|
||||
:precision 2
|
||||
:subscription expense-accounts
|
||||
:value (get-in expense-account [:amount-percentage])
|
||||
:max "100"
|
||||
:step "0.01"}]])]]]]])])
|
||||
(into [appearing-group]
|
||||
(for [[index {:keys [account id amount amount-mode]}] (map vector (range) expense-accounts)]
|
||||
^{:key id}
|
||||
[:div.card {:style {:margin-bottom "2em"}}
|
||||
[:div.card-header
|
||||
[:p.card-header-title "Expense Account"]
|
||||
(when-not disabled
|
||||
[:div.card-header-icon {:on-click (fn []
|
||||
(on-change (into [] (filter #(not= id (:id %)) expense-accounts))))}
|
||||
[:a.delete ]])]
|
||||
[:div.card-content
|
||||
[:div.field
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:div.control.is-fullwidth
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field [index :account]}
|
||||
"Account"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i
|
||||
:client-id (:id client)}
|
||||
[:name :id :location]])
|
||||
:disabled disabled}]]]]
|
||||
[:div.column.is-narrow
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field [index :location]}
|
||||
"Location"
|
||||
[com/select-field {:options (if (:location account)
|
||||
[[(:location account) (:location account)]]
|
||||
(map (fn [l] [l l])
|
||||
locations))
|
||||
:disabled (boolean (:location account))
|
||||
:allow-nil? true}]]]]]
|
||||
(if percentage-only?
|
||||
[form-builder/raw-field-v2 {:field [index :amount-percentage]}
|
||||
[percentage-field {}]]
|
||||
[left-stack
|
||||
[:div.field.has-addons
|
||||
[form-builder/raw-field-v2 {:field [index :amount-mode]}
|
||||
[button-radio/button-radio {:options [["$" "Amount"]
|
||||
["%" "Percent"]]}]]
|
||||
(if (= "$" amount-mode)
|
||||
[form-builder/raw-field-v2 {:field [index :amount]}
|
||||
[money-field {}]
|
||||
]
|
||||
[form-builder/raw-field-v2 {:field [index :amount-percentage]}
|
||||
[percentage-field {}]])]
|
||||
(when (= "%" amount-mode)
|
||||
[:div.tag.is-primary.is-light (gstring/format "$%.2f" (or amount 0) )])])]]))
|
||||
(when-not disabled
|
||||
[:p.buttons
|
||||
[:a.button {:on-click (fn []
|
||||
(on-change
|
||||
(recalculate-amounts (mapv
|
||||
(fn [ea]
|
||||
(assoc ea :amount-percentage (* 100.0 (/ 1 (count expense-accounts)))))
|
||||
expense-accounts)
|
||||
max-value))
|
||||
)} "Spread evenly"]
|
||||
[:a.button {:on-click
|
||||
(fn []
|
||||
(on-change (conj value {:id (str "new-" (random-uuid))
|
||||
:amount-mode "%"
|
||||
:location (if (= 1 (count locations))
|
||||
(first locations)
|
||||
nil)})))}
|
||||
"Add"]])]])
|
||||
|
||||
@@ -20,28 +20,31 @@
|
||||
[auto-ap.views.pages.data-page :as data-page]))
|
||||
|
||||
(defn data-params->query-params [params]
|
||||
{:exact-match-id (some-> params :exact-match-id str)
|
||||
:start (:start params 0)
|
||||
:sort (:sort params)
|
||||
:per-page (:per-page params)
|
||||
(if (:exact-match-id params)
|
||||
{:exact-match-id (some-> params :exact-match-id str)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))}
|
||||
{:exact-match-id (some-> params :exact-match-id str)
|
||||
:start (:start params 0)
|
||||
:sort (:sort params)
|
||||
:per-page (:per-page params)
|
||||
|
||||
:vendor-id (:id (:vendor params))
|
||||
:date-range (:date-range params)
|
||||
:due-range (:due-range params)
|
||||
:amount-gte (:amount-gte (:amount-range params))
|
||||
:amount-lte (:amount-lte (:amount-range params))
|
||||
:location (:location params)
|
||||
:unresolved (:unresolved params)
|
||||
:scheduled-payments (:scheduled-payments params)
|
||||
:invoice-number-like (:invoice-number-like params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:import-status (:import-status params)
|
||||
:status (condp = @(re-frame/subscribe [::subs/active-page])
|
||||
:invoices nil
|
||||
:import-invoices nil
|
||||
:unpaid-invoices :unpaid
|
||||
:paid-invoices :paid
|
||||
:voided-invoices :voided)})
|
||||
:vendor-id (:id (:vendor params))
|
||||
:date-range (:date-range params)
|
||||
:due-range (:due-range params)
|
||||
:amount-gte (:amount-gte (:amount-range params))
|
||||
:amount-lte (:amount-lte (:amount-range params))
|
||||
:location (:location params)
|
||||
:unresolved (:unresolved params)
|
||||
:scheduled-payments (:scheduled-payments params)
|
||||
:invoice-number-like (:invoice-number-like params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:import-status (:import-status params)
|
||||
:status (condp = @(re-frame/subscribe [::subs/active-page])
|
||||
:invoices nil
|
||||
:import-invoices nil
|
||||
:unpaid-invoices :unpaid
|
||||
:paid-invoices :paid
|
||||
:voided-invoices :voided)}))
|
||||
|
||||
(defn query [params]
|
||||
{:venia/queries [[:invoice_page
|
||||
|
||||
@@ -3,44 +3,53 @@
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.dropdown :refer [drop-down drop-down-contents]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.vendor-dialog :as vendor-dialog]
|
||||
[auto-ap.views.utils
|
||||
:refer [active-when appearing bind-field dispatch-event dispatch-event-with-propagation login-url]]
|
||||
:refer [active-when
|
||||
appearing
|
||||
dispatch-event-with-propagation
|
||||
login-url]]
|
||||
[bidi.bidi :as bidi]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]))
|
||||
[reagent.core :as r]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[auto-ap.forms :as forms]))
|
||||
|
||||
(defn navbar-drop-down-contents [{:keys [id]} children ]
|
||||
(let [toggle-fn (fn [] (re-frame/dispatch [::events/toggle-menu id]))]
|
||||
(r/create-class {:component-did-mount (fn [] (.addEventListener js/document "click" toggle-fn))
|
||||
:component-will-unmount (fn [] (.removeEventListener js/document "click" toggle-fn))
|
||||
:reagent-render
|
||||
(fn [children]
|
||||
children)})))
|
||||
|
||||
(defn navbar-drop-down [{:keys [ header id class]} child]
|
||||
(r/create-class
|
||||
{:reagent-render (fn [{:keys [header id]} child]
|
||||
(let [menu-active? @(re-frame/subscribe [::subs/menu-active? id])]
|
||||
[:div { :class (str "navbar-item has-dropdown " (when menu-active? "is-active " ) " " class)}
|
||||
[:a {:class "navbar-link login" :on-click (fn [e]
|
||||
(.preventDefault e)
|
||||
(.stopPropagation e)
|
||||
(re-frame/dispatch [::events/toggle-menu id])
|
||||
true)} header]
|
||||
[appearing {:visible? menu-active? :enter-class "appear" :exit-class "disappear" :timeout 200}
|
||||
[:div {:class "navbar-dropdown"}
|
||||
[navbar-drop-down-contents {:id id}
|
||||
[:div child]]]]]))}))
|
||||
(defn navbar-drop-down [{:keys [ class]} _]
|
||||
(let [!child (r/atom nil)]
|
||||
(r/create-class
|
||||
{:reagent-render (fn [{:keys [header id]} child]
|
||||
(let [menu-active? @(re-frame/subscribe [::subs/menu-active? id])]
|
||||
[:div { :class (str "navbar-item has-dropdown " (when menu-active? "is-active " ) " " class)
|
||||
:ref (fn [n]
|
||||
(reset! !child n))
|
||||
:tab-index 0
|
||||
:onBlur (fn [e]
|
||||
(js/setTimeout (fn []
|
||||
(println @!child)
|
||||
(println (.-activeElement js/document))
|
||||
(when-not (.contains @!child (.-activeElement js/document))
|
||||
(re-frame/dispatch [::events/toggle-menu id])))
|
||||
2))
|
||||
}
|
||||
[:a {:class "navbar-link login" :on-click (fn [e]
|
||||
(.preventDefault e)
|
||||
(.stopPropagation e)
|
||||
(re-frame/dispatch [::events/toggle-menu id])
|
||||
true)} header]
|
||||
[appearing {:visible? menu-active? :enter-class "appear" :exit-class "disappear" :timeout 200}
|
||||
[:div {:class "navbar-dropdown"}
|
||||
[:div child]]]]))})))
|
||||
|
||||
(defn login-dropdown []
|
||||
(let [user (re-frame/subscribe [::subs/user])]
|
||||
(if @user
|
||||
[navbar-drop-down {:header [:span [:span.icon [:i.fa.fa-user] ]
|
||||
[:span (:user/name @user)]] :id ::account}
|
||||
[:span (:user/name @user)]]
|
||||
:id ::account}
|
||||
[:div
|
||||
[:a {:class "navbar-item"
|
||||
:href (bidi/path-for routes/routes :reports)} "My company"]
|
||||
@@ -60,8 +69,8 @@
|
||||
(re-frame/reg-sub
|
||||
::matching-clients
|
||||
:<- [::subs/clients]
|
||||
:<- [::client-search]
|
||||
(fn [[clients {client-search :value}]]
|
||||
:<- [::forms/form ::client-search]
|
||||
(fn [[clients {{client-search :value} :data}]]
|
||||
(if (empty? client-search)
|
||||
clients
|
||||
(if-let [exact-match (first (filter
|
||||
@@ -76,21 +85,46 @@
|
||||
(str/includes? (str/lower-case (:name client)) (str/lower-case client-search))))
|
||||
clients)))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::client-search-changed
|
||||
[(re-frame/path [::client-search])]
|
||||
(fn [client-search [_ path value]]
|
||||
(assoc-in client-search path value)))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::client-searched
|
||||
[(re-frame/inject-cofx ::inject/sub [::matching-clients])]
|
||||
(fn [{::keys [matching-clients]}]
|
||||
{:dispatch-n [[::events/swap-client (first matching-clients)]
|
||||
[::events/toggle-menu ::select-client]]}))
|
||||
|
||||
(defn client-dropdown []
|
||||
(let [client (re-frame/subscribe [::subs/client])
|
||||
matching-clients @(re-frame/subscribe [::matching-clients])]
|
||||
|
||||
[navbar-drop-down {:header (str "Company: " (if @client (:name @client)
|
||||
"All"))
|
||||
:id ::select-client}
|
||||
[:div
|
||||
[:a {:class "navbar-item"
|
||||
:on-click (fn []
|
||||
(re-frame/dispatch [::events/toggle-menu ::select-client])
|
||||
(re-frame/dispatch [::forms/form-closing ::client-search])
|
||||
(re-frame/dispatch [::events/swap-client nil]))} "All" ]
|
||||
[:hr {:class "navbar-divider"}]
|
||||
[form-builder/builder {:id ::client-search
|
||||
:submit-event [::client-searched]}
|
||||
[form-builder/raw-field-v2 {:field :value}
|
||||
[:input.input.navbar-item {:placeholder "Company name"
|
||||
:auto-focus true}]]]
|
||||
(for [{:keys [name id] :as client} (take 8 matching-clients)]
|
||||
^{:key id }
|
||||
[:a {:class "navbar-item"
|
||||
:on-click (fn []
|
||||
(re-frame/dispatch [::events/toggle-menu ::select-client])
|
||||
(re-frame/dispatch [::events/swap-client client]))
|
||||
} name])]]))
|
||||
|
||||
(defn navbar [ap]
|
||||
(let [navbar-menu-shown? (r/atom false)]
|
||||
(fn [ap]
|
||||
(let [user (re-frame/subscribe [::subs/user])
|
||||
client (re-frame/subscribe [::subs/client])
|
||||
clients (re-frame/subscribe [::subs/clients])
|
||||
matching-clients @(re-frame/subscribe [::matching-clients])
|
||||
menu (re-frame/subscribe [::subs/menu])
|
||||
client-search @(re-frame/subscribe [::client-search])
|
||||
(let [user (re-frame/subscribe [::subs/user])
|
||||
clients (re-frame/subscribe [::subs/clients])
|
||||
is-initial-loading @(re-frame/subscribe [::subs/is-initial-loading?])]
|
||||
[:nav {:class "navbar has-shadow is-fixed-top is-grey"}
|
||||
|
||||
@@ -133,33 +167,8 @@
|
||||
[:div.navbar-end
|
||||
|
||||
(when (> (count @clients) 1)
|
||||
[navbar-drop-down {:header (str "Company: " (if @client (:name @client)
|
||||
"All"))
|
||||
:id ::select-client}
|
||||
[:div
|
||||
[:a {:class "navbar-item"
|
||||
:on-click (fn []
|
||||
(re-frame/dispatch [::events/swap-client nil]))} "All" ]
|
||||
[:hr {:class "navbar-divider"}]
|
||||
[bind-field
|
||||
[:input.input.navbar-item {:placeholder "Company name"
|
||||
:auto-focus true
|
||||
:field [:value]
|
||||
:on-key-up (fn [k]
|
||||
(when (= 13 (.-which k))
|
||||
(do
|
||||
(re-frame/dispatch [::events/swap-client (first matching-clients)])
|
||||
(re-frame/dispatch [::events/toggle-menu ::select-client])
|
||||
(re-frame/dispatch [::client-search-changed [:value] nil])))
|
||||
)
|
||||
:event [::client-search-changed]
|
||||
:subscription client-search}]]
|
||||
(for [{:keys [name id] :as client} matching-clients]
|
||||
^{:key id }
|
||||
[:a {:class "navbar-item"
|
||||
:on-click (fn []
|
||||
(re-frame/dispatch [::events/swap-client client]))
|
||||
} name])]])])]
|
||||
[client-dropdown]
|
||||
)])]
|
||||
(when-not is-initial-loading
|
||||
[login-dropdown])]
|
||||
|
||||
@@ -167,11 +176,7 @@
|
||||
]))))
|
||||
|
||||
|
||||
(defn footer []
|
||||
[:footer {:style {:padding "1em"}}
|
||||
[:div {:class "content has-text-centered"}
|
||||
[:p
|
||||
[:strong "Integreat"] ]]])
|
||||
|
||||
|
||||
(defn appearing-side-bar [{:keys [visible?]} ]
|
||||
[appearing {:visible? visible? :enter-class "slide-in-right" :exit-class "slide-out-right" :timeout 500}
|
||||
@@ -179,7 +184,7 @@
|
||||
(into [:div.sub-main {} ]
|
||||
(r/children (r/current-component)))]])
|
||||
|
||||
(defn side-bar-layout [{:keys [side-bar main ap bottom right-side-bar]}]
|
||||
(defn side-bar-layout [{:keys [side-bar main bottom right-side-bar]}]
|
||||
(let [ap @(re-frame/subscribe [::subs/active-page])
|
||||
client @(re-frame/subscribe [::subs/client])]
|
||||
[:div
|
||||
@@ -200,7 +205,6 @@
|
||||
(when right-side-bar
|
||||
right-side-bar)
|
||||
]
|
||||
#_[footer]
|
||||
[:div
|
||||
bottom]
|
||||
[:div#dz-hidden]]))
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
[react :as react]))
|
||||
(def good-$ #"^\-?[0-9]+(\.[0-9][0-9])?$")
|
||||
|
||||
(defn -money-field [{:keys [min max disabled on-change value class style]}]
|
||||
(defn -money-field [{:keys [min max disabled on-blur on-change value class style placeholder]}]
|
||||
(let [[ parsed-amount set-parsed-amount] (react/useState {:parsed value
|
||||
:raw (cond
|
||||
(str/blank? value)
|
||||
@@ -40,6 +40,7 @@
|
||||
[:div.control.has-icons-left
|
||||
[:input.input {:type "text"
|
||||
:disabled disabled
|
||||
:placeholder placeholder
|
||||
:class class
|
||||
:on-change (fn [e]
|
||||
(let [raw (.. e -target -value)
|
||||
@@ -58,7 +59,9 @@
|
||||
|
||||
(set-parsed-amount {:raw ""
|
||||
:parsed nil})
|
||||
(on-change nil)))
|
||||
(on-change nil))
|
||||
(when on-blur
|
||||
(on-blur)))
|
||||
:min min
|
||||
:max max
|
||||
:step "0.01"
|
||||
@@ -69,3 +72,6 @@
|
||||
|
||||
(defn money-field []
|
||||
[:f> -money-field (r/props (r/current-component))])
|
||||
|
||||
(defn field []
|
||||
[:f> -money-field (r/props (r/current-component))])
|
||||
|
||||
79
src/cljs/auto_ap/views/components/multi.cljs
Normal file
79
src/cljs/auto_ap/views/components/multi.cljs
Normal file
@@ -0,0 +1,79 @@
|
||||
(ns auto-ap.views.components.multi
|
||||
(:require
|
||||
[cemerick.url]
|
||||
#_{:clj-kondo/ignore [:unused-namespace]}
|
||||
[reagent.core :as reagent]
|
||||
[react :as react]
|
||||
[auto-ap.entities.shared :as shared]
|
||||
[auto-ap.views.utils :refer [appearing-group]]
|
||||
[auto-ap.forms.builder :as form-builder]))
|
||||
|
||||
|
||||
;; TODO just embrace the fact that it will need to be remounted, and use index based keys
|
||||
(defn multi-field-v2-internal [{:keys [template key-fn allow-change? disable-new? disable-remove? schema on-change disabled new-text] prop-value :value :as props} ]
|
||||
(let [prop-value (if (seq prop-value)
|
||||
(vec prop-value)
|
||||
[])]
|
||||
[form-builder/virtual-builder {:value prop-value
|
||||
:schema schema
|
||||
:on-change on-change}
|
||||
[:fieldset {:disabled disabled}
|
||||
[:div {:style {:margin-bottom "0.25em"}}
|
||||
(into [(if key-fn
|
||||
appearing-group
|
||||
:<>)]
|
||||
(for [[i override] (map vector (range) prop-value)
|
||||
:let [extant? (if key-fn
|
||||
(boolean (key-fn override))
|
||||
true)
|
||||
is-disabled? (boolean (and (= false allow-change?)
|
||||
extant?))
|
||||
key (or (when key-fn (key-fn override))
|
||||
(::key override)
|
||||
i)]]
|
||||
^{:key key}
|
||||
[form-builder/with-scope {:scope [i]}
|
||||
[:div.level {:style {:margin-bottom "0.25em"}}
|
||||
[:div.level-left {:style {:padding "0.5em 1em"}
|
||||
:class (when-not extant?
|
||||
"has-background-info-light")}
|
||||
(let [template (if (fn? template)
|
||||
(template override)
|
||||
template)]
|
||||
[:fieldset.level-left {:disabled is-disabled?
|
||||
:style {:padding "0.5em 1em"}
|
||||
:class (when-not extant?
|
||||
"has-background-info-light")}
|
||||
(for [[idx template] (map vector (range ) template)]
|
||||
^{:key idx}
|
||||
[:div.level-item
|
||||
template])
|
||||
(when-not (and disable-remove?
|
||||
extant?)
|
||||
[:div.level-item
|
||||
[:a.button.level-item
|
||||
{:disabled is-disabled?
|
||||
:on-click (fn []
|
||||
(on-change (into []
|
||||
(for [[idx item] (map vector (range) prop-value)
|
||||
:when (not= idx i)]
|
||||
item))))}
|
||||
[:span.icon [:span.icon-remove]]]])])
|
||||
]]]))
|
||||
(when-not disable-new?
|
||||
[:button.button.is-outline
|
||||
{:type "button"
|
||||
:on-click (fn [e]
|
||||
(println "ADDING" prop-value)
|
||||
(on-change (conj prop-value {::key (random-uuid)}))
|
||||
(.stopPropagation e)
|
||||
(.preventDefault e))}
|
||||
[:span.icon [:i.fa.fa-plus]]
|
||||
[:span (or new-text "New")]])]]]))
|
||||
|
||||
|
||||
(defn multi-field-v2 []
|
||||
(into
|
||||
[:f> multi-field-v2-internal
|
||||
(reagent/props (reagent/current-component))]
|
||||
(reagent/children (reagent/current-component))))
|
||||
@@ -1,43 +0,0 @@
|
||||
(ns auto-ap.views.components.number-filter
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead]]
|
||||
[auto-ap.views.utils :refer [bind-field date-picker date->str local-now standard]]
|
||||
[cljs-time.core :as t]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(defn dispatch-change [on-change-event start end]
|
||||
(fn [_]
|
||||
(re-frame/dispatch (into on-change-event [[:start] start]) )
|
||||
(re-frame/dispatch (into on-change-event [[:end] end]))))
|
||||
|
||||
(defn number-filter [{:keys [value on-change-event]}]
|
||||
[:div
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[bind-field
|
||||
[date-picker {:class-name "input is-fullwidth"
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:placeholder-text "Start"
|
||||
:next-month-button-label ""
|
||||
:next-month-label ""
|
||||
:event on-change-event
|
||||
:type "date"
|
||||
:field [:start]
|
||||
:subscription value}]]]
|
||||
[:div.control
|
||||
[bind-field
|
||||
[date-picker {:class-name "input is-fullwidth"
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:placeholder-text "End"
|
||||
:next-month-button-label ""
|
||||
:event on-change-event
|
||||
:next-month-label ""
|
||||
:type "date"
|
||||
:field [:end]
|
||||
:subscription value}]]]]])
|
||||
44
src/cljs/auto_ap/views/components/number.cljs
Normal file
44
src/cljs/auto_ap/views/components/number.cljs
Normal file
@@ -0,0 +1,44 @@
|
||||
(ns auto-ap.views.components.number
|
||||
(:require [react :as react]
|
||||
[reagent.core :as r]))
|
||||
|
||||
|
||||
(defn number-internal [props]
|
||||
|
||||
(let [[text set-text ] (react/useState (some-> props :value str))
|
||||
[value set-value ] (react/useState (some-> props :value))
|
||||
coerce-value (fn [new-value]
|
||||
(let [new-value (js/parseInt new-value)]
|
||||
(cond
|
||||
(nil? new-value)
|
||||
nil
|
||||
|
||||
(js/Number.isNaN new-value)
|
||||
nil
|
||||
|
||||
:else
|
||||
new-value)))]
|
||||
(react/useEffect (fn []
|
||||
(let [prop-value (:value props)]
|
||||
(when (not (= prop-value value))
|
||||
(set-value prop-value)
|
||||
(if prop-value
|
||||
(set-text (str prop-value))
|
||||
(set-text ""))))))
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[:input.input (assoc props
|
||||
:on-change (fn [e]
|
||||
((:on-change props)
|
||||
(coerce-value (.. e -target -value))))
|
||||
:value text
|
||||
:type "number"
|
||||
:step "1"
|
||||
:style (or (:style props)
|
||||
{:width "5em"})
|
||||
:size 3)]]]))
|
||||
|
||||
(defn number-input []
|
||||
[:f> number-internal
|
||||
(r/props (r/current-component))])
|
||||
|
||||
@@ -1,34 +1,21 @@
|
||||
(ns auto-ap.views.components.number-filter
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.views.utils :refer [bind-field date-picker date->str local-now standard]]
|
||||
[cljs-time.core :as t]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(defn dispatch-change [on-change-event start end]
|
||||
(fn [_]
|
||||
(re-frame/dispatch (into on-change-event [[:start] start]) )
|
||||
(re-frame/dispatch (into on-change-event [[:end] end]))))
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
(defn number-filter [{:keys [value on-change-event]}]
|
||||
[:div.field
|
||||
[:div.control
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[bind-field
|
||||
[:input.input {:type "number"
|
||||
:placeholder ">="
|
||||
:field [:amount-gte]
|
||||
:step "0.01"
|
||||
:event on-change-event
|
||||
:subscription value}]]]
|
||||
[:div.column
|
||||
[bind-field
|
||||
[:input.input {:type "number"
|
||||
:placeholder "<="
|
||||
:field [:amount-lte]
|
||||
:event on-change-event
|
||||
:step "0.01"
|
||||
:subscription value}]]]]]]
|
||||
[form-builder/virtual-builder {:value (or value {})
|
||||
:on-change (fn [v]
|
||||
(re-frame/dispatch (conj on-change-event v)))}
|
||||
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field :amount-gte}
|
||||
[com/money-input {:placeholder ">="}]]]]
|
||||
[:div.column
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field :amount-lte}
|
||||
[com/money-input {:placeholder "<="}]]]]]]
|
||||
)
|
||||
|
||||
74
src/cljs/auto_ap/views/components/percentage_field.cljs
Normal file
74
src/cljs/auto_ap/views/components/percentage_field.cljs
Normal file
@@ -0,0 +1,74 @@
|
||||
(ns auto-ap.views.components.percentage-field
|
||||
(:require [reagent.core :as r]
|
||||
[auto-ap.views.utils :refer [->short$]]
|
||||
[clojure.string :as str]
|
||||
[react :as react]))
|
||||
(def good-% #"^\d{1,3}$")
|
||||
|
||||
(defn -percentage-field [{:keys [min max disabled on-blur on-change value class style placeholder]}]
|
||||
(let [[ parsed-amount set-parsed-amount] (react/useState {:parsed value
|
||||
:raw (cond
|
||||
(str/blank? value)
|
||||
""
|
||||
|
||||
(js/Number.isNaN (js/parseInt value))
|
||||
""
|
||||
|
||||
:else
|
||||
(str (js/parseInt value)))})]
|
||||
(react/useEffect (fn []
|
||||
;; allow the controlling field to change the raw representation
|
||||
;; when the raw amount is a valid representation, so that 33.
|
||||
;; doesn't get unset
|
||||
(when (or
|
||||
(and (:raw parsed-amount)
|
||||
(re-find good-% (:raw parsed-amount)))
|
||||
(str/blank? (:raw parsed-amount)))
|
||||
(set-parsed-amount
|
||||
(assoc parsed-amount
|
||||
:parsed value
|
||||
:raw (cond
|
||||
(str/blank? value)
|
||||
""
|
||||
|
||||
(js/Number.isNaN (js/parseInt value))
|
||||
""
|
||||
|
||||
:else
|
||||
(str (js/parseInt value))))))
|
||||
nil))
|
||||
[:div.control.has-icons-left
|
||||
[:input.input {:type "text"
|
||||
:disabled disabled
|
||||
:placeholder placeholder
|
||||
:class class
|
||||
:on-change (fn [e]
|
||||
(let [raw (.. e -target -value)
|
||||
new-value (when (and raw
|
||||
(not (str/blank? raw))
|
||||
(re-find good-% raw))
|
||||
(js/parseFloat raw))]
|
||||
(set-parsed-amount {:raw raw
|
||||
:parsed new-value})
|
||||
(when (not= value new-value)
|
||||
(on-change new-value))))
|
||||
:value (or (:raw parsed-amount)
|
||||
"")
|
||||
:on-blur (fn []
|
||||
(when-not (re-find good-% (:raw parsed-amount))
|
||||
|
||||
(set-parsed-amount {:raw ""
|
||||
:parsed nil})
|
||||
(on-change nil))
|
||||
(when on-blur
|
||||
(on-blur)))
|
||||
:min min
|
||||
:max max
|
||||
:step "0.01"
|
||||
:style (or style
|
||||
{:width "8em"})}]
|
||||
[:span.icon.is-left
|
||||
[:i.fa.fa-percent]]]))
|
||||
|
||||
(defn percentage-field []
|
||||
[:f> -percentage-field (r/props (r/current-component))])
|
||||
@@ -3,12 +3,34 @@
|
||||
[downshift :as ds :refer [useCombobox]]
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.views.utils :refer [with-user]]
|
||||
[react-popper :refer [usePopper] :as react-popper]
|
||||
[reagent.core :as r]
|
||||
|
||||
[clojure.string :as str]
|
||||
[react :as react]))
|
||||
|
||||
(set! *warn-on-infer* true)
|
||||
|
||||
(defn popper-test-internal [props children]
|
||||
(let [[reference-element set-reference-element] (react/useState nil)
|
||||
[popper-element set-popper-element] (react/useState nil)
|
||||
^js/Popper use (usePopper reference-element popper-element
|
||||
#js {:placement "bottom-start"
|
||||
:strategy "fixed"})
|
||||
popper-props (into {:ref set-popper-element
|
||||
:style (assoc (js->clj (.. use -styles -popper))
|
||||
:z-index "1000")
|
||||
}
|
||||
(js->clj (.. use -attributes -popper)))]
|
||||
[:<>
|
||||
[:div {:ref set-reference-element}]
|
||||
[:div popper-props
|
||||
(into [:div {:class (:class props)}] children)]]))
|
||||
|
||||
(defn popper []
|
||||
[:f> popper-test-internal (r/props (r/current-component))
|
||||
(r/children (r/current-component))])
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::search-completed
|
||||
(fn [_ [_ set-items set-loading-status result]]
|
||||
@@ -61,9 +83,8 @@
|
||||
:time 250
|
||||
:key ::input-value-settled}})))
|
||||
|
||||
(defn typeahead-v3-internal [{:keys [class entity->text entities on-input-change style ^js on-change disabled value name auto-focus] :or {disabled false}
|
||||
prop-value :value
|
||||
:as i}]
|
||||
(defn typeahead-v3-internal [{:keys [class entity->text entities on-input-change style ^js on-change disabled value name auto-focus on-blur] :or {disabled false}
|
||||
prop-value :value}]
|
||||
(let [[items set-items] (react/useState (or entities
|
||||
[]))
|
||||
[focused set-focus] (react/useState (boolean auto-focus))
|
||||
@@ -113,13 +134,16 @@
|
||||
|
||||
focused
|
||||
(conj "is-focused")
|
||||
)}
|
||||
)
|
||||
}
|
||||
(when selectedItem
|
||||
^{:key "hidden"}
|
||||
[:div.level-item
|
||||
[:div.control
|
||||
[:div.tags.has-addons
|
||||
[:span.tag (:name selectedItem)]
|
||||
[:span.tag (if entity->text
|
||||
(entity->text selectedItem)
|
||||
(:name selectedItem))]
|
||||
(when name
|
||||
[:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}])
|
||||
(when-not disabled
|
||||
@@ -142,22 +166,25 @@
|
||||
:disabled disabled
|
||||
|
||||
:onFocus #(set-focus true)
|
||||
:onBlur #(set-focus false)
|
||||
:onBlur #(do (set-focus false)
|
||||
(when on-blur
|
||||
(on-blur)))
|
||||
:autoFocus (if auto-focus
|
||||
"autoFocus"
|
||||
"")}))]]]
|
||||
[:div {:class (when (and isOpen (seq items))
|
||||
"typeahead-menu")}
|
||||
[:ul (js->clj (getMenuProps))
|
||||
(when (and isOpen (seq items))
|
||||
(for [[index item] (map vector (range) (js->clj items :keywordize-keys true))]
|
||||
^{:key item}
|
||||
[:li.typeahead-suggestion (assoc (js->clj (getItemProps #js {:item item :index index}))
|
||||
:class (when (= index highlightedIndex)
|
||||
"typeahead-highlighted"))
|
||||
(if entity->text
|
||||
(entity->text item)
|
||||
(:name item))]))]]]]))
|
||||
[:div (js->clj (getMenuProps))
|
||||
(when (and isOpen (seq items))
|
||||
[popper {:class "typeahead-menu"}
|
||||
[:ul
|
||||
(when (and isOpen (seq items))
|
||||
(for [[index item] (map vector (range) (js->clj items :keywordize-keys true))]
|
||||
^{:key item}
|
||||
[:li.typeahead-suggestion (assoc (js->clj (getItemProps #js {:item item :index index}))
|
||||
:class (when (= index highlightedIndex)
|
||||
"typeahead-highlighted"))
|
||||
(if entity->text
|
||||
(entity->text item)
|
||||
(:name item))]))]])]]]))
|
||||
|
||||
(defn search-backed-typeahead [{:keys [search-query] :as props}]
|
||||
[:div
|
||||
@@ -174,11 +201,10 @@
|
||||
:on-input-change
|
||||
(fn [input set-items]
|
||||
(if entities-by-id
|
||||
(do
|
||||
(set-items
|
||||
(into-array
|
||||
(->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
|
||||
(take 10)))))
|
||||
(set-items
|
||||
(into-array
|
||||
(->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
|
||||
(take 10))))
|
||||
|
||||
(set-items (into-array
|
||||
(take 10 (filter (fn [x] (str/includes? (or (some-> (entity->text x) str/lower-case) "")
|
||||
|
||||
@@ -1,33 +1,72 @@
|
||||
(ns auto-ap.views.components.vendor-dialog
|
||||
(:require
|
||||
[auto-ap.entities.contact :as contact]
|
||||
[auto-ap.entities.vendors :as entity]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.address :refer [address2-field]]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.multi :refer [multi-field-v2]]
|
||||
[auto-ap.views.components.number :refer [number-input]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.pages.admin.vendors.common :as common]
|
||||
[auto-ap.views.utils
|
||||
:refer [dispatch-event multi-field str->int with-is-admin? with-user]]
|
||||
[clojure.spec.alpha :as s]
|
||||
:refer [dispatch-event str->int with-is-admin? with-user]]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]))
|
||||
|
||||
;; Remaining cleanup todos:
|
||||
;; test minification
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::vendor-form]
|
||||
(fn [form]
|
||||
(s/valid? ::entity/vendor (:data form))))
|
||||
(def terms-override-schema (m/schema [:map
|
||||
[:client schema/reference]
|
||||
[:terms :int]]))
|
||||
|
||||
(def automatically-paid-schema (m/schema [:map
|
||||
[:client schema/reference]]))
|
||||
|
||||
(def schedule-payment-dom-schema (m/schema [:map
|
||||
[:client schema/reference]
|
||||
[:dom [:int {:max 30}]]]))
|
||||
|
||||
(def account-override-schema (m/schema [:map
|
||||
[:client schema/reference]
|
||||
[:account schema/reference]]))
|
||||
|
||||
(def schema (m/schema [:map [:name schema/not-empty-string]
|
||||
[:print-as {:optional true}
|
||||
[:maybe :string]]
|
||||
[:hidden {:optional true}
|
||||
[:maybe :boolean]]
|
||||
[:terms {:optional true}
|
||||
[:maybe :int]]
|
||||
[:terms-overrides {:optional true}
|
||||
[:maybe [:sequential terms-override-schema]]]
|
||||
[:schedule-payment-dom {:optional true}
|
||||
[:maybe [:sequential schedule-payment-dom-schema]]]
|
||||
[:default-account schema/reference]
|
||||
[:account-overrides {:optional true}
|
||||
[:sequential account-override-schema]]
|
||||
[:legal-entity-first-name {:optional true}
|
||||
[:maybe :string]]
|
||||
[:legal-entity-middle-name {:optional true}
|
||||
[:maybe :string]]
|
||||
[:legal-entity-last-name {:optional true}
|
||||
[:maybe :string]]
|
||||
[:legal-entity-tin {:optional true}
|
||||
[:maybe :string]]
|
||||
[:legal-entity-tin-type {:optional true}
|
||||
[:or [:maybe :string]
|
||||
[:maybe keyword?]]]
|
||||
[:legal-entity-1099-type {:optional true}
|
||||
[:or [:maybe :string]
|
||||
[:maybe keyword?]]]]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save-complete
|
||||
@@ -39,13 +78,13 @@
|
||||
::save
|
||||
[with-user with-is-admin? (forms/triggers-loading ::vendor-form) (forms/in-form ::vendor-form)]
|
||||
(fn [{:keys [user is-admin?] {{:keys [name hidden print-as terms invoice-reminder-schedule primary-contact automatically-paid-when-due schedule-payment-dom secondary-contact address default-account terms-overrides account-overrides id legal-entity-tin legal-entity-tin-type legal-entity-first-name legal-entity-last-name legal-entity-middle-name legal-entity-1099-type] :as data} :data} :db} _]
|
||||
(when (s/valid? ::entity/vendor data)
|
||||
(if (m/validate schema data)
|
||||
(let [query [:upsert-vendor
|
||||
{:vendor (cond-> {:id id
|
||||
:name name
|
||||
:print-as print-as
|
||||
:terms (or (str->int terms)
|
||||
nil)
|
||||
:terms (or terms
|
||||
nil)
|
||||
:default-account-id (:id default-account)
|
||||
:address address
|
||||
:primary-contact primary-contact
|
||||
@@ -75,12 +114,11 @@
|
||||
(comp :id :client)
|
||||
automatically-paid-when-due)
|
||||
:legal-entity-first-name legal-entity-first-name
|
||||
:legal-entity-middle-name legal-entity-middle-name
|
||||
:legal-entity-middle-name legal-entity-middle-name
|
||||
:legal-entity-last-name legal-entity-last-name
|
||||
:legal-entity-tin legal-entity-tin
|
||||
:legal-entity-tin-type (some-> legal-entity-tin-type clojure.core/name not-empty keyword)
|
||||
:legal-entity-1099-type (some-> legal-entity-1099-type clojure.core/name not-empty keyword)
|
||||
))}
|
||||
:legal-entity-1099-type (some-> legal-entity-1099-type clojure.core/name not-empty keyword)))}
|
||||
common/default-read]]
|
||||
{ :graphql
|
||||
{:token user
|
||||
@@ -88,7 +126,10 @@
|
||||
:query-obj {:venia/operation
|
||||
{:operation/type :mutation
|
||||
:operation/name "UpsertVendor"} :venia/queries [{:query/data query}]}
|
||||
:on-success [::save-complete]}}))))
|
||||
:on-success [::save-complete]}})
|
||||
|
||||
{:dispatch-n [[::forms/attempted-submit ::vendor-form]
|
||||
[::status/error ::vendor-form [{:message "Please fix the errors and try again."}]]]})))
|
||||
|
||||
(defn pull-left []
|
||||
(into [:div {:style {:position "relative"
|
||||
@@ -103,10 +144,8 @@
|
||||
[form-builder/vertical-control {:is-small? true}
|
||||
"Name"
|
||||
[:div.control.has-icons-left
|
||||
[form-builder/raw-field
|
||||
[:input.input.is-expanded {:type "text"
|
||||
:field :name
|
||||
:spec ::contact/name}]]
|
||||
[form-builder/raw-field-v2 {:field :name}
|
||||
[:input.input.is-expanded {:type "text"}]]
|
||||
[:span.icon.is-small.is-left
|
||||
[:i.fa.fa-user]]]]
|
||||
[form-builder/vertical-control {:is-small? true}
|
||||
@@ -115,201 +154,165 @@
|
||||
[:div.control.has-icons-left
|
||||
[:span.icon.is-small.is-left
|
||||
[:i.fa.fa-envelope]]
|
||||
[form-builder/raw-field
|
||||
[:input.input {:type "email"
|
||||
:field :email
|
||||
:spec ::contact/email}]]]]
|
||||
[form-builder/raw-field-v2 {:field :email}
|
||||
[:input.input {:type "email"}]]]]
|
||||
[form-builder/vertical-control {:is-small? true}
|
||||
"Phone"
|
||||
[:div.control.has-icons-left
|
||||
[form-builder/raw-field
|
||||
[:input.input {:type "phone"
|
||||
:field :phone
|
||||
:spec ::contact/phone}]]
|
||||
[form-builder/raw-field-v2 {:field :phone}
|
||||
[:input.input {:type "phone"}]]
|
||||
[:span.icon.is-small.is-left
|
||||
[:i.fa.fa-phone]]]]]]])
|
||||
|
||||
|
||||
|
||||
(defn form-content [{:keys [data]}]
|
||||
(let [is-admin? @(re-frame/subscribe [::subs/is-admin?])
|
||||
clients @(re-frame/subscribe [::subs/clients])]
|
||||
clients @(re-frame/subscribe [::subs/client-refs])]
|
||||
[form-builder/builder {:submit-event [::save]
|
||||
:can-submit [::can-submit]
|
||||
:id ::vendor-form}
|
||||
[form-builder/field
|
||||
[:span "Name " [:span.has-text-danger "*"]]
|
||||
[:input.input {:type "text"
|
||||
:auto-focus true
|
||||
:field :name
|
||||
:spec ::entity/name}]]
|
||||
:id ::vendor-form
|
||||
:schema schema}
|
||||
[form-builder/field-v2 {:field :name
|
||||
:required true}
|
||||
"Name"
|
||||
[:input.input {:auto-focus true}]]
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :print-as}
|
||||
"Print Checks As"
|
||||
[:input.input {:type "text"
|
||||
:field :print-as
|
||||
:spec ::entity/print-as}]]
|
||||
[:input.input]]
|
||||
(when is-admin?
|
||||
[:div.field
|
||||
[:label.checkbox
|
||||
[form-builder/raw-field
|
||||
[:input {:type "checkbox"
|
||||
:field [:hidden]
|
||||
:spec ::entity/hidden}]]
|
||||
" Hidden"]])
|
||||
[form-builder/raw-field-v2 {:field :hidden}
|
||||
[com/checkbox {:label "Hidden"}]])
|
||||
|
||||
[form-builder/section {:title "Terms"}
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :terms}
|
||||
"Terms"
|
||||
[:input.input {:type "number"
|
||||
:step "1"
|
||||
:style {:width "4em"}
|
||||
:field [:terms]
|
||||
:size 3
|
||||
:spec ::entity/terms}]]
|
||||
[number-input ]]
|
||||
(when is-admin?
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field [:terms-overrides]}
|
||||
"Overrides"
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:terms-overrides]
|
||||
:template [[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}
|
||||
:type "typeahead-v3"
|
||||
:field [:client]}]
|
||||
[:input.input {:type "number"
|
||||
:step "1"
|
||||
:style {:width "4em"}
|
||||
:field [:terms]
|
||||
:size 3
|
||||
:spec ::entity/terms}]]}]])
|
||||
]
|
||||
[multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:client]}
|
||||
[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}
|
||||
:type "typeahead-v3"
|
||||
}]]
|
||||
[form-builder/raw-field-v2 {:field :terms}
|
||||
[number-input]]]
|
||||
:schema [:sequential terms-override-schema]
|
||||
:key-fn :id
|
||||
:next-key (random-uuid)
|
||||
:new-text "New Terms Override"}]])]
|
||||
|
||||
(when is-admin?
|
||||
[form-builder/section {:title "Schedule payment when due"}
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field [:automatically-paid-when-due]}
|
||||
"Client"
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:automatically-paid-when-due]
|
||||
:template [[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}
|
||||
:type "typeahead-v3"
|
||||
:field [:client]}]]}]]])
|
||||
[multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
|
||||
[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}}]]]
|
||||
:schema [:sequential automatically-paid-schema]
|
||||
:key-fn :id
|
||||
:next-key (random-uuid)
|
||||
:new-text "Schedule another client"}]]])
|
||||
|
||||
(when is-admin?
|
||||
[form-builder/section {:title "Schedule payment on day of month"}
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field [:schedule-payment-dom]}
|
||||
"Overrides"
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:schedule-payment-dom]
|
||||
:template [[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}
|
||||
:type "typeahead-v3"
|
||||
:field [:client]}]
|
||||
[:input.input {:type "number"
|
||||
:step "1"
|
||||
:style {:width "4em"}
|
||||
:field [:dom]
|
||||
:size 3
|
||||
:spec ::entity/terms}]]}]]])
|
||||
[multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
|
||||
[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}}]]
|
||||
[form-builder/raw-field-v2 {:field :dom}
|
||||
[number-input]]]
|
||||
:schema [:sequential schedule-payment-dom-schema]
|
||||
:key-fn :id
|
||||
:next-key (random-uuid)
|
||||
:new-text "Schedule another client"}]]])
|
||||
[form-builder/section {:title "Expense Accounts"}
|
||||
[form-builder/field
|
||||
"Default *"
|
||||
[form-builder/field-v2 {:field :default-account
|
||||
:required? true}
|
||||
"Default"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:style {:width "19em"}
|
||||
:field [:default-account]}]]
|
||||
:style {:width "19em"}}]]
|
||||
|
||||
(when is-admin?
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field [:account-overrides]}
|
||||
"Overrides"
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:account-overrides]
|
||||
:template (fn [entity]
|
||||
[[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "19em"}
|
||||
:type "typeahead-v3"
|
||||
:field [:client]}]
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i
|
||||
:client_id (:id (:client entity))}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:style {:width "15em"}
|
||||
:field [:account]}]])}]])]
|
||||
[multi-field-v2 {:template (fn [entity]
|
||||
[[form-builder/raw-field-v2 {:field :client}
|
||||
[typeahead-v3 {:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "19em"}
|
||||
}]]
|
||||
[form-builder/raw-field-v2 {:field :account}
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i
|
||||
:client_id (:id (:client entity))}
|
||||
[:name :id]])
|
||||
:style {:width "15em"}}]]])
|
||||
:schema [:sequential account-override-schema]
|
||||
:key-fn :id
|
||||
:next-key (random-uuid)
|
||||
:new-text "Add override"}]])]
|
||||
|
||||
[form-builder/with-scope {:scope [:address ]}
|
||||
[form-builder/section {:title "Address"}
|
||||
[:div {:style {:width "30em"}}
|
||||
[form-builder/section {:title "Address"}
|
||||
[:div {:style {:width "30em"}}
|
||||
[form-builder/raw-field-v2 {:field :address}
|
||||
[address2-field]]]]
|
||||
|
||||
|
||||
[form-builder/section {:title "Contact"}
|
||||
[contact-field {:name "Primary"
|
||||
:field [:primary-contact]}]
|
||||
[contact-field {:name "Secondary"
|
||||
:field [:secondary-contact]}]]
|
||||
|
||||
|
||||
(when is-admin?
|
||||
[form-builder/section {:title "Legal Entity"}
|
||||
[form-builder/vertical-control
|
||||
"Name"
|
||||
[left-stack
|
||||
[left-stack
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[form-builder/raw-field-v2 {:field :legal-entity-first-name}
|
||||
[:input.input {:type "text"
|
||||
:placeholder "First Name"
|
||||
:field [:legal-entity-first-name]
|
||||
:spec ::contact/name}]]]
|
||||
:placeholder "First Name"}]]]
|
||||
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[form-builder/raw-field-v2 {:field :legal-entity-middle-name}
|
||||
[:input.input {:type "text"
|
||||
:placeholder "Middle Name"
|
||||
:field [:legal-entity-middle-name]
|
||||
:spec ::contact/name}]]]
|
||||
:placeholder "Middle Name"}]]]
|
||||
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[form-builder/raw-field-v2 {:field :legal-entity-last-name}
|
||||
[:input.input {:type "text"
|
||||
:placeholder "Last Name"
|
||||
:field [:legal-entity-last-name]
|
||||
:spec ::contact/name}]]]]]
|
||||
:placeholder "Last Name"}]]]]]
|
||||
[form-builder/vertical-control
|
||||
"TIN"
|
||||
[left-stack
|
||||
[form-builder/raw-field
|
||||
[left-stack
|
||||
[form-builder/raw-field-v2 {:field :legal-entity-tin}
|
||||
[:input.input {:type "text"
|
||||
:placeholder "SSN or EIN"
|
||||
:field [:legal-entity-tin]
|
||||
:size "12"
|
||||
:spec ::contact/name}]]
|
||||
}]]
|
||||
|
||||
[:div.control
|
||||
[:div.select
|
||||
[form-builder/raw-field
|
||||
[:select {:type "select"
|
||||
:field [:legal-entity-tin-type]}
|
||||
[:option {:value nil} ""]
|
||||
[:option {:value "ein"} "EIN"]
|
||||
[:option {:value "ssn"} "SSN"]]]]]]]
|
||||
[form-builder/raw-field-v2 {:field :legal-entity-tin-type}
|
||||
[com/select-field {:options [["ein" "EIN"]
|
||||
["ssn" "SSN"]]
|
||||
:allow-nil? true}]]]]]
|
||||
|
||||
[form-builder/vertical-control
|
||||
[form-builder/field-v2 {:field :legal-entity-1099-type}
|
||||
"1099 Type"
|
||||
[:div.control
|
||||
[:div.select
|
||||
[form-builder/raw-field
|
||||
[:select {:type "select"
|
||||
:field [:legal-entity-1099-type]}
|
||||
[:option {:value nil} ""]
|
||||
[:option {:value "none"} "Don't 1099"]
|
||||
[:option {:value "misc"} "Misc"]
|
||||
[:option {:value "landlord"} "Landlord"]]]]]]])
|
||||
[com/select-field {:options [["none" "Don't 1099"]
|
||||
["misc" "Misc"]
|
||||
["landlord" "Landlord"]]
|
||||
:allow-nil? true}]]])
|
||||
[form-builder/hidden-submit-button]]))
|
||||
|
||||
(defn vendor-dialog [ ]
|
||||
@@ -321,36 +324,32 @@
|
||||
::vendor-selected
|
||||
[with-user (forms/in-form ::select-vendor-form)]
|
||||
(fn [{{:keys [data]} :db :keys [user]} _]
|
||||
{:graphql {:token user
|
||||
:query-obj {:venia/queries [[:vendor-by-id
|
||||
{:id (:id (:vendor data))}
|
||||
common/default-read]]}
|
||||
:owns-state {:single ::select-vendor-form}
|
||||
:on-success (fn [r]
|
||||
[::started (:vendor-by-id r)])}}))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit-select-vendor-form
|
||||
:<- [::forms/field ::select-vendor-form [:vendor]]
|
||||
(fn [vendor]
|
||||
(if vendor
|
||||
true
|
||||
false)))
|
||||
|
||||
(if (:vendor data)
|
||||
{:graphql {:token user
|
||||
:query-obj {:venia/queries [[:vendor-by-id
|
||||
{:id (:id (:vendor data))}
|
||||
common/default-read]]}
|
||||
:owns-state {:single ::select-vendor-form}
|
||||
:on-success (fn [r]
|
||||
[::started (:vendor-by-id r)])}}
|
||||
{:dispatch-n [[::forms/attempted-submit ::select-vendor-form]
|
||||
[::status/error ::select-vendor-form [{:message "Please select a vendor."}]]]})))
|
||||
|
||||
(defn select-vendor-form-content []
|
||||
[form-builder/builder {:submit-event [::vendor-selected]
|
||||
:can-submit [::can-submit-select-vendor-form]
|
||||
:id ::select-vendor-form}
|
||||
[form-builder/field
|
||||
:id ::select-vendor-form
|
||||
:validation-error-string "Please select a vendor."
|
||||
:schema [:map
|
||||
[:vendor schema/reference]]}
|
||||
[form-builder/field-v2 {:field :vendor
|
||||
:required? true}
|
||||
"Vendor to edit"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:auto-focus true
|
||||
:field [:vendor]}]]
|
||||
:style {:width "20em"}
|
||||
:auto-focus true}]]
|
||||
[form-builder/hidden-submit-button]])
|
||||
|
||||
|
||||
@@ -360,6 +359,7 @@
|
||||
(fn [{:keys [db]} [_ vendor]]
|
||||
{:db (-> db (forms/start-form ::vendor-form (-> vendor
|
||||
(update :automatically-paid-when-due #(mapv (fn [apwd]
|
||||
apwd
|
||||
{:id (:id apwd)
|
||||
:client apwd})
|
||||
%))
|
||||
@@ -370,11 +370,11 @@
|
||||
{:title "Vendor"
|
||||
:body [vendor-dialog]
|
||||
:class "semi-wide"
|
||||
:status-from [::status/single ::vendor-form]
|
||||
:confirm {:value "Save Vendor"
|
||||
:status-from [::status/single ::vendor-form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::save])
|
||||
:can-submit [::can-submit]
|
||||
:close-event [::status/completed ::vendor-form]}}]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
@@ -388,5 +388,4 @@
|
||||
:status-from [::status/single ::select-vendor-form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::vendor-selected])
|
||||
:can-submit [::can-submit-select-vendor-form]
|
||||
:close-event [::status/completed ::select-vendor-form]}}]}))
|
||||
|
||||
@@ -1,7 +1,3 @@
|
||||
(ns auto-ap.views.components.vendor-filter
|
||||
(:require
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.views.utils :refer [bind-field]]))
|
||||
(ns auto-ap.views.components.vendor-filter)
|
||||
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
[auto-ap.views.pages.admin.users :refer [admin-users-page]]
|
||||
[auto-ap.views.pages.admin.import-batches :refer [import-batches-page]]
|
||||
[auto-ap.views.pages.admin.yodlee2 :as yodlee2]
|
||||
[auto-ap.views.pages.admin.plaid :as plaid]))
|
||||
[auto-ap.views.pages.company.plaid :as plaid]))
|
||||
|
||||
(defmulti page (fn [active-page] active-page))
|
||||
(defmethod page :unpaid-invoices [_]
|
||||
@@ -112,8 +112,8 @@
|
||||
(defmethod page :admin-yodlee2 [_]
|
||||
(yodlee2/admin-yodle-provider-accounts-page))
|
||||
|
||||
(defmethod page :admin-plaid [_]
|
||||
(plaid/admin-plaid-page))
|
||||
(defmethod page :plaid [_]
|
||||
(plaid/plaid-page))
|
||||
|
||||
(defmethod page :admin-accounts [_]
|
||||
(admin-accounts-page))
|
||||
@@ -144,8 +144,9 @@
|
||||
(let [ap (re-frame/subscribe [::subs/active-page])
|
||||
current-client @(re-frame/subscribe [::subs/client])
|
||||
is-loading? @(re-frame/subscribe [::subs/is-initial-loading?])]
|
||||
(if is-loading?
|
||||
[loading-layout]
|
||||
|
||||
[:div
|
||||
^{:key (str @ap "-" current-client)} [page @ap]])))
|
||||
(when @ap
|
||||
(if is-loading?
|
||||
[loading-layout]
|
||||
|
||||
[:div
|
||||
^{:key (str @ap "-" current-client)} [page @ap]]))))
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
[buttons/new-button {:name "Account"
|
||||
:class "is-primary"
|
||||
:event [::account-form/editing
|
||||
{:type :asset
|
||||
:account-set "default"}]}]]
|
||||
{:account-set "default"}]}]]
|
||||
[table/accounts-table {:data-page ::page}]])
|
||||
|
||||
(defn admin-accounts-page []
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
(ns auto-ap.views.pages.admin.accounts.form
|
||||
(:require [auto-ap.entities.account :as entity]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.layouts :refer [side-bar]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.utils :refer [dispatch-event multi-field]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]))
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.layouts :refer [side-bar]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.utils :refer [dispatch-event with-user]]
|
||||
[clojure.string :as str]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]
|
||||
[vimsical.re-frame.cofx.inject :as inject]))
|
||||
|
||||
(def types [:dividend :expense :asset :liability :equity :revenue])
|
||||
(def applicabilities [:global :optional :customized])
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-sub
|
||||
::request
|
||||
:<- [::forms/form ::form]
|
||||
@@ -35,11 +37,7 @@
|
||||
(:name client-override))})
|
||||
client-overrides)}))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::request]
|
||||
(fn [request]
|
||||
(s/valid? ::entity/account request)))
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::editing
|
||||
@@ -51,103 +49,98 @@
|
||||
(re-frame/reg-event-fx
|
||||
::edited
|
||||
[(forms/triggers-saved ::form :upsert-account)]
|
||||
(fn [{:keys [db]} [_ {:keys [upsert-account]}]]))
|
||||
(fn [_ [_ _]]))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::add-client-override
|
||||
[(forms/in-form ::form)]
|
||||
(fn [form]
|
||||
(update form :data (fn [data]
|
||||
(-> data
|
||||
(update :client-overrides conj (:new-client-override data))
|
||||
(dissoc :new-client-override))))))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::saving
|
||||
(fn [{:keys [db]} _]
|
||||
(when @(re-frame/subscribe [::can-submit])
|
||||
(let [{{:keys [id type name numeric-code account-set]} :data :as data} @(re-frame/subscribe [::forms/form ::form])]
|
||||
{:db (forms/loading db ::form )
|
||||
:graphql
|
||||
{:token (-> db :user)
|
||||
:query-obj {:venia/operation {:operation/type :mutation
|
||||
:operation/name "UpsertAccount"}
|
||||
:venia/queries [{:query/data [:upsert-account
|
||||
{:account @(re-frame/subscribe [::request])}
|
||||
[:id :type :name :account-set :numeric-code :location :applicability [:client-overrides [:name :id [:client [:id :name]]]]]]}]}
|
||||
:on-success [::edited]
|
||||
:on-error [::forms/save-error ::form]}}))))
|
||||
[with-user (re-frame/inject-cofx ::inject/sub [::request]) ]
|
||||
(fn [{:keys [user] ::keys [request]} _]
|
||||
(let [_ @(re-frame/subscribe [::forms/form ::form])]
|
||||
{:graphql
|
||||
{:owns-state {:single ::form}
|
||||
:token user
|
||||
:query-obj {:venia/operation {:operation/type :mutation
|
||||
:operation/name "UpsertAccount"}
|
||||
:venia/queries [{:query/data [:upsert-account
|
||||
{:account request}
|
||||
[:id :type :name :account-set :numeric-code :location :applicability [:client-overrides [:name :id [:client [:id :name]]]]]]}]}
|
||||
:on-success [::edited]
|
||||
:on-error [::forms/save-error ::form]}})))
|
||||
|
||||
(def account-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::forms/change ::form]
|
||||
:submit-event [::saving]
|
||||
:id ::form}))
|
||||
(def account-customization-schema
|
||||
(m/schema
|
||||
[:map [:client schema/reference]
|
||||
[:name schema/not-empty-string]]))
|
||||
|
||||
(def account-schema
|
||||
(m/schema
|
||||
[:map
|
||||
[:numeric-code schema/integer-code]
|
||||
[:name schema/not-empty-string]
|
||||
[:type [:enum :dividend :expense :asset :liability :equity :revenue]]
|
||||
[:location {:optional true} [:maybe :string]]
|
||||
[:applicability [:enum :global :optional :customized]]
|
||||
[:client-overrides {:optional true}
|
||||
[:maybe [:sequential account-customization-schema]]]]))
|
||||
|
||||
(defn form [_]
|
||||
(let [{error :error account :data } @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field field-holder raw-field error-notification submit-button]} account-form]
|
||||
|
||||
^{:key (:id account)}
|
||||
(let [{account :data } @(re-frame/subscribe [::forms/form ::form])]
|
||||
[side-bar {:on-close (dispatch-event [::forms/form-closing ::form])}
|
||||
(form-inline {:title (if (:id account)
|
||||
"Edit account"
|
||||
"Add account")}
|
||||
[:<>
|
||||
[form-builder/builder {:change-event [::forms/change ::form]
|
||||
:submit-event [::saving]
|
||||
:id ::form
|
||||
:schema account-schema}
|
||||
[form-builder/section {:title (if (:id account)
|
||||
"Edit account"
|
||||
"Add account")}
|
||||
[form-builder/field-v2 {:field :numeric-code}
|
||||
"Code"
|
||||
[com/number-input
|
||||
{:disabled (boolean (:id account))
|
||||
:auto-focus (not (boolean (:id account)))
|
||||
:style {:width "9em"}}]]
|
||||
|
||||
(field "Account Set"
|
||||
[:input.input {:type "text"
|
||||
:field :account-set
|
||||
:disabled (boolean (:id account))
|
||||
:spec ::entity/account-set}])
|
||||
[form-builder/field-v2 {:field :name}
|
||||
"Name"
|
||||
[:input.input {:type "text"
|
||||
:auto-focus (boolean (:id account))}]]
|
||||
|
||||
(field "Code"
|
||||
[:input.input {:type "text"
|
||||
:field :numeric-code
|
||||
:disabled (boolean (:id account))
|
||||
:spec ::entity/numeric-code}])
|
||||
|
||||
(field "Name"
|
||||
[:input.input {:type "text"
|
||||
:field :name
|
||||
:spec ::entity/name}])
|
||||
|
||||
(field-holder "Account Type"
|
||||
[:div.select
|
||||
(raw-field
|
||||
[:select {:type "select"
|
||||
:field :type
|
||||
:spec (set types)}
|
||||
(map (fn [l]
|
||||
[:option {:value (name l)} (str/capitalize (name l))]) types)])])
|
||||
[form-builder/field-v2 {:field :type}
|
||||
"Account Type"
|
||||
[com/select-field {:options (map (fn [l]
|
||||
[l (str/capitalize (name l))])
|
||||
types)
|
||||
:allow-nil? true
|
||||
:keywordize? true}]]
|
||||
|
||||
|
||||
(field "Location"
|
||||
[:input.input.known-field.location {:type "text"
|
||||
:field :location
|
||||
:spec ::entity/location}])
|
||||
[form-builder/field-v2 {:field :location}
|
||||
"Location"
|
||||
[:input.input.known-field.location {:type "text"}]]
|
||||
|
||||
[:h2.subtitle "Client"]
|
||||
(field-holder "Applicability"
|
||||
[:div.select
|
||||
(raw-field
|
||||
[:select {:type "select"
|
||||
:field :applicability
|
||||
:spec (set applicabilities)}
|
||||
(map (fn [l]
|
||||
[:option {:value (name l)} (str/capitalize (name l))]) applicabilities)])])
|
||||
(field "Customizations"
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:client-overrides]
|
||||
:template [[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:style {:width "13em"}
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:field [:client]}]
|
||||
[:input.input {:type "text"
|
||||
:style {:width "15em"}
|
||||
:placeholder "Bubblegum"
|
||||
:field [:name]}]
|
||||
]}])
|
||||
(error-notification)
|
||||
[form-builder/section {:title "Client"}
|
||||
[:h2.subtitle "Client"]
|
||||
[form-builder/field-v2 {:field :applicability}
|
||||
"Applicability"
|
||||
[com/select-field {:options (map (fn [l]
|
||||
[l
|
||||
(str/capitalize (name l))])
|
||||
applicabilities)
|
||||
:allow-nil? true
|
||||
:keywordize? true}]]
|
||||
[form-builder/field-v2 {:field :client-overrides}
|
||||
"Customizations"
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:style {:width "13em"}
|
||||
:entity->text :name}]]
|
||||
[form-builder/raw-field-v2 {:field :name}
|
||||
[:input.input {:type "text"
|
||||
:style {:width "15em"}
|
||||
:placeholder "Bubblegum"}]]]
|
||||
:key-fn :id
|
||||
:schema [:sequential account-customization-schema]}]]]
|
||||
[form-builder/error-notification]
|
||||
|
||||
(submit-button "Save")])]))
|
||||
[form-builder/submit-button "Save"]]]]))
|
||||
|
||||
@@ -9,28 +9,58 @@
|
||||
[auto-ap.views.components.address :refer [address2-field]]
|
||||
[react-signature-canvas]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.views.components.level :refer [left-stack] :as level]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.utils
|
||||
:refer [date->str
|
||||
date-picker
|
||||
date-picker-friendly
|
||||
dispatch-event
|
||||
horizontal-field
|
||||
multi-field
|
||||
standard]]
|
||||
:refer [date-picker
|
||||
dispatch-event]]
|
||||
[bidi.bidi :as bidi]
|
||||
[cljs-time.coerce :as coerce]
|
||||
[cljs-time.core :as t]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]
|
||||
[react-signature-canvas]
|
||||
[vimsical.re-frame.cofx.inject :as inject]))
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[auto-ap.schema :as schema]
|
||||
[malli.core :as m]))
|
||||
|
||||
(def signature-canvas (r/adapt-react-class (.-default react-signature-canvas)))
|
||||
|
||||
(def location-schema (m/schema [:map
|
||||
[:location schema/not-empty-string]]))
|
||||
|
||||
(def square-location-schema (m/schema [:map
|
||||
[:square-location schema/reference]
|
||||
[:client-location schema/not-empty-string]]))
|
||||
|
||||
(def ezcater-schema (m/schema [:map
|
||||
[:caterer schema/reference]
|
||||
[:client-location schema/not-empty-string]]))
|
||||
|
||||
(def name-match-schema (m/schema [:map
|
||||
[:match schema/not-empty-string]]))
|
||||
(def location-match-schema (m/schema [:map
|
||||
[:match schema/not-empty-string]
|
||||
[:location schema/not-empty-string]]))
|
||||
(def email-schema [:map
|
||||
[:email schema/not-empty-string]
|
||||
[:description schema/not-empty-string]])
|
||||
|
||||
(def client-schema [:map
|
||||
[:name schema/not-empty-string]
|
||||
[:code schema/code-string]
|
||||
[:locations [:sequential location-schema]]
|
||||
[:emails {:optional true}
|
||||
[:maybe [:sequential email-schema]]]
|
||||
[:matches {:optional true}
|
||||
[:maybe [:sequential name-match-schema]]]
|
||||
[:location-matches {:optional true}
|
||||
[:maybe [:sequential location-match-schema]]]
|
||||
[:selected-square-locations {:optional true}
|
||||
[:maybe [:sequential square-location-schema]]]])
|
||||
|
||||
(defn upload-replacement-button [{:keys [on-change]} text]
|
||||
(let [button (atom nil)]
|
||||
(r/create-class {:display-name "Upload button"
|
||||
@@ -104,14 +134,6 @@
|
||||
[upload-replacement-button {:on-change on-change} "Upload signature"]]]))
|
||||
])))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::new-client-request]
|
||||
(fn [_ _]
|
||||
true
|
||||
|
||||
#_(s/valid? ::entity/client r)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::new-client-request
|
||||
:<- [::forms/form ::form]
|
||||
@@ -120,7 +142,8 @@
|
||||
{:id (:id new-client-data),
|
||||
:name (:name new-client-data)
|
||||
:code (:code new-client-data) ;; TODO add validation can't change
|
||||
:emails (:emails new-client-data)
|
||||
:emails (map #(select-keys % [:id :email :description])
|
||||
(:emails new-client-data))
|
||||
:square-auth-token (:square-auth-token new-client-data)
|
||||
:square-locations (map
|
||||
(fn [x]
|
||||
@@ -135,15 +158,7 @@
|
||||
:location (:location x)})
|
||||
(:ezcater-locations new-client-data))
|
||||
|
||||
:locked-until (cond (not (:locked-until new-client-data))
|
||||
nil
|
||||
|
||||
(instance? goog.date.Date (:locked-until new-client-data))
|
||||
(date->str (:locked-until new-client-data) standard)
|
||||
|
||||
:else
|
||||
(:locked-until new-client-data)
|
||||
)
|
||||
:locked-until (:locked-until new-client-data)
|
||||
:locations (mapv :location (:locations new-client-data))
|
||||
:matches (mapv :match (:matches new-client-data))
|
||||
:location-matches (:location-matches new-client-data)
|
||||
@@ -166,28 +181,16 @@
|
||||
:bank-accounts (map-indexed (fn [i {:keys [number name check-number plaid-account intuit-bank-account include-in-reports type id code numeric-code start-date bank-name routing bank-code new? sort-order visible yodlee-account-id locations yodlee-account use-date-instead-of-post-date]}]
|
||||
{:number number
|
||||
:name name
|
||||
:check-number (when-not (str/blank? check-number)
|
||||
(js/parseInt check-number))
|
||||
:numeric-code (when-not (str/blank? numeric-code)
|
||||
(js/parseInt numeric-code))
|
||||
:check-number check-number
|
||||
:numeric-code numeric-code
|
||||
:include-in-reports include-in-reports
|
||||
:start-date (cond (not start-date)
|
||||
nil
|
||||
|
||||
(instance? goog.date.Date start-date)
|
||||
(date->str start-date standard)
|
||||
|
||||
:else
|
||||
start-date
|
||||
)
|
||||
:start-date start-date
|
||||
:type type
|
||||
:id id
|
||||
:sort-order i
|
||||
:visible visible
|
||||
:locations (mapv :location locations)
|
||||
:use-date-instead-of-post-date use-date-instead-of-post-date
|
||||
:yodlee-account-id (when-not (str/blank? yodlee-account-id)
|
||||
(js/parseInt yodlee-account-id))
|
||||
:yodlee-account (:id yodlee-account)
|
||||
:plaid-account (:id plaid-account)
|
||||
:intuit-bank-account (:id intuit-bank-account)
|
||||
@@ -204,7 +207,7 @@
|
||||
:<- [::subs/route-params]
|
||||
:<- [::subs/clients-by-id]
|
||||
(fn [[rp clients-by-id]]
|
||||
(or (clients-by-id (:id rp))
|
||||
(or (get clients-by-id (:id rp))
|
||||
{})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
@@ -220,13 +223,16 @@
|
||||
{:id (:id sl)
|
||||
:square-location sl
|
||||
:client-location (:client-location sl)}))))
|
||||
(update :locations #(mapv (fn [l] {:location l}) %))
|
||||
(update :matches #(mapv (fn [l] {:match l}) %))
|
||||
(update :locations #(mapv (fn [l] {:location l
|
||||
:id (random-uuid)}) %))
|
||||
(update :matches #(mapv (fn [l] {:match l
|
||||
:id (random-uuid)}) %))
|
||||
(update :bank-accounts
|
||||
(fn [bas]
|
||||
(mapv (fn [ba]
|
||||
(update ba :locations (fn [ls]
|
||||
(map (fn [l] {:location l})
|
||||
(map (fn [l] {:location l
|
||||
:id (random-uuid)})
|
||||
ls))))
|
||||
bas))))))}))
|
||||
|
||||
@@ -375,188 +381,132 @@
|
||||
(when active?
|
||||
[:div.card-content
|
||||
[:label.label "General"]
|
||||
[horizontal-field
|
||||
nil
|
||||
[level/left-stack
|
||||
[:div.control
|
||||
[:p.help "Account Code"]
|
||||
(if new?
|
||||
[:div.field.has-addons.is-extended
|
||||
[:div.field.has-addons
|
||||
[:p.control [:a.button.is-static (:code new-client) "-" ]]
|
||||
[:p.control
|
||||
[form-builder/raw-field
|
||||
[:input.input {:type "code"
|
||||
:field [:code]
|
||||
:spec ::entity/code}]]]]
|
||||
[form-builder/raw-field-v2 {:field :code}
|
||||
[:input.input {:type "text"}]]]]
|
||||
[:div.field [:p.control code]])]
|
||||
|
||||
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :name}
|
||||
"Nickname"
|
||||
[:input.input {:placeholder "BOA Checking #1"
|
||||
:type "text"
|
||||
:field [:name]}]]
|
||||
[horizontal-field
|
||||
nil
|
||||
[form-builder/field
|
||||
"Numeric Code"
|
||||
[:input.input {:placeholder "20101"
|
||||
:type "text"
|
||||
:field [:numeric-code]}]]]
|
||||
[form-builder/field
|
||||
:type "text"}]]
|
||||
[form-builder/field-v2 {:field :numeric-code}
|
||||
"Numeric Code"
|
||||
[com/number-input {:placeholder "20101"
|
||||
:style {:width "8em"}}]]
|
||||
[form-builder/field-v2 {:field :start-date}
|
||||
"Start date"
|
||||
[date-picker {:class-name "input"
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:placeholder "mm/dd/yyyy"
|
||||
:next-month-button-label ""
|
||||
:next-month-label ""
|
||||
:type "date"
|
||||
:field [:start-date]}]]]
|
||||
[date-picker {:output :cljs-date}]]]
|
||||
|
||||
(when (#{:check ":check"} type )
|
||||
[:div
|
||||
|
||||
[:label.label "Bank"]
|
||||
[horizontal-field
|
||||
nil
|
||||
[form-builder/field
|
||||
[level/left-stack
|
||||
[form-builder/field-v2 {:field :bank-name}
|
||||
"Bank Name"
|
||||
[:input.input {:placeholder "Bank of America"
|
||||
:type "text"
|
||||
:field [:bank-name]}]]
|
||||
[form-builder/field
|
||||
:type "text"}]]
|
||||
[form-builder/field-v2 {:field [:routing]}
|
||||
"Routing #"
|
||||
[:input.input {:placeholder "104819123"
|
||||
:style {:width "9em"}
|
||||
:type "text"
|
||||
:field [:routing]}]]
|
||||
[form-builder/field
|
||||
:type "text"}]]
|
||||
[form-builder/field-v2 {:field :bank-code}
|
||||
"Bank code"
|
||||
[:input.input {:placeholder "12/10123"
|
||||
:type "text"
|
||||
:field [:bank-code]}]]]
|
||||
:type "text"}]]]
|
||||
|
||||
[horizontal-field
|
||||
nil
|
||||
[form-builder/field
|
||||
[level/left-stack
|
||||
[form-builder/field-v2 {:field :number}
|
||||
"Account #"
|
||||
[:input.input {:placeholder "123456789"
|
||||
:type "text"
|
||||
:style {:width "20em"}
|
||||
:field [:number]}]]
|
||||
:style {:width "20em"}}]]
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :check-number}
|
||||
"Check Number"
|
||||
[:input.input {:placeholder "10000"
|
||||
:style {:width "6em"}
|
||||
:type "text"
|
||||
:field [:check-number]}]]]
|
||||
[form-builder/field
|
||||
"Yodlee Account"
|
||||
[:input.input {:placeholder "Yodlee Account #"
|
||||
:type "text"
|
||||
:field [:yodlee-account-id]}]]
|
||||
[form-builder/field
|
||||
[com/number-input {:style {:width "8em"}
|
||||
:placeholder "10000"}]]]
|
||||
|
||||
[form-builder/field-v2 {:field :yodlee-account}
|
||||
"Yodlee Account (new)"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)])
|
||||
:entity->text (fn [m] (str (:name m) " - " (:number m)))
|
||||
:type "typeahead-v3"
|
||||
:field [:yodlee-account]}]]
|
||||
[:div.field
|
||||
[:label.checkbox
|
||||
[form-builder/raw-field
|
||||
[:input {:type "checkbox"
|
||||
:field [:use-date-instead-of-post-date]}]]
|
||||
" (Yodlee only) Use 'date' instead of 'postDate'"]]
|
||||
:entity->text (fn [m] (str (:name m) " - " (:number m)))}]]
|
||||
|
||||
[form-builder/field
|
||||
|
||||
[form-builder/raw-field-v2 {:field :use-date-instead-of-post-date}
|
||||
[com/checkbox {:label " (Yodlee only) Use 'date' instead of 'postDate'"}]]
|
||||
|
||||
[form-builder/field-v2 {:field :intuit-bank-account}
|
||||
"Intuit Bank Account"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
|
||||
:entity->text (fn [m] (str (:name m)))
|
||||
:type "typeahead-v3"
|
||||
:field [:intuit-bank-account]}]]
|
||||
[form-builder/field
|
||||
:entity->text (fn [m] (str (:name m)))}]]
|
||||
[form-builder/field-v2 {:field :plaid-accounti}
|
||||
"Plaid Account"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)])
|
||||
:entity->text (fn [m] (str (:name m)))
|
||||
:type "typeahead-v3"
|
||||
:field [:plaid-account]}]]])
|
||||
:entity->text (fn [m] (str (:name m)))}]]])
|
||||
|
||||
(when (#{:credit ":credit"} type )
|
||||
[:div
|
||||
|
||||
|
||||
[:label.label "Account"]
|
||||
[horizontal-field
|
||||
nil
|
||||
[form-builder/field
|
||||
"Bank Name"
|
||||
[:input.input {:placeholder "Bank of America"
|
||||
:type "text"
|
||||
:field [:bank-name]}]]]
|
||||
|
||||
[form-builder/field-v2 {:field :bank-name}
|
||||
"Bank Name"
|
||||
[:input.input {:placeholder "Bank of America"
|
||||
:type "text"}]]
|
||||
|
||||
[horizontal-field
|
||||
nil
|
||||
[form-builder/field
|
||||
"Account #"
|
||||
[:input.input {:placeholder "123456789"
|
||||
:type "text"
|
||||
:field [:number]}]]]
|
||||
|
||||
[form-builder/field
|
||||
"Yodlee Account"
|
||||
[:input.input {:placeholder "Yodlee Account #"
|
||||
[form-builder/field-v2 {:field :number}
|
||||
"Account #"
|
||||
[:input.input {:placeholder "123456789"
|
||||
:type "text"
|
||||
:field [:yodlee-account-id]}]]
|
||||
[form-builder/field
|
||||
:style {:width "20em"}}]]
|
||||
|
||||
[form-builder/field-v2 {:field :yodlee-account}
|
||||
"Yodlee Account (new)"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)])
|
||||
:entity->text (fn [m] (str (:name m) " - " (:number m)))
|
||||
:type "typeahead-v3"
|
||||
:field [:yodlee-account]}]]
|
||||
[:div.field
|
||||
[:label.checkbox
|
||||
[form-builder/raw-field
|
||||
[:input {:type "checkbox"
|
||||
:field [:use-date-instead-of-post-date]}]]
|
||||
" (Yodlee only) Use 'date' instead of 'postDate'"]]
|
||||
[form-builder/field
|
||||
:entity->text (fn [m] (str (:name m) " - " (:number m)))}]]
|
||||
|
||||
[form-builder/raw-field-v2 {:field :use-date-instead-of-post-date}
|
||||
[com/checkbox {:label "(Yodlee only) Use 'date' instead of 'postDate'"}]
|
||||
[:input {:type "checkbox"
|
||||
:field [:use-date-instead-of-post-date]}]]
|
||||
|
||||
[form-builder/field-v2 {:field :intuit-bank-account}
|
||||
"Intuit Bank Account"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
|
||||
:entity->text (fn [m] (str (:name m)))
|
||||
:type "typeahead-v3"
|
||||
:field [:intuit-bank-account]}]]
|
||||
[form-builder/field
|
||||
:entity->text (fn [m] (str (:name m)))}]]
|
||||
|
||||
[form-builder/field-v2 {:field :plaid-account}
|
||||
"Plaid Account"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)])
|
||||
:entity->text (fn [m] (str (:name m)))
|
||||
:type "typeahead-v3"
|
||||
:field [:plaid-account]}]]])
|
||||
:entity->text (fn [m] (str (:name m)))}]]])
|
||||
[:div.field
|
||||
[:label.label "Locations"]
|
||||
[:div.control
|
||||
[:p.help "If this account is location-specific, add the valid locations"]
|
||||
[form-builder/raw-field
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:locations]
|
||||
:template [[:select.select {:type "select"
|
||||
:style {:width "7em"}
|
||||
:allow-nil? true
|
||||
:field [:location]
|
||||
:spec (set (map :location (get-in new-client [:locations])))}
|
||||
[:<>
|
||||
[:option ""]
|
||||
[:<> (map (fn [l] ^{:key (:location l)}
|
||||
[:option {:value (:location l)} (:location l)])
|
||||
(get-in new-client [:locations]))]]]]}]]]]
|
||||
[:div.field
|
||||
[:label.checkbox
|
||||
[form-builder/raw-field
|
||||
[:input {:type "checkbox"
|
||||
:field [:include-in-reports]}]]
|
||||
" Include in reports"]]])
|
||||
[form-builder/raw-field-v2 {:field :locations}
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :location}
|
||||
[com/select-field {:options (map (fn [l]
|
||||
[(:location l) (:location l)])
|
||||
(get-in new-client [:locations]))
|
||||
:allow-nil? true
|
||||
:style {:width "7em"}
|
||||
}]]]
|
||||
:schema [:sequential location-schema]
|
||||
:key-fn :id}]]]]
|
||||
|
||||
[form-builder/raw-field-v2 {:field :include-in-reports}
|
||||
[com/checkbox {:label "Include in reports"}]
|
||||
]
|
||||
])
|
||||
|
||||
(when active?
|
||||
[:footer.card-footer
|
||||
@@ -568,86 +518,87 @@
|
||||
(defn general-section []
|
||||
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/section {:title "General"}
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :name}
|
||||
"Name"
|
||||
[:input.input {:type "text"
|
||||
:style {:width "20em"}
|
||||
:field [:name]
|
||||
:spec ::entity/name}]]
|
||||
[form-builder/field
|
||||
:style {:width "20em"}}]]
|
||||
[form-builder/field-v2 {:field :code}
|
||||
"Client code"
|
||||
[:input.input {:type "code"
|
||||
:style {:width "5em"}
|
||||
:field :code
|
||||
:disabled (boolean (:id new-client))
|
||||
:spec ::entity/code}]]
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :locations}
|
||||
"Locations"
|
||||
[multi-field {:type "multi-field"
|
||||
:field :locations
|
||||
:allow-change? false
|
||||
:template [[:input.input {:field [:location]
|
||||
:max-length 2
|
||||
:style { :width "4em"}}]]}]]
|
||||
[com/multi-field-v2 {:allow-change? false
|
||||
:template [[form-builder/raw-field-v2 {:field :location}
|
||||
[:input.input {:max-length 2
|
||||
:style { :width "4em"}}]]]
|
||||
:disable-remove? true
|
||||
:key-fn :id
|
||||
:schema [:sequential location-schema]
|
||||
:next-key (random-uuid)}]]
|
||||
|
||||
[:div.field
|
||||
[:label.label "Signature"]
|
||||
[form-builder/vertical-control
|
||||
"Signature"
|
||||
[signature {:signature-file (:signature-file new-client)
|
||||
:signature-data (:signature-data new-client)
|
||||
:on-change (fn [uri]
|
||||
(re-frame/dispatch [::forms/change ::form [:signature-data] uri]))}]]
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :locked-until}
|
||||
"Locked Until"
|
||||
[date-picker-friendly {:type "date"
|
||||
:field [:locked-until]
|
||||
:style {:width "15em"}}]]]))
|
||||
[date-picker {:output :cljs-date
|
||||
:style {:width "15em"}}]]]))
|
||||
|
||||
(defn contacts-section []
|
||||
[form-builder/section {:title "Contacts"}
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :emails}
|
||||
"Emails (address/description)"
|
||||
[multi-field {:type "multi-field"
|
||||
:field :emails
|
||||
:template [[:input.input {:type "email"
|
||||
:field [:email]
|
||||
:placeholder "tom@myspace.com"
|
||||
:spec ::entity/email}]
|
||||
[:input.input {:type "text"
|
||||
:placeholder "Manager"
|
||||
:field [:description]}]]}]]
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :email}
|
||||
[:input.input {:type "email"
|
||||
:placeholder "tom@myspace.com"}]]
|
||||
[form-builder/raw-field-v2 {:field :description}
|
||||
[:input.input {:type "text"
|
||||
:placeholder "Manager"}]]]
|
||||
:key-fn :id
|
||||
:schema [:sequential email-schema]
|
||||
:next-key (random-uuid)}]]
|
||||
|
||||
[form-builder/with-scope {:scope [:address ]}
|
||||
[:div.field
|
||||
[:label.label "Address"]
|
||||
[:div.control
|
||||
[:div {:style {:width "30em"}}
|
||||
[address2-field]]]]]])
|
||||
[form-builder/vertical-control
|
||||
"Address"
|
||||
[:div {:style {:width "30em"}}
|
||||
[form-builder/raw-field-v2 {:field :address}
|
||||
[address2-field]]]]])
|
||||
|
||||
;; TODO Name matches, locations, bank account locations are all "single field multis", and require weird mounting and
|
||||
;; unmounting. A new field could sort that out easily
|
||||
(defn matching-section []
|
||||
[form-builder/section {:title "Matching"}
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :matches}
|
||||
"Name matches"
|
||||
[multi-field {:type "multi-field"
|
||||
:field :matches
|
||||
:template [[:input.input {:field [:match]
|
||||
:placeholder "Harry's burger joint"
|
||||
:style { :width "15em"}}]]}]]
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:match]}
|
||||
[:input.input {:placeholder "Harry's burger joint"
|
||||
:style { :width "15em"}}]]]
|
||||
:key-fn :id
|
||||
:next-key (random-uuid)
|
||||
:schema [:sequential name-match-schema]}]]
|
||||
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :location-matches}
|
||||
"Location Matches"
|
||||
[multi-field {:type "multi-field"
|
||||
:field :location-matches
|
||||
:template [[:input.input {:field [:match]
|
||||
:placeholder "Downtown"
|
||||
:style { :width "15em"}}]
|
||||
[:input.input {:field [:location]
|
||||
:placeholder "DT"
|
||||
:max-length 2
|
||||
:style { :width "4em"}}]]}]]])
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :match}
|
||||
[:input.input {:placeholder "Downtown"
|
||||
:style { :width "15em"}}]]
|
||||
[form-builder/raw-field-v2 {:field :location}
|
||||
[:input.input {:placeholder "DT"
|
||||
:max-length 2
|
||||
:style { :width "4em"}}]]]
|
||||
:schema [:sequential location-match-schema]
|
||||
:next-key (random-uuid)
|
||||
:key-fn :id}]]])
|
||||
|
||||
(defn bank-accounts-section []
|
||||
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
|
||||
@@ -674,99 +625,82 @@
|
||||
[form-builder/section {:title "Cash Flow"}
|
||||
[:label.label (str "Week A (" next-week-a ")")]
|
||||
[left-stack
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field :week-a-credits}
|
||||
"Regular Credits"
|
||||
[:input.input {:type "number"
|
||||
:style {:width "10em"}
|
||||
:placeholder "500.00"
|
||||
:field [:week-a-credits]
|
||||
:step "0.01"}]]
|
||||
[form-builder/field
|
||||
[com/money-input]]
|
||||
[form-builder/field-v2 {:field :week-a-debits}
|
||||
"Regular Debits"
|
||||
[:input.input {:type "number"
|
||||
:style {:width "10em"}
|
||||
:placeholder "150.00"
|
||||
:field [:week-a-debits]
|
||||
:step "0.01"}]]]
|
||||
[com/money-input]]]
|
||||
[:label.label (str "Week B (" next-week-b ")")]
|
||||
[left-stack
|
||||
[form-builder/field "Regular Credits"
|
||||
[:input.input {:type "number"
|
||||
:style {:width "10em"}
|
||||
:placeholder "1000.00"
|
||||
:field [:week-b-credits]
|
||||
:step "0.01"}]]
|
||||
[form-builder/field "Regular Debits"
|
||||
[:input.input {:type "number"
|
||||
:style {:width "10em"}
|
||||
:placeholder "250.00"
|
||||
:field [:week-b-debits]
|
||||
:step "0.01"}]]]
|
||||
[:div.field
|
||||
[:label.label "Forecasted transactions"]
|
||||
[form-builder/field-v2 {:field :week-b-credits}
|
||||
"Regular Credits"
|
||||
[com/money-input]]
|
||||
[form-builder/field-v2 {:field :week-b-debits}
|
||||
"Regular Debits"
|
||||
[com/money-input]]]
|
||||
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[multi-field {:type "multi-field"
|
||||
:field :forecasted-transactions
|
||||
:template [[:input.input {:type "text"
|
||||
:placeholder "Identifier"
|
||||
:style {:width "10em"}
|
||||
:field [ :identifier]}]
|
||||
[:input.input {:type "number"
|
||||
:style {:width "8em"}
|
||||
:placeholder "DOM"
|
||||
:step "1"
|
||||
:field [:day-of-month]}]
|
||||
[:input.input {:type "number"
|
||||
:placeholder "250.00"
|
||||
:class "has-text-right"
|
||||
:style {:width "7em"}
|
||||
:field [:amount]
|
||||
:step "0.01"}]]}]]]]]))
|
||||
[form-builder/field-v2 {:field :forecasted-transactions}
|
||||
"Forecasted transactions"
|
||||
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :identifier}
|
||||
[:input.input {:type "text"
|
||||
:placeholder "Identifier"
|
||||
:style {:width "10em"}}]]
|
||||
[form-builder/raw-field-v2 {:field :day-of-month}
|
||||
[com/number-input {:placeholder "DOM"}]]
|
||||
[form-builder/raw-field-v2 {:field :amount
|
||||
:placeholder "AMT"}
|
||||
[com/money-input]]]
|
||||
:key-fn :id}]]]))
|
||||
|
||||
(defn square-section []
|
||||
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/section {:title "Square Integration"}
|
||||
[form-builder/field "Square Authentication Token"
|
||||
[form-builder/field-v2 {:field :square-auth-token}
|
||||
"Square Authentication Token"
|
||||
[:input.input {:type "text"
|
||||
:style {:width "40em"}
|
||||
:field [:square-auth-token]}]]
|
||||
[form-builder/field
|
||||
:style {:width "40em"}}]]
|
||||
[form-builder/field-v2 {:field :selected-square-locations}
|
||||
"Square Locations"
|
||||
[multi-field {:type "multi-field"
|
||||
:field :selected-square-locations
|
||||
:template [[typeahead-v3 {:entities (:square-locations new-client)
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:style {:width "15em"}
|
||||
:field [:square-location]}]
|
||||
[:input.input {:type "text"
|
||||
:style {:width "4em"}
|
||||
:field [:client-location]
|
||||
:step "0.01"}]]
|
||||
:disable-remove? true}]]]))
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :square-location}
|
||||
[typeahead-v3 {:entities (:square-locations new-client)
|
||||
:entity->text :name
|
||||
:style {:width "15em"}}]]
|
||||
[form-builder/raw-field-v2 {:field :client-location}
|
||||
[com/select-field {:options (map (fn [l]
|
||||
[(:location l) (:location l)])
|
||||
(get-in new-client [:locations]))
|
||||
:allow-nil? true
|
||||
:style {:width "7em"}
|
||||
}]]]
|
||||
:disable-remove? true
|
||||
:key-fn :id
|
||||
:schema [:sequential square-location-schema]}]]]))
|
||||
|
||||
(defn ezcater-section []
|
||||
[form-builder/section {:title "EZCater integration"}
|
||||
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/section {:title "EZCater integration"}
|
||||
|
||||
[form-builder/field-v2 {:field :ezcater-locations}
|
||||
"EZCater Locations"
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :caterer}
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_ezcater_caterer
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:entity->text :name
|
||||
:style {:width "20em"}}]]
|
||||
[form-builder/raw-field-v2 {:field [:location]}
|
||||
[com/select-field {:options (map (fn [l]
|
||||
[(:location l) (:location l)])
|
||||
(get-in new-client [:locations]))
|
||||
:allow-nil? true
|
||||
:style {:width "7em"}}]]]
|
||||
:key-fn :id
|
||||
:schema [:sequential ezcater-schema]
|
||||
:disable-remove? true}]]]))
|
||||
|
||||
[form-builder/field
|
||||
"EZCater Locations"
|
||||
[multi-field {:type "multi-field"
|
||||
:field :ezcater-locations
|
||||
:template [[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_ezcater_caterer
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:field [:caterer]
|
||||
:style {:width "20em"}}]
|
||||
[:input.input {:type "text"
|
||||
:style {:width "4em"}
|
||||
:field [:location]
|
||||
:step "0.01"}]]
|
||||
:disable-remove? true}]]])
|
||||
|
||||
(defn form-content []
|
||||
(let [_ @(re-frame/subscribe [::client])
|
||||
@@ -774,10 +708,10 @@
|
||||
|
||||
^{:key (or (:id new-client)
|
||||
"new")}
|
||||
[form-builder/builder {:can-submit [::can-submit]
|
||||
:submit-event [::save-new-client ]
|
||||
[form-builder/builder {:submit-event [::save-new-client ]
|
||||
:id ::form
|
||||
:fullwidth? false}
|
||||
:fullwidth? false
|
||||
:schema client-schema}
|
||||
|
||||
[general-section]
|
||||
[contacts-section]
|
||||
|
||||
@@ -1,168 +1,114 @@
|
||||
(ns auto-ap.views.pages.admin.excel-import
|
||||
(:require [auto-ap.events :as all-events]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.utils :refer [bind-field dispatch-event]]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::excel-import
|
||||
(fn [db]
|
||||
(::excel-import db)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::expense-accounts
|
||||
(fn [db]
|
||||
(::expense-accounts db)))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::change
|
||||
(fn [db [_ field v]]
|
||||
(assoc-in db (into [::excel-import] field) v)))
|
||||
|
||||
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.utils :refer [with-user]]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save
|
||||
[(forms/in-form ::excel-import)]
|
||||
(fn [{{excel-import-data :data :as excel-import-form} :db}]
|
||||
(let [user @(re-frame/subscribe [::subs/token])]
|
||||
{:db (-> excel-import-form
|
||||
(assoc :status :loading)
|
||||
(assoc :error nil))
|
||||
:http {:token user
|
||||
:method :post
|
||||
:body (pr-str excel-import-data)
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/invoices/upload-integreat")
|
||||
:on-success [::save-complete]
|
||||
:on-error [::forms/save-error ::excel-import]}})))
|
||||
[ with-user (forms/in-form ::form)]
|
||||
(fn [{:keys [db user]}]
|
||||
{:http {:token user
|
||||
:method :post
|
||||
:body (pr-str {:excel-rows (:excel-rows (:data db))})
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/invoices/upload-integreat")
|
||||
:owns-state {:single ::form}
|
||||
:on-success [::save-complete]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save-complete
|
||||
(fn [{:keys [db]} [_ rows]]
|
||||
{:db
|
||||
(-> db
|
||||
(forms/save-succeeded ::excel-import)
|
||||
(assoc-in [::excel-import :rows] rows))}))
|
||||
(cond->
|
||||
{:db (assoc db ::result rows)}
|
||||
(seq (:vendors-not-found rows)) (assoc :dispatch [::forms/start-form ::create-vendors ]))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::result
|
||||
(fn [db]
|
||||
(::result db)))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save-error
|
||||
(fn [{:keys [db]}]
|
||||
(println "ERROR")
|
||||
{:dispatch [::change [:error] true]
|
||||
:db (-> db
|
||||
(assoc-in [::excel-import :rows] nil)
|
||||
(assoc-in [::excel-import :saving?] false))}))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::toggle-vendor
|
||||
(fn [db [_ data]]
|
||||
(update-in db [::excel-import :create-vendors] (fn [x]
|
||||
(let [x (or x #{})]
|
||||
(if (x data)
|
||||
(disj x data)
|
||||
(conj x data)))))))
|
||||
(re-frame/reg-event-fx
|
||||
::create-vendors
|
||||
(fn [{:keys [db]}]
|
||||
(let [excel-import (::excel-import db)]
|
||||
(println (::expense-accounts db))
|
||||
{:graphql {:token (:user db)
|
||||
:query-obj {:venia/operation {:operation/type :mutation
|
||||
:operation/name "UpsertVendor"}
|
||||
|
||||
:venia/queries (map (fn [v ]
|
||||
{:query/data [:upsert-vendor
|
||||
{:vendor {:name v :default-account-id (-> db ::expense-accounts (get v) :default-account-id :id)}}
|
||||
[:id :name]]})
|
||||
|
||||
(get-in db [::excel-import :create-vendors]))}
|
||||
:on-success [::create-vendor-complete]
|
||||
:on-error [::create-vendor-error]}
|
||||
:db (-> db
|
||||
(assoc-in [::excel-import :saving-vendors?] true))})))
|
||||
[with-user (forms/in-form ::create-vendors)]
|
||||
(fn [{:keys [user db]}]
|
||||
{:graphql {:token user
|
||||
:owns-state {:single ::create-vendors}
|
||||
:query-obj {:venia/operation {:operation/type :mutation
|
||||
:operation/name "UpsertVendor"}
|
||||
|
||||
:venia/queries
|
||||
(for [[vendor-name {:keys [default-account]}] (:data db)]
|
||||
{:query/data [:upsert-vendor
|
||||
{:vendor {:name vendor-name :default-account-id (:id default-account)}}
|
||||
[:id :name]]})}
|
||||
:on-success [::create-vendor-complete]}}))
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::create-vendor-complete
|
||||
(fn [db [_ data]]
|
||||
(-> db
|
||||
(update-in [::excel-import :rows :vendors-not-found]
|
||||
(fn [v]
|
||||
(reduce disj v (get-in db [::excel-import :create-vendors]))))
|
||||
(update-in [::excel-import] dissoc :create-vendors))))
|
||||
(fn [db _]
|
||||
(dissoc db ::result )))
|
||||
|
||||
(def missing-vendor-schema
|
||||
(m/schema [:map-of :string
|
||||
[:map
|
||||
[:default-account schema/reference]]]))
|
||||
|
||||
(defn create-missing-vendors [{:keys [vendors]}]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::create-vendors])
|
||||
vendors-to-create (filter (fn [v] (:checked v))
|
||||
(vals data))]
|
||||
[form-builder/builder {:id ::create-vendors
|
||||
:submit-event [::create-vendors]
|
||||
:schema missing-vendor-schema}
|
||||
[:article.message.is-warning.is-paddingless
|
||||
[:div.message-header
|
||||
"Some vendors could not be found"]
|
||||
|
||||
[:div.message-body
|
||||
[:h2 "Check the vendors you want to create"]
|
||||
(for [v vendors]
|
||||
^{:key v}
|
||||
[:div.field.is-grouped
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field [v :checked]}
|
||||
[com/checkbox {:label v}]]]
|
||||
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field [v :default-account]}
|
||||
[com/search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_account
|
||||
{:query i}
|
||||
[:name :id :location]])}]]]])
|
||||
[form-builder/error-notification]
|
||||
[form-builder/submit-button {:disabled (when-not (seq vendors-to-create) "disabled")}
|
||||
(str "Create " (count vendors-to-create) " vendors")]
|
||||
[:div.is-clearfix]]]]))
|
||||
|
||||
(defn admin-excel-import-content []
|
||||
[:div
|
||||
(let [{{:keys [vendors-not-found already-imported imported]} :rows
|
||||
:keys [create-vendors]
|
||||
:or {create-vendors #{}}
|
||||
:as excel-import-data} @(re-frame/subscribe [::excel-import])
|
||||
data @(re-frame/subscribe [::expense-accounts])
|
||||
form @(re-frame/subscribe [::forms/form ::excel-import])
|
||||
|
||||
|
||||
chooseable-expense-accounts @(re-frame/subscribe [::subs/all-accounts])
|
||||
change-event [::all-events/change-form [::expense-accounts]]]
|
||||
(let [{:keys [vendors-not-found errors already-imported imported]} @(re-frame/subscribe [::result])]
|
||||
[:div
|
||||
[:h1.title "Import Invoices from Integreat Excel"]
|
||||
(when (seq vendors-not-found)
|
||||
[:article.message.is-warning.is-paddingless
|
||||
[:div.message-header
|
||||
"Some vendors could not be found"]
|
||||
|
||||
[:div.message-body
|
||||
[:h2 "Check the vendors you want to create"]
|
||||
[:div.columns
|
||||
(for [[i vendor-group] (map vector (range) (partition-all (max 1 (/ (count vendors-not-found) 3)) vendors-not-found))]
|
||||
^{:key i}
|
||||
[:div.column
|
||||
(for [v vendor-group]
|
||||
^{:key v} [:div.field.is-grouped
|
||||
[:p.control
|
||||
[:label.checkbox
|
||||
[:input {:value v
|
||||
:checked (if (create-vendors v)
|
||||
"checked"
|
||||
"")
|
||||
|
||||
:type "checkbox"
|
||||
:on-change (fn []
|
||||
(re-frame/dispatch [::toggle-vendor v]))}]
|
||||
(str " " v)]]
|
||||
|
||||
[:p.control
|
||||
[bind-field
|
||||
[typeahead-v3 {:entities chooseable-expense-accounts
|
||||
:entity->text (fn [x ] (str (:numeric-code x) " - " (:name x)))
|
||||
:type "typeahead-v3"
|
||||
:field [v :default-account-id]
|
||||
:event change-event
|
||||
:subscription data}]]]])])]
|
||||
[:div
|
||||
[:button.button.is-pulled-right
|
||||
{:on-click (dispatch-event [::create-vendors])
|
||||
:disabled (when-not (seq create-vendors)
|
||||
"disabled")
|
||||
}
|
||||
(str "Create " (count create-vendors) " vendors")]]
|
||||
[:div.is-clearfix]]])
|
||||
[bind-field
|
||||
[:textarea.textarea {:rows "20"
|
||||
:field :excel-rows
|
||||
:type "text"
|
||||
:event [::forms/change ::excel-import]
|
||||
:subscription (:data form)}]]
|
||||
|
||||
[:button.button.is-large.is-pulled-right.is-primary {:on-click (dispatch-event [::save])
|
||||
:class (str @(re-frame/subscribe [::forms/loading-class ::excel-import])
|
||||
(when (:error form) " animated shake"))
|
||||
:disabled (when (= :saving (:status form)) "disabled")} "Import"]
|
||||
|
||||
[create-missing-vendors {:vendors vendors-not-found}]
|
||||
)
|
||||
[form-builder/builder {:id ::form
|
||||
:submit-event [::save]}
|
||||
[form-builder/raw-field-v2 {:field :excel-rows}
|
||||
[:textarea.textarea {:rows "20"
|
||||
:type "text"}]]
|
||||
[form-builder/error-notification]
|
||||
[form-builder/submit-button "Import"]]
|
||||
[:div.is-clearfix]
|
||||
[:div.is-clearfix
|
||||
[:p
|
||||
@@ -171,29 +117,49 @@
|
||||
[:p
|
||||
(when already-imported
|
||||
(str already-imported " rows already imported."))]]
|
||||
(when-let [errors (:errors (:rows excel-import-data))]
|
||||
(when errors
|
||||
[:div
|
||||
[:h3 (str "Import errors (" (min 100 (count errors)) " / " (count errors) " )")]
|
||||
[:table.table.is-fullwidth
|
||||
[:thead
|
||||
[:th "Date"]
|
||||
[:th "Invoice #"]
|
||||
[:th "Client"]
|
||||
[:th "Vendor"]
|
||||
[:th "Amount"]
|
||||
[:th "Errors"]]
|
||||
[:td "Date"]
|
||||
[:td "Invoice #"]
|
||||
[:td "Client"]
|
||||
[:td "Vendor"]
|
||||
[:td "Amount"]
|
||||
[:td "Errors"]]
|
||||
|
||||
(for [{:keys [raw-date invoice-number client vendor-name amount] row-errors :errors} (take 100 errors)]
|
||||
^{:key (str raw-date invoice-number client vendor-name amount)}
|
||||
[:tr
|
||||
[:td raw-date]
|
||||
[:td invoice-number]
|
||||
[:td client]
|
||||
[:td vendor-name]
|
||||
[:td amount]
|
||||
[:td (map (fn [{:keys [info]}] ^{:key info} [:p info]) row-errors)]])]])])])
|
||||
[:tbody
|
||||
(for [{:keys [raw-date invoice-number client vendor-name amount] row-errors :errors} (take 100 errors)]
|
||||
^{:key (str raw-date invoice-number client vendor-name amount)}
|
||||
[:tr
|
||||
[:td raw-date]
|
||||
[:td invoice-number]
|
||||
[:td client]
|
||||
[:td vendor-name]
|
||||
[:td amount]
|
||||
[:td (map (fn [{:keys [info]}] ^{:key info} [:p info]) row-errors)]])]]])])])
|
||||
|
||||
|
||||
(defn admin-excel-import-page []
|
||||
(defn admin-excel-import-page-internal []
|
||||
[side-bar-layout {:side-bar [admin-side-bar {}]
|
||||
:main [admin-excel-import-content]}])
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn [_ _]
|
||||
{:dispatch [::forms/start-form ::form]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::unmounted
|
||||
(fn [{:keys [db]} _]
|
||||
{:dispatch-n [[::forms/form-closing ::form]
|
||||
[::forms/form-closing ::create-vendors]]
|
||||
:db (dissoc db ::results)}))
|
||||
|
||||
(defn admin-excel-import-page []
|
||||
(r/create-class
|
||||
{:display-name "excel-import-page"
|
||||
:component-will-unmount #(re-frame/dispatch-sync [::unmounted])
|
||||
:component-did-mount #(re-frame/dispatch [::mounted])
|
||||
:reagent-render admin-excel-import-page-internal}))
|
||||
|
||||
@@ -79,6 +79,7 @@
|
||||
(defn table [{:keys [status data-page]}]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page])
|
||||
params @(re-frame/subscribe [::params])
|
||||
is-admin? @(re-frame/subscribe [::subs/is-admin?])
|
||||
statuses @(re-frame/subscribe [::status/multi ::refresh])]
|
||||
[grid/grid {:data-page data-page
|
||||
:column-count 5}
|
||||
@@ -105,5 +106,6 @@
|
||||
[:li (:name a) [:div.tag (->$ (:balance a))]])]]
|
||||
[grid/cell {}
|
||||
[:div.buttons
|
||||
[buttons/fa-icon {:event [::delete-requested (:id c)]
|
||||
:icon "fa-times"}]]]])]]]))
|
||||
(when is-admin?
|
||||
[buttons/fa-icon {:event [::delete-requested (:id c)]
|
||||
:icon "fa-times"}])]]])]]]))
|
||||
|
||||
@@ -1,193 +0,0 @@
|
||||
(ns auto-ap.views.pages.admin.reminders
|
||||
(:require-macros [cljs.core.async.macros :refer [go]])
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[auto-ap.views.components.paginator :refer [paginator]]
|
||||
[auto-ap.views.components.sorter :refer [sorted-column]]
|
||||
[auto-ap.entities.vendors :as entity]
|
||||
[reagent.core :as reagent]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.utils :refer [login-url dispatch-value-change dispatch-event date-time->str date->str horizontal-field bind-field]]
|
||||
[cljs.reader :as edn]
|
||||
|
||||
[auto-ap.routes :as routes]
|
||||
[bidi.bidi :as bidi]))
|
||||
(re-frame/reg-sub
|
||||
::editing-reminder
|
||||
(fn [db]
|
||||
(::editing-reminder db)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::reminder-page
|
||||
(fn [db]
|
||||
(::reminder-page db)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::reminder-params
|
||||
(fn [db]
|
||||
(::reminder-params db)))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::edit
|
||||
(fn [db [_ which]]
|
||||
(assoc db ::editing-reminder which)))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::change
|
||||
(fn [db [_ field v]]
|
||||
(assoc-in db (into [::editing-reminder] field) v)))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn [{:keys [db]} _]
|
||||
{:db (assoc db ::reminder-params {:start 0})
|
||||
:dispatch [::invalidated]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::params-changed
|
||||
(fn [{:keys [db]} [_ params]]
|
||||
{:db (update db ::reminder-params merge params)
|
||||
:dispatch [::invalidated]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save
|
||||
(fn [{:keys [db]}]
|
||||
(let [edited-reminder (::editing-reminder db)]
|
||||
(println edited-reminder)
|
||||
{:http {:token (:user db)
|
||||
:method :put
|
||||
:body (pr-str (dissoc edited-reminder :sent :scheduled))
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/reminders/" (:id edited-reminder))
|
||||
:on-success [::save-complete]
|
||||
:on-error [::save-error]}})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save-complete
|
||||
(fn [{:keys [db]}]
|
||||
{:dispatch [::edit nil]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save-error
|
||||
(fn [{:keys [db]}]
|
||||
{:dispatch [::change [:error] true]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::invalidated
|
||||
(fn [{:keys [db]}]
|
||||
{:graphql {:token (:user db)
|
||||
:query-obj {:venia/queries [[:reminder_page
|
||||
(::reminder-params db)
|
||||
|
||||
[[:reminders [:id :email :sent :scheduled :subject :body [:vendor [:name :id]] ]]
|
||||
:total
|
||||
:start
|
||||
:end]]]}
|
||||
|
||||
:on-success [::received]}}))
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::received
|
||||
(fn [db [_ reminders]]
|
||||
(assoc db ::reminder-page (first (:reminder-page reminders)))))
|
||||
|
||||
(defn edit-dialog []
|
||||
(let [editing-reminder @(re-frame/subscribe [::editing-reminder])]
|
||||
[:div.modal.is-active
|
||||
[:div.modal-background {:on-click (fn [] (re-frame/dispatch [::edit nil]))}]
|
||||
|
||||
[:div.modal-card
|
||||
[:header.modal-card-head
|
||||
[:p.modal-card-title
|
||||
(str "Reminder for " (:name (:vendor editing-reminder)))]
|
||||
(when (:error editing-reminder)
|
||||
[:span.icon.has-text-danger
|
||||
[:i.fa.fa-exclamation-triangle]])
|
||||
[:button.delete {:on-click (fn [] (re-frame/dispatch [::edit nil]))}]]
|
||||
[:section.modal-card-body
|
||||
[horizontal-field
|
||||
[:label.label "Email"]
|
||||
[:div.control
|
||||
[bind-field
|
||||
[:input.input {:type "text"
|
||||
:field :email
|
||||
:event ::change
|
||||
:subscription editing-reminder}]]]]
|
||||
[horizontal-field
|
||||
[:label.label "Subject"]
|
||||
[:div.control
|
||||
[bind-field
|
||||
[:input.input {:type "text"
|
||||
:field :subject
|
||||
:event ::change
|
||||
:subscription editing-reminder}]]]]
|
||||
|
||||
[horizontal-field
|
||||
[:label.label "Body"]
|
||||
[:div.control
|
||||
|
||||
[bind-field
|
||||
[:textarea.textarea.is-expanded {:type "text"
|
||||
:field :body
|
||||
:event ::change
|
||||
:subscription editing-reminder}]]]]
|
||||
|
||||
(when (:saving? editing-reminder) [:div.is-overlay {:style {"backgroundColor" "rgba(150,150,150, 0.5)"}}])]
|
||||
|
||||
[:footer.modal-card-foot
|
||||
[:button.button.is-primary {:on-click (fn [] (re-frame/dispatch [::save]))
|
||||
#_#_:disabled (when (not (s/valid? ::entity/vendor editing-reminder ))
|
||||
"disabled")}
|
||||
[:span "Save"]
|
||||
(when (:saving? editing-reminder)
|
||||
[:span.icon
|
||||
[:i.fa.fa-spin.fa-spinner]])]]]]))
|
||||
|
||||
(defn reminders-table []
|
||||
(let [{:keys [reminders start end total count]} @(re-frame/subscribe [::reminder-page])
|
||||
{:keys [sort-by asc]} @(re-frame/subscribe [::reminder-params])
|
||||
reminders (or reminders [])]
|
||||
[:div
|
||||
[:div.is-pulled-right
|
||||
[paginator {:start start
|
||||
:end end
|
||||
:total total
|
||||
:count count
|
||||
:on-change (fn [params]
|
||||
(re-frame/dispatch [::params-changed params]))}]]
|
||||
[:table {:class "table", :style {:width "100%"}}
|
||||
[:thead
|
||||
[:tr
|
||||
(for [[sort-key name] [["vendor" "Vendor"]
|
||||
["scheduled" "Scheduled Date"]
|
||||
["sent" "Status"]
|
||||
["email" "Email"]]]
|
||||
^{:key name}
|
||||
[sorted-column {:on-sort (fn [params] (re-frame/dispatch [::params-changed params]))
|
||||
:style {:width "20%" :cursor "pointer"}
|
||||
:sort-key sort-key
|
||||
:sort-by sort-by
|
||||
:asc asc}
|
||||
name])]]
|
||||
[:tbody (for [{:keys [id vendor scheduled sent email ] :as r} reminders]
|
||||
^{:key id}
|
||||
[:tr (when-not sent
|
||||
{:on-click (fn [] (re-frame/dispatch [::edit r])) :style {:cursor "pointer"}})
|
||||
[:td (:name vendor)]
|
||||
[:td (date->str scheduled)]
|
||||
[:td (when sent
|
||||
[:span [:span.icon [:i.fa.fa-check]] "Sent " (date-time->str sent)]) ]
|
||||
[:td email]])]]
|
||||
|
||||
(when @(re-frame/subscribe [::editing-reminder])
|
||||
[edit-dialog])]))
|
||||
|
||||
|
||||
(defn admin-reminders-page []
|
||||
[(with-meta
|
||||
(fn []
|
||||
[:div
|
||||
[:h1.title "Reminders"]
|
||||
[reminders-table]])
|
||||
{:component-did-mount (fn []
|
||||
(re-frame/dispatch [::mounted]))})])
|
||||
@@ -1,23 +1,22 @@
|
||||
(ns auto-ap.views.pages.admin.rules.form
|
||||
(:require
|
||||
[auto-ap.entities.transaction-rule :as entity]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.button-radio :refer [button-radio]]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.expense-accounts-field
|
||||
:as expense-accounts-field
|
||||
:refer [expense-accounts-field]]
|
||||
:refer [expense-accounts-field-v2]]
|
||||
[auto-ap.views.components.layouts :as layouts]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.views.pages.admin.rules.common :refer [default-read]]
|
||||
[auto-ap.views.pages.admin.rules.results-modal :as results-modal]
|
||||
[auto-ap.views.utils :refer [dispatch-event with-user]]
|
||||
[clojure.spec.alpha :as s]
|
||||
[auto-ap.views.utils :refer [coerce-float dispatch-event with-user]]
|
||||
[clojure.string :as str]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
@@ -29,7 +28,6 @@
|
||||
::default-note
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{{:keys [client description amount-lte amount-gte dom-lte dom-gte]} :data}]
|
||||
|
||||
(str/join " - " (filter (complement str/blank?)
|
||||
[(:code client)
|
||||
description
|
||||
@@ -47,12 +45,6 @@
|
||||
(when dom-lte
|
||||
(str "<" dom-lte))))]))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{:keys [data]} _]
|
||||
(s/valid? ::entity/transaction-rule data)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::query
|
||||
:<- [::forms/form ::form]
|
||||
@@ -89,10 +81,6 @@
|
||||
(assoc :bank-account-id (:id (:bank-account data))))}
|
||||
default-read]}]}))
|
||||
|
||||
(defn ungraphql-transaction-rule [x]
|
||||
(-> x
|
||||
(update :amount-lte #(some-> % js/parseFloat))
|
||||
(update :amount-gte #(some-> % js/parseFloat))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::test-query
|
||||
@@ -160,6 +148,9 @@
|
||||
:accounts
|
||||
:yodlee-merchant
|
||||
:transaction-approval-status])
|
||||
|
||||
(update :amount-lte coerce-float)
|
||||
(update :amount-gte coerce-float)
|
||||
(update :accounts (fn [xs]
|
||||
(mapv #(-> %
|
||||
(assoc :amount-percentage (* (:percentage %) 100.0)))
|
||||
@@ -185,18 +176,21 @@
|
||||
:on-success [::changed [:vendor-preferences]]}]})))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::changed
|
||||
(forms/change-handler ::form
|
||||
(fn [data field value]
|
||||
(cond (and (= [:vendor-preferences] field)
|
||||
value
|
||||
(expense-accounts-field/can-replace-with-default? (:accounts data)))
|
||||
[[:accounts] (expense-accounts-field/default-account (:accounts data)
|
||||
(:default-account value)
|
||||
(:total data)
|
||||
[])]
|
||||
:else
|
||||
[]))))
|
||||
::changed
|
||||
(forms/change-handler ::form
|
||||
(fn [data field value]
|
||||
(cond (and (= [:vendor-preferences] field)
|
||||
value
|
||||
(expense-accounts-field/can-replace-with-default? (:accounts data)))
|
||||
[[:accounts] (expense-accounts-field/default-account (:accounts data)
|
||||
(:default-account value)
|
||||
(:total data)
|
||||
[])]
|
||||
|
||||
(= [:client] field)
|
||||
[[:bank-account] nil]
|
||||
:else
|
||||
[]))))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::saving
|
||||
@@ -205,9 +199,9 @@
|
||||
{:graphql
|
||||
{:token user
|
||||
:query-obj query
|
||||
:owns-state {:single ::form}
|
||||
:on-success (fn [result]
|
||||
[::updated (:upsert-transaction-rule result)])
|
||||
:on-error [::forms/save-error ::form]}}))
|
||||
[::updated (:upsert-transaction-rule result)])}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::test-clicked
|
||||
@@ -223,9 +217,8 @@
|
||||
(re-frame/reg-event-fx
|
||||
::updated
|
||||
[(forms/triggers-stop ::form)]
|
||||
(fn [{:keys [db]} [_ {:keys [rule-saved]} result]]
|
||||
{:db (forms/start-form db ::form {:client @(re-frame/subscribe [::subs/client])})
|
||||
:dispatch (conj rule-saved (:upsert-transaction-rule result))}))
|
||||
(fn [{:keys [db]} _]
|
||||
{:db (forms/start-form db ::form {:client @(re-frame/subscribe [::subs/client])})}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::succeeded-test
|
||||
@@ -238,10 +231,6 @@
|
||||
|
||||
;; VIEWS
|
||||
|
||||
(def rule-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::changed]
|
||||
:submit-event [::saving ]
|
||||
:id ::form}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
@@ -261,133 +250,119 @@
|
||||
{::track/dispose [{:id ::client}
|
||||
{:id ::vendor-change}]}))
|
||||
|
||||
(defn form-contents [params]
|
||||
(def rule-schema
|
||||
(m/schema [:map
|
||||
[:client {:optional true}
|
||||
[:maybe schema/reference]]
|
||||
[:bank-account {:optional true}
|
||||
[:maybe schema/reference]]
|
||||
[:description
|
||||
schema/not-empty-string]
|
||||
[:amount-gte {:optional true}
|
||||
[:maybe schema/money]]
|
||||
[:amount-lte {:optional true}
|
||||
[:maybe schema/money]]
|
||||
[:dom-gte {:optional true}
|
||||
[:maybe [:int {:min 1 :max 31}]]]
|
||||
[:dom-lte {:optional true}
|
||||
[:maybe [:int {:min 1 :max 31}]]]
|
||||
[:vendor {:optional true}
|
||||
[:maybe schema/reference]]
|
||||
[:transaction-approval-status {:optional true}
|
||||
[:maybe schema/approval-status]]
|
||||
[:note {:optional true}
|
||||
[:maybe :string]]]))
|
||||
|
||||
(defn form-contents []
|
||||
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
|
||||
(let [{:keys [data id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field raw-field error-notification submit-button ]} rule-form
|
||||
default-note @(re-frame/subscribe [::default-note])
|
||||
test-state @(re-frame/subscribe [::status/single ::test])]
|
||||
^{:key id}
|
||||
(form-inline (assoc params :title "New Transaction Rule")
|
||||
[:<>
|
||||
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
default-note @(re-frame/subscribe [::default-note])
|
||||
test-state @(re-frame/subscribe [::status/single ::test])]
|
||||
[form-builder/builder {:change-event [::changed]
|
||||
:submit-event [::saving ]
|
||||
:id ::form
|
||||
:schema rule-schema}
|
||||
[form-builder/section {:title "Transaction Rule"}
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :client}
|
||||
"Client"
|
||||
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:auto-focus true
|
||||
:entity->text :name}]]
|
||||
|
||||
(field "Client"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:auto-focus true
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:field [:client]
|
||||
:spec ::entity/client}])
|
||||
|
||||
[form-builder/field-v2 {:field [:bank-account]}
|
||||
"Bank account"
|
||||
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/real-bank-accounts-for-client (:client data)])
|
||||
:entity->text :name}]]
|
||||
|
||||
|
||||
(with-meta
|
||||
(field "Bank account"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/real-bank-accounts-for-client (:client data)])
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:field [:bank-account]
|
||||
:spec ::entity/bank-account}])
|
||||
;; TODO this forces unmounting when client changes, since it is an "uncontorlled" input
|
||||
{:key (str "client-" (:id (:client data)))})
|
||||
[form-builder/field-v2 {:field :description
|
||||
:required? true}
|
||||
[:span "Description (" [:a {:href "https://regex101.com" :target "_new"} "regex tester"] ")" ]
|
||||
[:input.input {:type "text"}]]
|
||||
|
||||
(field "Yodlee Merchant"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/yodlee-merchants])
|
||||
:entity->text #(str (:name %) " - " (:yodlee-id %))
|
||||
:type "typeahead-v3"
|
||||
:field [:yodlee-merchant]}])
|
||||
[:div.field
|
||||
[:p.help "Amount"]
|
||||
[left-stack
|
||||
[form-builder/raw-field-v2 {:field :amount-gte}
|
||||
[com/money-input {:placeholder ">="}]]
|
||||
"-"
|
||||
[form-builder/raw-field-v2 {:field :amount-lte}
|
||||
[com/money-input {:placeholder "<="}]]]]
|
||||
|
||||
(field [:span "Description (" [:a {:href "https://regex101.com" :target "_new"} "regex tester"] ")" ]
|
||||
[:input.input {:type "text"
|
||||
:field [:description]
|
||||
:spec ::entity/description}])
|
||||
[:div.field
|
||||
[:p.help "Day of month"]
|
||||
[left-stack
|
||||
[form-builder/raw-field-v2 {:field :dom-gte}
|
||||
[com/number-input {:placeholder ">="
|
||||
:style {:width "7em"}}]]
|
||||
"-"
|
||||
[form-builder/raw-field-v2 {:field :dom-lte}
|
||||
[com/number-input {:placeholder "<="
|
||||
:style {:width "7em"}}]]]]
|
||||
|
||||
|
||||
[:div.field
|
||||
[:p.help "Amount"]
|
||||
[:div.control
|
||||
[:div.columns
|
||||
[:div.column
|
||||
(raw-field
|
||||
[:input.input {:type "number"
|
||||
:placeholder ">="
|
||||
:field [:amount-gte]
|
||||
:spec ::entity/amount-gte
|
||||
:step "0.01"}])]
|
||||
[:div.column
|
||||
(raw-field
|
||||
[:input.input {:type "number"
|
||||
:placeholder "<="
|
||||
:field [:amount-lte]
|
||||
:spec ::entity/amount-lte
|
||||
:step "0.01"}])]]]]
|
||||
[:h2.title.is-4 "Outcomes"]
|
||||
|
||||
[:div.field
|
||||
[:p.help "Day of Month"]
|
||||
[:div.control
|
||||
[:div.columns
|
||||
[:div.column
|
||||
(raw-field
|
||||
[:input.input {:type "number"
|
||||
:placeholder ">="
|
||||
:field [:dom-gte]
|
||||
:spec ::entity/dom-gte
|
||||
:precision 0
|
||||
:step "1"}])]
|
||||
[:div.column
|
||||
(raw-field
|
||||
[:input.input {:type "number"
|
||||
:placeholder "<="
|
||||
:field [:dom-lte]
|
||||
:spec ::entity/dom-lte
|
||||
:precision 0
|
||||
:step "1"}])]]]]
|
||||
[form-builder/field-v2 {:field :vendor}
|
||||
"Assign Vendor"
|
||||
[com/search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])}]]
|
||||
|
||||
[:h2.title.is-4 "Outcomes"]
|
||||
[form-builder/field-v2 {:field :accounts}
|
||||
"Accounts"
|
||||
[expense-accounts-field-v2 {:descriptor "account asssignment"
|
||||
:percentage-only? true
|
||||
:client (:client data)
|
||||
:locations (into ["Shared"] @(re-frame/subscribe [::subs/locations-for-client-or-bank-account (:id (:client data)) (:id (:bank-account data))]))
|
||||
:max 100}]]
|
||||
|
||||
(field "Assign Vendor"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:field [:vendor]}])
|
||||
[form-builder/field-v2 {:field :transaction-approval-status}
|
||||
"Approval Status"
|
||||
[com/button-radio-input
|
||||
{:options [[:unapproved "Unapproved"]
|
||||
[:requires-feedback "Client Review"]
|
||||
[:approved "Approved"]
|
||||
[:excluded "Excluded from Ledger"]]}]]
|
||||
|
||||
(with-meta
|
||||
(field nil
|
||||
[expense-accounts-field {:type "expense-accounts"
|
||||
:descriptor "account asssignment"
|
||||
:percentage-only? true
|
||||
:client (:client data)
|
||||
:locations (into ["Shared"] @(re-frame/subscribe [::subs/locations-for-client-or-bank-account (:id (:client data)) (:id (:bank-account data))]))
|
||||
:max 100
|
||||
:field [:accounts]}])
|
||||
{:key (str (some-> data :vendor :id str) "-" (some-> data :client :id str))})
|
||||
[form-builder/field-v2 {:field :note}
|
||||
"Note"
|
||||
[:input.input {:type "text"
|
||||
:placeholder default-note}]]
|
||||
|
||||
(field "Approval Status"
|
||||
[button-radio
|
||||
{:type "button-radio"
|
||||
:field [:transaction-approval-status]
|
||||
:options [[:unapproved "Unapproved"]
|
||||
[:requires-feedback "Client Review"]
|
||||
[:approved "Approved"]
|
||||
[:excluded "Excluded from Ledger"]]}])
|
||||
|
||||
(field "Note"
|
||||
[:input.input {:type "text"
|
||||
:field [:note]
|
||||
:placeholder default-note
|
||||
:spec (s/nilable ::entity/note)}])
|
||||
|
||||
[:div.is-divider]
|
||||
(error-notification)
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:a.button.is-medium.is-fullwidth.is-outlined {:on-click (dispatch-event [::test-clicked])
|
||||
:disabled (status/disabled-for test-state)
|
||||
:class (status/class-for test-state)}
|
||||
"Test Rule"]]
|
||||
[:div.column
|
||||
(submit-button "Save")]]]))])
|
||||
[:div.is-divider]
|
||||
[form-builder/error-notification]
|
||||
[:div.columns
|
||||
[:div.column
|
||||
[:a.button.is-medium.is-fullwidth.is-outlined {:on-click (dispatch-event [::test-clicked])
|
||||
:disabled (status/disabled-for test-state)
|
||||
:class (status/class-for test-state)}
|
||||
"Test Rule"]]
|
||||
[:div.column
|
||||
[form-builder/submit-button {:class ["is-fullwidth"]}
|
||||
"Save"]]]]])])
|
||||
|
||||
(defn form [_]
|
||||
(r/create-class
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
(ns auto-ap.views.pages.admin.users
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]
|
||||
[clojure.string :as str]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.entities.clients :as entity]
|
||||
[auto-ap.views.components.address :refer [address-field]]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.pages.admin.users.table :as table]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.utils :refer [login-url dispatch-value-change bind-field horizontal-field dispatch-event]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.utils :refer [by replace-by]]
|
||||
[cljs.reader :as edn]
|
||||
[auto-ap.routes :as routes]
|
||||
[bidi.bidi :as bidi]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.pages.admin.users.form :as form]
|
||||
[auto-ap.effects.forward :as forward]))
|
||||
|
||||
|
||||
(:require
|
||||
[auto-ap.effects.forward :as forward]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.utils :refer [replace-by]]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.pages.admin.users.form :as form]
|
||||
[auto-ap.views.pages.admin.users.table :as table]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::params
|
||||
|
||||
@@ -1,129 +1,104 @@
|
||||
(ns auto-ap.views.pages.admin.users.form
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]
|
||||
[clojure.string :as str]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.entities.clients :as entity]
|
||||
[auto-ap.views.components.address :refer [address-field]]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.utils :refer [login-url dispatch-value-change bind-field horizontal-field dispatch-event with-user]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.utils :refer [by replace-if]]
|
||||
[cljs.reader :as edn]
|
||||
[auto-ap.routes :as routes]
|
||||
[bidi.bidi :as bidi]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.views.components.modal :as modal]))
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.utils :refer [dispatch-event with-user]]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
(fn [db]
|
||||
true))
|
||||
(def client-schema
|
||||
(m/schema [:map [:client schema/reference]]))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::changed
|
||||
(forms/change-handler ::form
|
||||
(fn [data field value]
|
||||
[])))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::add-client
|
||||
[(forms/in-form ::form)]
|
||||
(fn [form [_ d]]
|
||||
(let [client (get @(re-frame/subscribe [::subs/clients-by-id])
|
||||
(get-in form [:data :adding-client]))]
|
||||
(update-in form [:data :clients] conj client ))))
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::remove-client
|
||||
[(forms/in-form ::form)]
|
||||
(fn [form [_ d]]
|
||||
(update-in form [:data :clients] #(filter (fn [c] (not= (:id c) d)) %))))
|
||||
(def user-schema
|
||||
(m/schema
|
||||
[:map
|
||||
[:name schema/not-empty-string]
|
||||
[:role [:enum :none :user :manager :power_user :admin]]
|
||||
[:clients {:optional true}
|
||||
[:maybe
|
||||
[:sequential client-schema]]]]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::saving
|
||||
[with-user (forms/in-form ::form)]
|
||||
(fn [{:keys [db user]} [_]]
|
||||
{:graphql
|
||||
{:token user
|
||||
:owns-state {:single ::form}
|
||||
:query-obj {:venia/operation {:operation/type :mutation
|
||||
:operation/name "EditUser"}
|
||||
:venia/queries [{:query/data [:edit-user
|
||||
{:edit-user (-> (:data db)
|
||||
(update :clients #(map :id %))
|
||||
(select-keys #{:id :name :clients :role}))}
|
||||
[:id :name :role [:clients [:id :name]]]]}]}
|
||||
:on-success [::saved]}}))
|
||||
(if (m/validate user-schema (:data db))
|
||||
{:graphql
|
||||
{:token user
|
||||
:owns-state {:single ::form}
|
||||
:query-obj {:venia/operation {:operation/type :mutation
|
||||
:operation/name "EditUser"}
|
||||
:venia/queries [{:query/data [:edit-user
|
||||
{:edit-user (-> (:data db)
|
||||
(update :clients #(map (comp :id :client) %))
|
||||
(select-keys #{:id :name :clients :role}))}
|
||||
[:id :name :role [:clients [:id :name]]]]}]}
|
||||
:on-success [::saved]}}
|
||||
|
||||
{:dispatch-n [[::forms/attempted-submit ::form]
|
||||
[::status/error ::form [{:message "Please fix the errors and try again."}]]]})
|
||||
))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::saved
|
||||
(forms/triggers-stop ::form)
|
||||
(fn [{:keys [db]} [_ {:keys [edit-user]}]]
|
||||
(fn [_ _]
|
||||
{:dispatch [::modal/modal-closed]}))
|
||||
|
||||
(def user-form (forms/vertical-form {:submit-event [::saving]
|
||||
:change-event [::changed]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn form []
|
||||
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field raw-field error-notification submit-button]} user-form]
|
||||
(form-inline {}
|
||||
[:<>
|
||||
(field "Name"
|
||||
[:input.input {:type "text"
|
||||
:field [:name]
|
||||
:spec ::entity/name}])
|
||||
[:div.field
|
||||
[:p.help "Role"]
|
||||
[:div.control
|
||||
[:div.select
|
||||
[raw-field
|
||||
[:select {:type "select"
|
||||
:field [:role]}
|
||||
[:option {:value ":none"} "None"]
|
||||
[:option {:value ":user"} "User"]
|
||||
[:option {:value ":manager"} "Manager"]
|
||||
[:option {:value ":power_user"} "Power User"]
|
||||
[:option {:value ":admin"} "Admin"]]]]]]
|
||||
(when (#{":user" ":manager" ":power_user"} (:role data))
|
||||
[:div.field
|
||||
[:p.help "Clients"]
|
||||
[:div.control
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[:div.select
|
||||
[raw-field
|
||||
[:select {:type "select"
|
||||
:field [:adding-client]}
|
||||
[:option]
|
||||
(let [used-clients (set (map :id (:clients data)))]
|
||||
(for [{:keys [id name] :as client} @(re-frame/subscribe [::subs/clients])
|
||||
:when (not (used-clients id))]
|
||||
^{:key id} [:option {:value id} name]))]]]]
|
||||
[:p.control
|
||||
[:button.button.is-primary {:on-click (dispatch-event [::add-client])} "Add"]]]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
clients @(re-frame/subscribe [::subs/clients])]
|
||||
[:<>
|
||||
[form-builder/builder {:submit-event [::saving]
|
||||
:id ::form
|
||||
:schema user-schema}
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :name}
|
||||
"Name"
|
||||
[:input.input {:type "text"}]]
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :role}
|
||||
"Role"
|
||||
[com/select-field {:options [[:none "None"]
|
||||
[:user "User"]
|
||||
[:manager "Manager"]
|
||||
[:power_user "Power User"]
|
||||
[:admin "Admin"]]
|
||||
:allow-nil? false
|
||||
:keywordize? true}]]
|
||||
(when (#{:user :manager :power_user} (:role data))
|
||||
[form-builder/field-v2 {:field :clients}
|
||||
"Client"
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
|
||||
[com/entity-typeahead
|
||||
{:entities clients
|
||||
:entity->text :name
|
||||
:style {:width "13em"}}]]]
|
||||
:key-fn :id
|
||||
:schema [:sequential client-schema]
|
||||
:new-text "Grant access to client"}]])
|
||||
|
||||
[form-builder/hidden-submit-button]]]))
|
||||
|
||||
[:ul
|
||||
(for [{:keys [id name]} (:clients data)]
|
||||
^{:key id} [:li name [:a.icon {:on-click (dispatch-event [::remove-client id])} [:i.fa.fa-times ]]])]]])])))
|
||||
(re-frame/reg-event-fx
|
||||
::editing
|
||||
(fn [{:keys [db]} [_ d]]
|
||||
{:db (-> db
|
||||
(forms/start-form ::form d))
|
||||
:dispatch [::modal/modal-requested {:title (str "Edit user " (:name d))
|
||||
:body [form]
|
||||
:cancel? false
|
||||
:confirm {:value "Save"
|
||||
{:db (-> db
|
||||
(forms/start-form ::form (update d :clients #(mapv (fn [x] {:client x :id (random-uuid)}) %))))
|
||||
:dispatch [::modal/modal-requested {:title (str "Edit user " (:name d))
|
||||
:body [form]
|
||||
:cancel? false
|
||||
:confirm {:value "Save"
|
||||
:status-from [::status/single ::form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::saving])
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::saving])
|
||||
:close-event [::status/completed ::form]}}]}))
|
||||
|
||||
@@ -1,65 +1,53 @@
|
||||
(ns auto-ap.views.pages.admin.vendors.merge-dialog
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.utils :refer [dispatch-event]]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{:keys [data]}]
|
||||
(println data)
|
||||
(and (:from data)
|
||||
(:to data))))
|
||||
|
||||
(def merge-form (forms/vertical-form {:submit-event [::save]
|
||||
:change-event [::forms/change ::form]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
(def merge-schema
|
||||
(m/schema [:map
|
||||
[:from schema/reference]
|
||||
[:to schema/reference]]))
|
||||
|
||||
(defn form []
|
||||
(let [_ @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field]} merge-form]
|
||||
|
||||
|
||||
(form-inline {}
|
||||
[:<>
|
||||
(field "Form Vendor (will be deleted)"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:auto-focus true
|
||||
:field [:from]}])
|
||||
[form-builder/builder {:submit-event [::try-save]
|
||||
:id ::form
|
||||
:schema merge-schema}
|
||||
[form-builder/field-v2 {:field :from}
|
||||
"Form Vendor (will be deleted)"
|
||||
[com/search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:auto-focus true}]]
|
||||
|
||||
|
||||
(field "To Vendor"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:field [:to]}])])))
|
||||
[form-builder/field-v2 {:field :to}
|
||||
"To Vendor"
|
||||
[com/search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])}]]
|
||||
[form-builder/hidden-submit-button]])
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::show
|
||||
(fn [{:keys [db]} _]
|
||||
{:dispatch [::modal/modal-requested {:title "Merge Vendors"
|
||||
:body [form]
|
||||
{:dispatch [::modal/modal-requested {:title "Merge Vendors"
|
||||
:body [form]
|
||||
:confirm {:value "Merge"
|
||||
:status-from [::status/single ::form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::save])
|
||||
:can-submit [::can-submit]
|
||||
:on-click (dispatch-event [::try-save])
|
||||
:close-event [::status/completed ::form]}}]
|
||||
:db (forms/start-form db ::form {})}
|
||||
))
|
||||
:db (forms/start-form db ::form {})}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::complete
|
||||
@@ -81,3 +69,12 @@
|
||||
{:from (:id from) :to (:id to)} []]}]}
|
||||
:on-success [::complete]}})))
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::try-save
|
||||
[(forms/in-form ::form)]
|
||||
(fn [{:keys [db]}]
|
||||
(if (not (m/validate merge-schema (:data db)))
|
||||
{:dispatch-n [[::status/error ::form [{:message "Please correct any errors and try again"}]]
|
||||
[::forms/attempted-submit ::form]]}
|
||||
{:dispatch [::save]})))
|
||||
|
||||
@@ -1,415 +0,0 @@
|
||||
(ns auto-ap.views.pages.admin.yodlee
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[auto-ap.forms :as forms]
|
||||
[reagent.core :as reagent]
|
||||
[clojure.string :as str]
|
||||
[cljs-time.format :as f]
|
||||
[cljs-time.core :as time]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.entities.clients :as entity]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.address :refer [address-field]]
|
||||
[auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field str->date date->str with-user]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.status :as status]
|
||||
[cljs.reader :as edn]
|
||||
[auto-ap.routes :as routes]
|
||||
[bidi.bidi :as bidi]))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-sub
|
||||
::authentication
|
||||
(fn [db]
|
||||
(-> db ::yodlee :authentication)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
(fn [db]
|
||||
true))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::loading?
|
||||
(fn [db]
|
||||
(-> db ::yodlee :loading?)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::accounts
|
||||
(fn [db]
|
||||
(-> db ::yodlee :accounts)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::accounts-loading?
|
||||
(fn [db]
|
||||
(-> db ::yodlee :accounts-loading?)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::provider-accounts-loading?
|
||||
(fn [db]
|
||||
(-> db ::provider-accounts-loading?)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::provider-accounts
|
||||
(fn [db]
|
||||
(-> db ::provider-accounts)))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::authenticate-with-yodlee
|
||||
(fn [{:keys [db]} _]
|
||||
{:db (assoc-in db [::yodlee :loading?] true)
|
||||
:http {:token (:user db)
|
||||
:method :get
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/yodlee/fastlink")
|
||||
:on-success [::authenticated]
|
||||
:on-error [::save-error]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn [{:keys [db]} _]
|
||||
{:db (-> db
|
||||
(assoc ::yodlee {:provider-accounts-loading? true})
|
||||
(assoc ::save-error nil)
|
||||
(assoc ::provider-accounts [])
|
||||
(assoc ::provider-accounts-loading? true))
|
||||
:http {:token (:user db)
|
||||
:method :get
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/yodlee/provider-accounts")
|
||||
:on-success [::got-provider-accounts]
|
||||
:on-error [::save-error]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::kicked
|
||||
(fn [{:keys [db]} [_ id state]]
|
||||
{:dispatch [::mounted]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::kicked
|
||||
(fn [{:keys [db]} [_ id state]]
|
||||
{:dispatch [::mounted]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::kick
|
||||
(fn [{:keys [db]} [_ id]]
|
||||
{:http {:token (:user db)
|
||||
:method :post
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/yodlee/provider-accounts/" id)
|
||||
:on-success [::kicked id :kicked]
|
||||
:on-error [::kicked id :errored]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::got-accounts
|
||||
(fn [{:keys [db]} [_ accounts]]
|
||||
{:db (-> db
|
||||
(assoc-in [::yodlee :accounts] accounts)
|
||||
(assoc-in [::yodlee :accounts-loading?] false))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::got-provider-accounts
|
||||
(fn [{:keys [db]} [_ accounts]]
|
||||
{:db (-> db
|
||||
(assoc-in [::provider-accounts] accounts)
|
||||
(assoc-in [::provider-accounts-loading?] false))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::authenticated
|
||||
(fn [{:keys [db]} [_ authentication]]
|
||||
{:db (-> db
|
||||
(assoc-in [::yodlee :authentication] authentication)
|
||||
(assoc-in [::yodlee :loading?] false))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::authenticated-mfa
|
||||
(fn [{:keys [db]} [_ provider-account-id authentication]]
|
||||
{:db (-> db
|
||||
(assoc-in [::yodlee :authentication] authentication)
|
||||
(assoc-in [::yodlee :loading?] false)
|
||||
(forms/stop-form [::mfa-form provider-account-id]))}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save-error
|
||||
(fn [{:keys [db]} [_ authentication]]
|
||||
{:db (assoc db ::load-error "error")}))
|
||||
|
||||
(defn yodlee-link-button []
|
||||
[:div
|
||||
(let [authentication @(re-frame/subscribe [::authentication])
|
||||
loading? @(re-frame/subscribe [::loading?])]
|
||||
|
||||
(if authentication
|
||||
[:div
|
||||
"Authentication successful!"
|
||||
[:form {:action (:url authentication) :method "POST"}
|
||||
[:input {:type "hidden"
|
||||
:name "rsession"
|
||||
:value (:session authentication)}]
|
||||
[:input {:type "hidden"
|
||||
:name "token"
|
||||
:value (:token authentication)}]
|
||||
[:input {:type "hidden"
|
||||
:name "app"
|
||||
:value (:app authentication)}]
|
||||
|
||||
[:input {:type "hidden"
|
||||
:name "redirectReq"
|
||||
:value "true"}]
|
||||
[:button.button.is-primary [:span [:span.icon [:i.fa.fa-external-link]] " Go to yodlee"]]]]
|
||||
|
||||
[:button.button.is-primary {:class (if loading? "is-loading" "") :on-click (dispatch-event [::authenticate-with-yodlee])} "Authenticate with Yodlee"]))])
|
||||
|
||||
(defn yodlee-date->date [d]
|
||||
(try
|
||||
(some-> d
|
||||
(str->date (:date-time-no-ms f/formatters))
|
||||
)
|
||||
(catch js/Error e
|
||||
nil)))
|
||||
|
||||
(defn yodlee-date->str [d]
|
||||
(try
|
||||
(or (some-> d
|
||||
(str->date (:date-time-no-ms f/formatters))
|
||||
date->str)
|
||||
"N/A")
|
||||
(catch js/Error e
|
||||
"N/A")))
|
||||
|
||||
(defn yodlee-accounts-table [accounts]
|
||||
(let [bank-accounts @(re-frame/subscribe [::bank-accounts-by-yodlee-account-id])]
|
||||
[:div
|
||||
[:table.table
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Account Name"]
|
||||
[:th "Account Number"]
|
||||
[:th "Yodlee Account Number"]
|
||||
[:th "Balance"]
|
||||
[:th "Yodlee Status"]
|
||||
[:th "Usage"]]]
|
||||
[:tbody
|
||||
|
||||
(for [account accounts]
|
||||
^{:key (:id account)} [:tr
|
||||
[:td (:accountName account)]
|
||||
[:td (:accountNumber account)]
|
||||
[:td (:id account)]
|
||||
[:td.has-text-right (:amount (:balance account))]
|
||||
[:td (str/join ", " (map :additionalStatus (:dataset account)))]
|
||||
[:td
|
||||
(when-let [bank-accounts (get bank-accounts (:id account))]
|
||||
[:div.tags
|
||||
(for [bank-account bank-accounts]
|
||||
^{:key (:id bank-account)}
|
||||
[:div.tag (:name bank-account) " (" (:code bank-account) ")"])])]
|
||||
])]]]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::reauthenticate-mfa
|
||||
[with-user ]
|
||||
(fn [{:keys [user db]} [_ provider-account-id ]]
|
||||
{:db (forms/loading db [::mfa-form provider-account-id])
|
||||
:http {:token user
|
||||
:method :post
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/yodlee/reauthenticate/" provider-account-id )
|
||||
:body {"loginForm"
|
||||
{"row"
|
||||
(->> (get-in db [::forms/forms [::mfa-form provider-account-id]])
|
||||
:data
|
||||
:login
|
||||
(sort-by (fn [[k v]] k))
|
||||
(map second)
|
||||
(map (fn [row]
|
||||
{"field"
|
||||
(mapv (fn [[k v]]
|
||||
{"id" k
|
||||
"value" v})
|
||||
row)})))}
|
||||
"field"
|
||||
(mapv (fn [[k v]]
|
||||
{"id" k
|
||||
"value" v})
|
||||
(:mfa (:data (get-in db [::forms/forms [::mfa-form provider-account-id]]))))}
|
||||
|
||||
:on-success [::authenticated-mfa provider-account-id]
|
||||
:on-error [::forms/save-error [::mfa-form provider-account-id] ]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::provider-account-refreshed
|
||||
(fn [{:keys [db]} [_ i result]]
|
||||
|
||||
{:db (assoc-in db [::provider-accounts] result)
|
||||
:dispatch [::forms/form-closing [::refresh-provider-account i]]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::refresh-provider-account
|
||||
[with-user ]
|
||||
(fn [{:keys [user db]} [_ provider-account-id ]]
|
||||
{:db (forms/loading db [::refresh-provider-account provider-account-id])
|
||||
:http {:token user
|
||||
:method :post
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/yodlee/provider-accounts/refresh/" provider-account-id )
|
||||
:body {}
|
||||
:on-success [::provider-account-refreshed provider-account-id]
|
||||
:on-error [::forms/save-error [::refresh-provider-account provider-account-id] ]}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::provider-account-deleted
|
||||
(fn [{:keys [db]} [_ i result]]
|
||||
{:db (assoc-in db [::provider-accounts] result)
|
||||
:dispatch-n [[::forms/form-closing [::refresh-provider-account i]]
|
||||
[::modal/modal-closed ]]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::delete-provider-account
|
||||
[with-user ]
|
||||
(fn [{:keys [user db]} [_ provider-account-id ]]
|
||||
{:http {:token user
|
||||
:method :post
|
||||
:owns-state {:single ::delete-provider-account}
|
||||
:headers {"Content-Type" "application/edn"}
|
||||
:uri (str "/api/yodlee/provider-accounts/delete/" provider-account-id )
|
||||
:body {}
|
||||
:on-success [::provider-account-deleted provider-account-id]
|
||||
:on-error [::forms/save-error [::delete-provider-account provider-account-id] ]}}))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::delete-requested
|
||||
[with-user]
|
||||
(fn [{:keys [user db]} [_ account-id]]
|
||||
{:dispatch
|
||||
[::modal/modal-requested {:title "Delete Provider account "
|
||||
:body [:div "Are you sure you want to delete provider account " account-id "?"]
|
||||
:confirm {:value "Delete provider account"
|
||||
:status-from [::status/single ::delete-provider-account]
|
||||
:class "is-danger"
|
||||
:on-click (dispatch-event [::delete-provider-account account-id])
|
||||
:close-event [::status/completed ::delete-provider-account]}
|
||||
:cancel? true}]}))
|
||||
|
||||
|
||||
(defn delete-button [account-id]
|
||||
[:button.button
|
||||
{:on-click (dispatch-event [::delete-requested account-id])}
|
||||
[:span.icon [:i.fa.fa-times]]])
|
||||
|
||||
(re-frame/reg-sub
|
||||
::bank-accounts-by-yodlee-account-id
|
||||
:<- [::subs/bank-accounts]
|
||||
(fn [bank-accounts]
|
||||
(group-by :yodlee-account-id bank-accounts)))
|
||||
|
||||
(defn yodlee-provider-accounts-table []
|
||||
(let [bank-accounts @(re-frame/subscribe [::bank-accounts-by-yodlee-account-id])]
|
||||
|
||||
(if @(re-frame/subscribe [::provider-accounts-loading?])
|
||||
[:div "Loading..."]
|
||||
[:div.columns
|
||||
[:div.column.is-half
|
||||
(doall
|
||||
(for [account @(re-frame/subscribe [::provider-accounts])
|
||||
:let [{:keys [error status] :as g} @(re-frame/subscribe [::forms/form [::refresh-provider-account (:id account)]])
|
||||
total-usages (mapcat (comp bank-accounts :id) (:accounts account))]]
|
||||
|
||||
^{:key (:id account)}
|
||||
[:div.card {:style {:margin-bottom "1em"}}
|
||||
[:div.card-header
|
||||
[:div.card-header-title "Provider account " (:id account)]
|
||||
[:div.card-header-icon
|
||||
(when (seq total-usages)
|
||||
[:div.tags
|
||||
[:div.tag.is-primary (count total-usages) " usages"]])]
|
||||
[:div.card-header-icon
|
||||
[delete-button (:id account)]]
|
||||
[:div.card-header-icon
|
||||
(cond
|
||||
(= :loading status) [:button.button.is-disabled.is-loading [:i.fa.fa-refresh]]
|
||||
error [:button.button.is-disabled [:span.icon [:i.fa.fa-exclamation-triangle]]]
|
||||
:else
|
||||
[:button.button
|
||||
{:on-click (dispatch-event [::refresh-provider-account (:id account)])}
|
||||
[:span.icon [:i.fa.fa-refresh]]])]]
|
||||
[:div.card-content
|
||||
|
||||
(if (> (some-> (-> account :dataset first :lastUpdated)
|
||||
(yodlee-date->date )
|
||||
(time/interval (time/now))
|
||||
(time/in-days ))
|
||||
1)
|
||||
[:div.notification.is-info.is-light
|
||||
[:div.level
|
||||
[:div.level-left
|
||||
[:div.level-item
|
||||
[:p
|
||||
"This account was last updated on "
|
||||
(yodlee-date->str (-> account :dataset first :lastUpdated))
|
||||
", and last attempted "
|
||||
(yodlee-date->str (-> account :dataset first :lastUpdateAttempt))
|
||||
"."]]]
|
||||
[:div.level-right [:button.button.is-success {:on-click (dispatch-event [::kick (:id account)] )} "Sync yodlee with bank" ]]]
|
||||
|
||||
])
|
||||
|
||||
|
||||
[yodlee-accounts-table (:accounts account)]
|
||||
(if (not= (-> account :dataset first :additionalStatus)
|
||||
"AVAILABLE_DATA_RETRIEVED")
|
||||
[:div
|
||||
[:div.notification.is-info.is-warning
|
||||
[:div.level
|
||||
[:div.level-left
|
||||
[:div.level-item
|
||||
"This provider account's status is '"
|
||||
(-> account :dataset first :additionalStatus)
|
||||
"'. If this is in error, it might help to try reauthenticating by filling out the form below."]]]]
|
||||
(let [{error :error account-data :data } @(re-frame/subscribe [::forms/form [::mfa-form (:id account)]])
|
||||
change-event [::forms/change [::mfa-form (:id account)]]
|
||||
{:keys [form-inline field field-holder raw-field error-notification submit-button]} (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event change-event
|
||||
:submit-event [::reauthenticate-mfa (:id account)]
|
||||
:id [::mfa-form (:id account)]} )]
|
||||
(form-inline {:title "Reauthenticate"}
|
||||
[:<>
|
||||
(error-notification)
|
||||
(doall
|
||||
(for [[row i] (map vector (-> account :loginForm last :row) (range))
|
||||
f (:field row)
|
||||
:let [options (map :optionValue (:option f))]]
|
||||
^{:key (:id f)}
|
||||
[:div
|
||||
(field (:label row)
|
||||
[:input.input {:type "text" :field [:login i (:id f)]}])
|
||||
(if (seq options)
|
||||
[:ul
|
||||
(for [o options]
|
||||
^{:key o}
|
||||
[:li [:pre o]])])]))
|
||||
(doall
|
||||
(for [f (-> account :field)]
|
||||
^{:key (:id f)}
|
||||
(field (:label f)
|
||||
[:input.input {:type "text" :mfa [:form (:id f)] :value (-> f :field first :value)}])))
|
||||
(submit-button "Reauthenticate")]))])]]))]])))
|
||||
|
||||
|
||||
(defn admin-yodlee-content []
|
||||
[(with-meta
|
||||
(fn []
|
||||
[:div
|
||||
[:h1.title "Yodlee provider accounts"]
|
||||
|
||||
[yodlee-provider-accounts-table]
|
||||
[yodlee-link-button]])
|
||||
{:component-did-mount (fn []
|
||||
(re-frame/dispatch [::mounted]))})])
|
||||
|
||||
#_(defn admin-yodlee-page []
|
||||
[side-bar-layout {:side-bar [admin-side-bar {}]
|
||||
:main [admin-yodlee-content]}])
|
||||
@@ -1,26 +1,15 @@
|
||||
(ns auto-ap.views.pages.admin.yodlee2
|
||||
(:require [re-frame.core :as re-frame]
|
||||
[auto-ap.forms :as forms]
|
||||
[reagent.core :as reagent]
|
||||
[clojure.string :as str]
|
||||
[cljs-time.format :as f]
|
||||
[cljs-time.core :as time]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.entities.clients :as entity]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.address :refer [address-field]]
|
||||
[auto-ap.views.utils :refer [login-url dispatch-event dispatch-value-change bind-field horizontal-field str->date date->str with-user]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.status :as status]
|
||||
[cljs.reader :as edn]
|
||||
[auto-ap.routes :as routes]
|
||||
[bidi.bidi :as bidi]
|
||||
[auto-ap.views.pages.admin.yodlee2.table :as table]
|
||||
[auto-ap.views.pages.admin.yodlee2.form :as form]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.effects.forward :as forward]))
|
||||
(:require
|
||||
[auto-ap.effects.forward :as forward]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.pages.admin.yodlee2.table :as table]
|
||||
[auto-ap.views.utils :refer [dispatch-event]]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as reagent]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::authentication
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
(ns auto-ap.views.pages.admin.plaid
|
||||
(ns auto-ap.views.pages.company.plaid
|
||||
(:require
|
||||
[auto-ap.effects.forward :as forward]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
|
||||
[auto-ap.views.pages.company.side-bar :refer [company-side-bar]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.pages.admin.plaid.table :as table]
|
||||
@@ -85,7 +85,8 @@
|
||||
(re-frame/reg-event-fx
|
||||
::unmounted
|
||||
(fn [{:keys [db]} _]
|
||||
{::forward/dispose {:id ::plaid-item-deleted}}))
|
||||
{::forward/dispose {:id ::plaid-item-deleted}
|
||||
::track/dispose [{:id ::params}]}))
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
@@ -146,19 +147,17 @@
|
||||
|
||||
(defn plaid-link-token-button []
|
||||
(let [status @(re-frame/subscribe [::status/single ::get-link-token])
|
||||
client-code (:code @(re-frame/subscribe [::subs/client]))]
|
||||
client @(re-frame/subscribe [::subs/client])]
|
||||
[:button.button.is-primary {:disabled (status/disabled-for status)
|
||||
:class (status/class-for status)
|
||||
:on-click (dispatch-event [::get-link-token client-code])}
|
||||
"Authenticate with Plaid (" client-code ")"]))
|
||||
:on-click (dispatch-event [::get-link-token (:code client)])}
|
||||
"Authenticate with Plaid (" (:name client) ")"]))
|
||||
|
||||
(defn link-flow []
|
||||
[:div
|
||||
(let [link-token @(re-frame/subscribe [::link-token])
|
||||
client-code (:code @(re-frame/subscribe [::subs/client]))]
|
||||
(cond
|
||||
|
||||
|
||||
(and link-token client-code)
|
||||
[:div
|
||||
"Authentication successful!"
|
||||
@@ -185,12 +184,12 @@
|
||||
]))
|
||||
|
||||
|
||||
(defn admin-plaid-page []
|
||||
(defn plaid-page []
|
||||
(reagent/create-class
|
||||
{:component-will-unmount #(re-frame/dispatch [::unmounted])
|
||||
:component-did-mount #(re-frame/dispatch [::mounted])
|
||||
:reagent-render (fn []
|
||||
[side-bar-layout {:side-bar [admin-side-bar {}]
|
||||
[side-bar-layout {:side-bar [company-side-bar {}]
|
||||
:main [admin-plaid-item-content]}])}))
|
||||
|
||||
|
||||
@@ -14,4 +14,8 @@
|
||||
[:a.item {:href (bidi/path-for routes/routes :reports)
|
||||
:class [(active-when ap = :reports)]}
|
||||
[:span {:class "icon icon-receipt" :style {:font-size "25px"}}]
|
||||
[:span {:class "name"} "Reports"]]]]]))
|
||||
[:span {:class "name"} "Reports"]]]]
|
||||
[:li.menu-item
|
||||
[:a {:href (bidi/path-for routes/routes :plaid), :class (str "item" (active-when ap = :plaid))}
|
||||
[:span {:class "icon icon-saving-bank-1" :style {:font-size "25px"}}]
|
||||
[:span {:class "name"} "Plaid Link"]]]]))
|
||||
|
||||
@@ -1,105 +1,109 @@
|
||||
(ns auto-ap.views.pages.invoices.advanced-print-checks
|
||||
(:require [auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.pages.invoices.common :refer [invoice-read does-amount-exceed-outstanding?]]
|
||||
[auto-ap.views.pages.invoices.form :as form]
|
||||
[auto-ap.views.utils :refer [dispatch-event horizontal-field with-user]]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{ {:keys [invoices invoice-amounts]} :data}]
|
||||
(cond (seq (filter
|
||||
(fn [{:keys [id outstanding-balance]}]
|
||||
(does-amount-exceed-outstanding? (get-in invoice-amounts [id :amount]) outstanding-balance ))
|
||||
invoices))
|
||||
false
|
||||
|
||||
:else
|
||||
true)))
|
||||
|
||||
(def advanced-print-checks-form (forms/vertical-form {:submit-event [::save]
|
||||
:change-event [::forms/change ::form]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.money-field :refer [money-field]]
|
||||
[auto-ap.views.pages.invoices.common
|
||||
:refer [does-amount-exceed-outstanding? invoice-read]]
|
||||
[auto-ap.views.utils :refer [coerce-float dispatch-event with-user]]
|
||||
[malli.core :as m]
|
||||
[malli.error :as me]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(def advanced-print-schema (m/schema
|
||||
[:and
|
||||
[:map
|
||||
[:bank-account-id schema/not-empty-string]
|
||||
[:invoice-amounts [:map-of
|
||||
:string
|
||||
[:map
|
||||
[:amount schema/money]]]]]
|
||||
[:fn (fn [{:keys [invoices invoice-amounts] :as z}]
|
||||
(if (seq (filter
|
||||
(fn [{:keys [id outstanding-balance]}]
|
||||
(does-amount-exceed-outstanding? (get-in invoice-amounts [id :amount]) outstanding-balance ))
|
||||
invoices))
|
||||
(throw (ex-info "Invalid" {:type ::too-much-invoice}))
|
||||
true))]]))
|
||||
|
||||
(defn form []
|
||||
(let [real-bank-accounts @(re-frame/subscribe [::subs/real-bank-accounts])
|
||||
{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} advanced-print-checks-form]
|
||||
{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
|
||||
(form-inline {}
|
||||
[:<>
|
||||
[:div.field
|
||||
[:label.label "Pay using"]
|
||||
[:div.control
|
||||
[:span.select
|
||||
[raw-field
|
||||
[:select {:type "select"
|
||||
:field :bank-account-id}
|
||||
(for [{:keys [id number name]} real-bank-accounts]
|
||||
^{:key id} [:option {:value id} name])]]]]]
|
||||
[form-builder/builder {:submit-event [::try-save]
|
||||
:id ::form
|
||||
:schema advanced-print-schema}
|
||||
[form-builder/field-v2 {:field :bank-account-id}
|
||||
"Pay using"
|
||||
[com/select-field {:options (for [{:keys [id name]} real-bank-accounts]
|
||||
[id name])
|
||||
:allow-nil? true}]]
|
||||
|
||||
[:table.table.is-fullwidth
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Vendor"]
|
||||
[:th "Invoice ID"]
|
||||
[:th {:style {"width" "10em"}} "Payment"]]]
|
||||
[:tbody
|
||||
(doall
|
||||
(for [{:keys [vendor payment outstanding-balance invoice-number id] :as i} (:invoices data)]
|
||||
^{:key id}
|
||||
[:tr
|
||||
[:td (:name vendor)]
|
||||
[:td invoice-number]
|
||||
|
||||
[:td [:div.field.has-addons.is-extended
|
||||
[:p.control [:a.button.is-static "$"]]
|
||||
[:p.control
|
||||
(raw-field
|
||||
[:input.input.has-text-right {:type "number"
|
||||
:field [:invoice-amounts id :amount]
|
||||
:step "0.01"}])]]]]))]]])))
|
||||
[:table.table.is-fullwidth
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Vendor"]
|
||||
[:th "Invoice ID"]
|
||||
[:th {:style {"width" "10em"}} "Payment"]]]
|
||||
[:tbody
|
||||
(doall
|
||||
(for [{:keys [vendor invoice-number id]} (:invoices data)]
|
||||
^{:key id}
|
||||
[:tr
|
||||
[:td (:name vendor)]
|
||||
[:td invoice-number]
|
||||
|
||||
[:td
|
||||
[form-builder/raw-field-v2 {:field [:invoice-amounts id :amount]}
|
||||
[money-field]]]]))]]]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::show
|
||||
(fn [{:keys [db]} [_ invoices]]
|
||||
{:dispatch [::modal/modal-requested {:title "Print Checks"
|
||||
:body [form]
|
||||
:confirm {:value "Print checks"
|
||||
{:dispatch [::modal/modal-requested {:title "Print Checks"
|
||||
:body [form]
|
||||
:confirm {:value "Print checks"
|
||||
:status-from [::status/single ::form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::save])
|
||||
:can-submit [::can-submit]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::try-save])
|
||||
:close-event [::status/completed ::form]}}]
|
||||
:db (-> db
|
||||
:db (-> db
|
||||
(forms/start-form ::form
|
||||
{:bank-account-id (:id (first @(re-frame/subscribe [::subs/real-bank-accounts])))
|
||||
:invoices invoices
|
||||
{:invoices invoices
|
||||
:invoice-amounts (into {}
|
||||
(map (fn [i] [(:id i)
|
||||
{:amount (:outstanding-balance i)}])
|
||||
{:amount (coerce-float (:outstanding-balance i))}])
|
||||
invoices))}))}))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::try-save
|
||||
[(forms/in-form ::form)]
|
||||
(fn [{:keys [db]}]
|
||||
#_(println (m/explain advanced-print-schema (:data db)))
|
||||
(if (not (m/validate advanced-print-schema (:data db)))
|
||||
{:dispatch-n [[::status/error ::form [{:message "Please correct any errors and try again"}]]
|
||||
[::forms/attempted-submit ::form]]}
|
||||
{:dispatch [::save]})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::save
|
||||
[with-user (forms/in-form ::form) ]
|
||||
(fn [{:keys [db user]} [_ bank-account-id]]
|
||||
(fn [{:keys [db user]} _]
|
||||
|
||||
(let [type (or (->> @(re-frame/subscribe [::subs/client])
|
||||
:bank-accounts
|
||||
(filter #(= bank-account-id (:id %)))
|
||||
(filter #(= (:bank-account-id (:data db)) (:id %)))
|
||||
first
|
||||
:type)
|
||||
:check)
|
||||
{:keys [date invoices invoice-amounts check-number bank-account-id client]} (:data db)]
|
||||
{:keys [invoices invoice-amounts bank-account-id]} (:data db)]
|
||||
{:graphql
|
||||
{:token user
|
||||
:owns-state {:single ::form}
|
||||
@@ -123,6 +127,6 @@
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::checks-printed
|
||||
(fn [{:keys [db]} [_ data]]
|
||||
(fn [{:keys [_]} [_ _]]
|
||||
{:dispatch [::modal/modal-closed]}))
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
(ns auto-ap.views.pages.invoices.form
|
||||
(:require
|
||||
[auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
@@ -8,10 +7,11 @@
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.time-utils :refer [next-dom]]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.views.components.expense-accounts-field
|
||||
:as eaf
|
||||
:refer [recalculate-amounts
|
||||
expense-accounts-field]]
|
||||
expense-accounts-field-v2]]
|
||||
[auto-ap.views.components.layouts :as layouts]
|
||||
[auto-ap.views.components.level :refer [left-stack]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
@@ -20,19 +20,33 @@
|
||||
[auto-ap.views.components.switch-field :refer [switch-field]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.pages.invoices.common :refer [invoice-read]]
|
||||
[auto-ap.views.utils
|
||||
:refer [date-picker-optional
|
||||
:refer [date-picker
|
||||
dispatch-event
|
||||
with-user]]
|
||||
[cljs-time.core :as c]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]
|
||||
[malli.core :as m]
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[vimsical.re-frame.fx.track :as track]))
|
||||
[vimsical.re-frame.fx.track :as track]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
|
||||
(def schema (m/schema
|
||||
[:map
|
||||
[:client schema/reference]
|
||||
[:vendor schema/reference]
|
||||
[:date schema/date]
|
||||
[:due {:optional true} [:maybe schema/date]]
|
||||
[:scheduled-payment {:optional true} [:maybe schema/date]]
|
||||
[:invoice-number schema/not-empty-string]
|
||||
[:total schema/money]
|
||||
[:expense-accounts eaf/schema]]))
|
||||
|
||||
;; SUBS
|
||||
(re-frame/reg-sub
|
||||
@@ -42,11 +56,11 @@
|
||||
(let [min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
|
||||
nil
|
||||
(- (:total (:original data)) (:outstanding-balance (:original data))))
|
||||
account-total (reduce + 0 (map (fn [ea] (js/parseFloat (:amount ea))) (:expense-accounts data)))]
|
||||
(and (s/valid? ::invoice/invoice data)
|
||||
(or (not min-total) (>= (:total data) min-total))
|
||||
(or (not (:id data))
|
||||
(dollars= (Math/abs (js/parseFloat (:total data))) (Math/abs account-total)))))))
|
||||
account-total (reduce + 0 (map :amount (:expense-accounts data)))]
|
||||
(and
|
||||
(or (not min-total) (>= (:total data) min-total))
|
||||
(or (not (:id data))
|
||||
(dollars= (Math/abs (:total data)) (Math/abs account-total)))))))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::create-query
|
||||
@@ -143,8 +157,8 @@
|
||||
:vendor (:vendor edit-invoice)
|
||||
:client (:client edit-invoice)
|
||||
:expense-accounts (eaf/from-graphql (:expense-accounts which)
|
||||
(:total which)
|
||||
locations-for-client)}))})))
|
||||
(:total which)
|
||||
locations-for-client)}))})))
|
||||
|
||||
|
||||
|
||||
@@ -321,7 +335,8 @@
|
||||
[form-builder/builder {:can-submit [::can-submit]
|
||||
:change-event [::changed]
|
||||
:submit-event [::save-requested [::saving ]]
|
||||
:id ::form}
|
||||
:id ::form
|
||||
:schema schema}
|
||||
|
||||
[form-builder/section {:title [:div "New Invoice "
|
||||
(cond
|
||||
@@ -344,71 +359,61 @@
|
||||
nil)]}
|
||||
|
||||
(when-not active-client
|
||||
[form-builder/field {:required? true}
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field [:client]}
|
||||
"Client"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:style {:width "18em"}
|
||||
:auto-focus (if active-client false true)
|
||||
:field [:client]
|
||||
:disabled exists?}]])
|
||||
[form-builder/field {:required? true}
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field [:vendor]}
|
||||
"Vendor"
|
||||
[search-backed-typeahead {:disabled exists?
|
||||
:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:auto-focus (if active-client true false)
|
||||
:field [:vendor]}]]
|
||||
[form-builder/vertical-control {:required? true}
|
||||
:style {:width "18em"}
|
||||
:auto-focus (if active-client true false)}]]
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :date}
|
||||
"Date"
|
||||
[:label
|
||||
[form-builder/raw-field
|
||||
[date-picker-optional {:type "date2"
|
||||
:field [:date]
|
||||
:output :cljs-date}]]]]
|
||||
[date-picker {:output :cljs-date}]]
|
||||
|
||||
[form-builder/field
|
||||
[form-builder/field-v2 {:field [:due]}
|
||||
"Due (optional)"
|
||||
[date-picker-optional {:type "date2"
|
||||
:field [:due]
|
||||
:output :cljs-date}]]
|
||||
[date-picker {:output :cljs-date}]]
|
||||
[form-builder/vertical-control
|
||||
"Scheduled payment (optional)"
|
||||
[left-stack
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[date-picker-optional {:type "date2"
|
||||
:field [:scheduled-payment]
|
||||
:output :cljs-date}]]]
|
||||
[form-builder/raw-field-v2 {:field :scheduled-payment}
|
||||
[date-picker {:output :cljs-date}]]
|
||||
[form-builder/raw-error-v2 {:field :scheduled-payment}]]
|
||||
[:div.control
|
||||
[form-builder/raw-field
|
||||
[form-builder/raw-field-v2 {:field :schedule-when-due}
|
||||
|
||||
[switch-field {:id "schedule-when-due"
|
||||
:field [:schedule-when-due]
|
||||
:label "Same as due date"
|
||||
:type "checkbox"}]]]]]
|
||||
[form-builder/field {:required? true}
|
||||
[com/switch-input {:id "schedule-when-due"
|
||||
:label "Same as due date"}]]]]]
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :invoice-number}
|
||||
"Invoice #"
|
||||
[:input.input {:type "text"
|
||||
:field [:invoice-number]}]]
|
||||
[form-builder/field {:required? true}
|
||||
[:input.input {:style {:width "12em"}}]]
|
||||
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :total}
|
||||
"Total"
|
||||
[money-field {:type "money"
|
||||
:field [:total]
|
||||
:disabled (if can-change-amount? "" "disabled")
|
||||
[money-field {:disabled (if can-change-amount? "" "disabled")
|
||||
:style {:max-width "8em"}
|
||||
:min min-total
|
||||
:step "0.01"}]]]
|
||||
[form-builder/raw-field
|
||||
[expense-accounts-field {:type "expense-accounts"
|
||||
:descriptor "expense account"
|
||||
:locations (:locations (:client data))
|
||||
:max (:total data)
|
||||
:client (or (:client data) active-client)
|
||||
:field [:expense-accounts]}]]
|
||||
:min min-total}]]]
|
||||
[form-builder/field-v2 {:field :expense-accounts}
|
||||
"Expense Accounts"
|
||||
[expense-accounts-field-v2 {:descriptor "expense account"
|
||||
:locations (:locations (:client data))
|
||||
:max (:total data)
|
||||
:client (or (:client data) active-client)}]]
|
||||
[form-builder/error-notification]
|
||||
[:div {:style {:margin-bottom "1em"}}]
|
||||
[:div.columns
|
||||
@@ -433,6 +438,7 @@
|
||||
(list
|
||||
^{:key (str id "-check")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :check]])} "Print checks from " name]
|
||||
^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :debit]])} "Debit from " name]))))]]])
|
||||
|
||||
[:div.column
|
||||
[form-builder/submit-button {:class ["is-fullwidth"]}
|
||||
"Save"]]]])])
|
||||
|
||||
@@ -1,84 +1,74 @@
|
||||
(ns auto-ap.views.pages.invoices.handwritten-checks
|
||||
(:require [auto-ap.entities.invoice :as invoice]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.pages.invoices.common
|
||||
:refer
|
||||
[does-amount-exceed-outstanding? invoice-read]]
|
||||
[auto-ap.views.pages.invoices.form :as form]
|
||||
[auto-ap.views.utils
|
||||
:refer
|
||||
[date-picker dispatch-event horizontal-field with-user]]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]))
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.money-field :refer [money-field]]
|
||||
[auto-ap.views.pages.invoices.common
|
||||
:refer [does-amount-exceed-outstanding? invoice-read]]
|
||||
[auto-ap.views.utils
|
||||
:refer [date-picker dispatch-event with-user]]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.views.components :as com]
|
||||
[malli.core :as m]
|
||||
[auto-ap.schema :as schema]))
|
||||
|
||||
(def handwrite-checks-form (forms/vertical-form {:submit-event [::save]
|
||||
:change-event [::forms/change ::form]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
(def handwritten-check-schema
|
||||
(m/schema
|
||||
[:map
|
||||
[:bank-account-id schema/not-empty-string]
|
||||
[:date schema/date]
|
||||
[:check-number [:int {:min 1000 :max 99999}]]
|
||||
]))
|
||||
|
||||
(defn form []
|
||||
(let [real-bank-accounts @(re-frame/subscribe [::subs/real-bank-accounts])
|
||||
{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} handwrite-checks-form]
|
||||
(form-inline {}
|
||||
[:<>
|
||||
[:div.field
|
||||
[:label.label "Pay using"]
|
||||
[:div.control
|
||||
[:span.select
|
||||
[raw-field
|
||||
[:select {:type "select"
|
||||
:field :bank-account-id}
|
||||
(for [{:keys [id number name]} real-bank-accounts]
|
||||
^{:key id} [:option {:value id} name])]]]]]
|
||||
(field "Date"
|
||||
[date-picker {:class-name "input"
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:placeholder "mm/dd/yyyy"
|
||||
:next-month-button-label ""
|
||||
:next-month-label ""
|
||||
:type "date"
|
||||
:field [:date]
|
||||
:spec ::invoice/date}])
|
||||
(field "Check number"
|
||||
[:input.input {:type "number"
|
||||
:field [:check-number]}])
|
||||
[:table.table.is-fullwidth
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Invoice ID"]
|
||||
[:th {:style {"width" "14em"}} "Payment"]]]
|
||||
[:tbody
|
||||
(doall
|
||||
(for [{:keys [payment outstanding-balance invoice-number id] :as i} (:invoices data)]
|
||||
^{:key id}
|
||||
[:tr
|
||||
[:td invoice-number]
|
||||
[:td [:div.field.has-addons.is-extended
|
||||
[:p.control [:a.button.is-static "$"]]
|
||||
[:p.control
|
||||
{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/builder {:submit-event [::save]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form
|
||||
:schema handwritten-check-schema}
|
||||
[form-builder/field-v2 {:field :bank-account-id}
|
||||
"Pay using"
|
||||
[com/select-field {:options (for [{:keys [id name]} real-bank-accounts]
|
||||
[id name])
|
||||
:allow-nil? true}]]
|
||||
|
||||
(raw-field
|
||||
[:input.input.has-text-right {:type "number"
|
||||
:field [:invoice-amounts id :amount]
|
||||
#_#_:max outstanding-balance
|
||||
:step "0.01"}])]]]]))]]])))
|
||||
[form-builder/field-v2 {:field :date}
|
||||
"Date"
|
||||
[date-picker {:type "date"
|
||||
:output :cljs-date}]]
|
||||
|
||||
[form-builder/field-v2 {:field :check-number}
|
||||
"Check number"
|
||||
[com/number-input {:style {:width "8em"}}]]
|
||||
[:table.table.is-fullwidth
|
||||
[:thead
|
||||
[:tr
|
||||
[:th "Invoice ID"]
|
||||
[:th {:style {"width" "14em"}} "Payment"]]]
|
||||
[:tbody
|
||||
(doall
|
||||
(for [{:keys [invoice-number id]} (:invoices data)]
|
||||
^{:key id}
|
||||
[:tr
|
||||
[:td invoice-number]
|
||||
[:td
|
||||
[form-builder/raw-field-v2 {:field [:invoice-amounts id :amount]}
|
||||
[money-field {:style {:max-width "9em"}}]]]]))]]
|
||||
[form-builder/hidden-submit-button]]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{ {:keys [check-number date invoices invoice-amounts]} :data}]
|
||||
(boolean (cond (seq (filter
|
||||
(fn [{:keys [id outstanding-balance]}]
|
||||
(does-amount-exceed-outstanding? (get-in invoice-amounts [id :amount]) outstanding-balance ))
|
||||
invoices))
|
||||
(fn [{:keys [id outstanding-balance]}]
|
||||
(does-amount-exceed-outstanding? (get-in invoice-amounts [id :amount]) outstanding-balance ))
|
||||
invoices))
|
||||
false
|
||||
|
||||
:else
|
||||
@@ -98,7 +88,7 @@
|
||||
:close-event [::status/completed ::form]}}]
|
||||
:db (-> db
|
||||
(forms/start-form ::form
|
||||
{:bank-account-id (:id (first @(re-frame/subscribe [::subs/real-bank-accounts])))
|
||||
{:bank-account-id nil
|
||||
:invoices invoices
|
||||
:invoice-amounts (into {}
|
||||
(map (fn [i] [(:id i)
|
||||
@@ -132,6 +122,6 @@
|
||||
(re-frame/reg-event-fx
|
||||
::succeeded
|
||||
[(forms/triggers-stop ::form)]
|
||||
(fn [{:keys [db]} [_ invoices]]
|
||||
(fn [_ _]
|
||||
{:dispatch [::modal/modal-closed]}))
|
||||
|
||||
|
||||
@@ -13,11 +13,9 @@
|
||||
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
|
||||
[auto-ap.views.pages.ledger.table :as ledger-table]
|
||||
[auto-ap.views.utils
|
||||
:refer [date->str
|
||||
date-picker-friendly
|
||||
:refer [date-picker
|
||||
dispatch-event
|
||||
local-now
|
||||
standard
|
||||
with-user]]
|
||||
[cljs-time.core :as t]
|
||||
[clojure.set :as set]
|
||||
@@ -25,7 +23,9 @@
|
||||
[reagent.core :as reagent]
|
||||
[vimsical.re-frame.fx.track :as track]
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[auto-ap.views.pages.ledger.report-table :as rtable]))
|
||||
[auto-ap.views.pages.ledger.report-table :as rtable]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
(defn data-params->query-params [params]
|
||||
(when params
|
||||
@@ -38,11 +38,6 @@
|
||||
:to-numeric-code (:to-numeric-code params)
|
||||
:date-range (:date-range params)}))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
(fn [_]
|
||||
true))
|
||||
|
||||
|
||||
(re-frame/reg-sub
|
||||
::ledger-list-active?
|
||||
@@ -78,9 +73,7 @@
|
||||
:graphql {:token user
|
||||
:query-obj {:venia/queries [[:balance-sheet
|
||||
(-> (:data db)
|
||||
(assoc :client-id (:id client))
|
||||
(update :date (fnil #(date->str % standard) nil))
|
||||
(update :comparison-date (fnil #(date->str % standard) nil)))
|
||||
(assoc :client-id (:id client)))
|
||||
[[:balance-sheet-accounts [:name :amount :account-type :id :numeric-code]]
|
||||
[:comparable-balance-sheet-accounts [:name :amount :account-type :id :numeric-code]]]]]}
|
||||
|
||||
@@ -122,9 +115,7 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
:graphql {:token user
|
||||
:query-obj {:venia/queries [[:balance-sheet-pdf
|
||||
(-> (:data db)
|
||||
(assoc :client-id (:id client))
|
||||
(update :date (fnil #(date->str % standard) nil))
|
||||
(update :comparison-date (fnil #(date->str % standard) nil)))
|
||||
(assoc :client-id (:id client)))
|
||||
[:url :name]]]}
|
||||
|
||||
:owns-state {:single ::page}
|
||||
@@ -139,7 +130,7 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
:from-numeric-code from-numeric-code
|
||||
:to-numeric-code to-numeric-code
|
||||
:date-range {:start "2000-01-01"
|
||||
:end (date->str date-range standard)}}]}))
|
||||
:end date-range}}]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::ledger-params-change
|
||||
@@ -189,53 +180,40 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
:event-fn (fn [params] [::ledger-params-change params])}}))
|
||||
|
||||
|
||||
(def balance-sheet-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::change]
|
||||
:submit-event [::report-requested]
|
||||
:id ::form}))
|
||||
|
||||
(defn report-form []
|
||||
(let [{:keys [form-inline raw-field]} balance-sheet-form
|
||||
{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
(form-inline {}
|
||||
[:div
|
||||
[:div.report-controls
|
||||
[:div.level
|
||||
[:div.level-left
|
||||
[:div.level-item
|
||||
[:div.control
|
||||
[:p.help "Date"]
|
||||
(raw-field
|
||||
[date-picker-friendly {:cljs-date? true
|
||||
:type "date"
|
||||
:field [:date]}])]]
|
||||
[:div.level-item
|
||||
[:div.control
|
||||
[:div.mt-3]
|
||||
[switch-field {:id "include-comparison"
|
||||
:checked (:include-comparison data)
|
||||
:on-change (fn [e]
|
||||
(re-frame/dispatch [::change [:include-comparison] (.-checked (.-target e))]))
|
||||
:label "Include comparison"
|
||||
:type "checkbox"}]]]
|
||||
[:div.level-item
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/builder {:change-event [::change]
|
||||
:submit-event [::report-requested]
|
||||
:id ::form}
|
||||
[:div
|
||||
[:div.report-controls
|
||||
[:div.level
|
||||
[:div.level-left
|
||||
[:div.level-item
|
||||
[:div.control
|
||||
[form-builder/field-v2 {:field :date}
|
||||
"Date"
|
||||
[date-picker {:output :cljs-date}]]]]
|
||||
[:div.level-item
|
||||
[form-builder/field-v2 {:field :include-comparison}
|
||||
[:div.mt-5]
|
||||
[com/switch-input {:id "include-comparison"
|
||||
:label "Include compariison"}]]]
|
||||
[:div.level-item
|
||||
|
||||
(when (boolean (:include-comparison data))
|
||||
[:div.control
|
||||
[:p.help "Comparison Date"]
|
||||
(raw-field
|
||||
[date-picker-friendly {:cljs-date? true
|
||||
:type "date"
|
||||
:field [:comparison-date]}])])]]
|
||||
[:div.level-right
|
||||
[:div.buttons
|
||||
(when (boolean (:include-comparison data))
|
||||
[form-builder/field-v2 {:field :comparison-date}
|
||||
"Comparison Date"
|
||||
[date-picker {:output :cljs-date}]])]]
|
||||
[:div.level-right
|
||||
[:div.buttons
|
||||
|
||||
(when @(re-frame/subscribe [::subs/is-admin?])
|
||||
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
|
||||
[:button.button.is-primary "Run"]]]]]])))
|
||||
(when @(re-frame/subscribe [::subs/is-admin?])
|
||||
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
|
||||
[:button.button.is-primary "Run"]]]]]]]))
|
||||
|
||||
(defn balance-sheet-report [{:keys [args report-data]}]
|
||||
(let [pnl-data (concat (->> (:balance-sheet-accounts report-data)
|
||||
(let [pnl-data (concat (->> (:balance-sheet-accounts report-data)
|
||||
(map (fn [b]
|
||||
(assoc b
|
||||
:period (:date args)
|
||||
|
||||
@@ -1,41 +1,29 @@
|
||||
(ns auto-ap.views.pages.ledger.external-import
|
||||
(:require [auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[goog.string :as gstring]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.utils :refer [by]]
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
|
||||
[auto-ap.views.utils :refer [date->str date-picker bind-field local-now standard ->$ str->date dispatch-event]]
|
||||
[auto-ap.views.components.dropdown :refer [drop-down drop-down-contents]]
|
||||
[cljs-time.core :as t]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]
|
||||
[clojure.string :as str]
|
||||
[auto-ap.status :as status]))
|
||||
(:require
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.dropdown
|
||||
:refer [drop-down drop-down-contents]]
|
||||
[auto-ap.views.components.layouts :refer [side-bar-layout]]
|
||||
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
|
||||
[auto-ap.views.utils :refer [dispatch-event]]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]
|
||||
[reagent.core :as r]))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-sub
|
||||
::loading
|
||||
(fn [db]
|
||||
(-> db ::loading)))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
(fn [db]
|
||||
true))
|
||||
|
||||
(defn line->id [{:keys [source id client-code date vendor-name] :as line}]
|
||||
(defn line->id [{:keys [source id client-code]}]
|
||||
(str client-code "-" source "-" id))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::request
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{{lines :line-items :as d} :data :as g}]
|
||||
(fn [{{lines :line-items} :data}]
|
||||
(into []
|
||||
(for [[external-id lines] (group-by line->id lines)
|
||||
:let [{:keys [source id client-code date vendor-name note cleared-against] :as line} (first lines)]]
|
||||
(for [[_ lines] (group-by line->id lines)
|
||||
:let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
|
||||
{:source source
|
||||
:external-id (line->id line)
|
||||
:client-code client-code
|
||||
@@ -142,6 +130,8 @@
|
||||
[:form.form
|
||||
(if value
|
||||
[:div
|
||||
[:a.button {:on-click #(on-change nil)}
|
||||
"reset"]
|
||||
[:table.table {:style {:width "100%"}}
|
||||
[:thead
|
||||
[:tr
|
||||
@@ -193,68 +183,72 @@
|
||||
(def balance-sheet-content
|
||||
(with-meta
|
||||
(fn []
|
||||
(let [current-client @(re-frame/subscribe [::subs/client])
|
||||
user @(re-frame/subscribe [::subs/user])
|
||||
status @(re-frame/subscribe [::status/single ::import])
|
||||
{:keys [data result active? error id]} @(re-frame/subscribe [::forms/form ::form]) ]
|
||||
[:div
|
||||
[:div.level
|
||||
[:div.level-left
|
||||
[:h1.title "Eternal Import"]]
|
||||
|
||||
[:div.level-right
|
||||
[:button.button.is-primary.is-pulled-right.is-large {:disabled (or (not data)
|
||||
(= :loading (:state status )))
|
||||
:on-click (dispatch-event [::importing])} "Import"]]]
|
||||
[status/status-notification {:statuses [[::status/single ::import]]} ]
|
||||
(when result
|
||||
[:div.notification
|
||||
"Imported with "
|
||||
(count (:errors result)) " errors, "
|
||||
(count (:ignored result)) " ignored, "
|
||||
(count (:success result)) " successful."])
|
||||
(if (= :loading (:state status ))
|
||||
[status/big-loader status]
|
||||
[:div
|
||||
[:div.is-clearfix
|
||||
[:div.is-pulled-right
|
||||
[:label.checkbox
|
||||
[bind-field
|
||||
[:input {:type "checkbox"
|
||||
:event [::forms/change ::form]
|
||||
:subscription data
|
||||
:field [:only-show-errors?]}]]
|
||||
"Only show errors"]]]
|
||||
[:div
|
||||
[bind-field
|
||||
[textarea->table {:type "textarea->table"
|
||||
:field [:line-items]
|
||||
:headings [["Id" :id]
|
||||
["Client" :client-code]
|
||||
["Source" :source]
|
||||
["Vendor" :vendor-name]
|
||||
["Date" :date]
|
||||
["Account" :account-identifier]
|
||||
["Location" :location]
|
||||
["Debit" :debit]
|
||||
["Credit" :credit]
|
||||
["Note" :note]
|
||||
["Cleared against" :cleared-against]]
|
||||
:read-only-headings
|
||||
[["status" :status]]
|
||||
(let [status @(re-frame/subscribe [::status/single ::import])
|
||||
{:keys [data result]} @(re-frame/subscribe [::forms/form ::form]) ]
|
||||
[form-builder/builder {:id ::form
|
||||
:submit-event [::importing]}
|
||||
[:div
|
||||
[:div.level
|
||||
[:div.level-left
|
||||
[:h1.title "Eternal Import"]]
|
||||
|
||||
[:div.level-right
|
||||
[form-builder/submit-button "Import"]]]
|
||||
[status/status-notification {:statuses [[::status/single ::import]]} ]
|
||||
(when result
|
||||
[:div.notification
|
||||
"Imported with "
|
||||
(count (:errors result)) " errors, "
|
||||
(count (:ignored result)) " ignored, "
|
||||
(count (:success result)) " successful."])
|
||||
(if (= :loading (:state status ))
|
||||
[status/big-loader status]
|
||||
[:div
|
||||
[:div.is-clearfix
|
||||
[:div.is-pulled-right
|
||||
[form-builder/raw-field-v2 {:field :only-show-errors?}
|
||||
[com/checkbox {:label "Only show errors"}]]]]
|
||||
[:div
|
||||
[form-builder/raw-field-v2 {:field :line-items}
|
||||
[textarea->table {:headings [["Id" :id]
|
||||
["Client" :client-code]
|
||||
["Source" :source]
|
||||
["Vendor" :vendor-name]
|
||||
["Date" :date]
|
||||
["Account" :account-identifier]
|
||||
["Location" :location]
|
||||
["Debit" :debit]
|
||||
["Credit" :credit]
|
||||
["Note" :note]
|
||||
["Cleared against" :cleared-against]]
|
||||
:read-only-headings
|
||||
[["status" :status]]
|
||||
|
||||
:row-filter
|
||||
(fn [{:keys [status-category]}]
|
||||
(if (:only-show-errors? data)
|
||||
(= :error status-category)
|
||||
true))
|
||||
|
||||
:event [::forms/change ::form]
|
||||
:subscription data}
|
||||
]]]])]))
|
||||
:row-filter
|
||||
(fn [{:keys [status-category]}]
|
||||
(if (:only-show-errors? data)
|
||||
(= :error status-category)
|
||||
true))}]]]])]]))
|
||||
{}))
|
||||
|
||||
(defn external-import-page []
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn [_ _]
|
||||
{:dispatch [::forms/start-form ::form]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::unmounted
|
||||
(fn [_ _]
|
||||
{:dispatch [::forms/form-closing ::form]}))
|
||||
|
||||
(defn external-import-page-internal []
|
||||
[side-bar-layout
|
||||
{:side-bar [ledger-side-bar]
|
||||
:main [balance-sheet-content]}])
|
||||
|
||||
(defn external-import-page []
|
||||
(r/create-class
|
||||
{:display-name "external-import-page"
|
||||
:component-will-unmount #(re-frame/dispatch-sync [::unmounted])
|
||||
:component-did-mount #(re-frame/dispatch [::mounted])
|
||||
:reagent-render external-import-page-internal}))
|
||||
|
||||
@@ -10,16 +10,14 @@
|
||||
:refer [appearing-side-bar side-bar-layout]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.switch-field :refer [switch-field]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
|
||||
[auto-ap.views.pages.ledger.table :as ledger-table]
|
||||
[auto-ap.views.utils
|
||||
:refer [date->str
|
||||
date-picker-friendly
|
||||
date-picker
|
||||
dispatch-event
|
||||
local-today
|
||||
multi-field
|
||||
query-params
|
||||
standard
|
||||
str->date
|
||||
@@ -31,7 +29,9 @@
|
||||
[react-dom :as react-dom]
|
||||
[reagent.core :as reagent]
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[vimsical.re-frame.fx.track :as track]))
|
||||
[vimsical.re-frame.fx.track :as track]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
|
||||
|
||||
@@ -72,8 +72,8 @@
|
||||
(cond-> {:graphql {:token user
|
||||
:owns-state {:single ::page}
|
||||
:query-obj {:venia/queries [[:profit-and-loss
|
||||
{:client-ids (map :id (:clients (:data db)))
|
||||
:periods (mapv encode-period (:periods (:data db)))
|
||||
{:client-ids (map (comp :id :client) (:clients (:data db)))
|
||||
:periods (mapv #(select-keys % #{:start :end} ) (:periods (:data db)))
|
||||
:include-deltas (:include-deltas (:data db))
|
||||
:column-per-location (:column-per-location (:data db))}
|
||||
[[:periods [[:accounts [:name :amount :client_id :account-type :id :count :numeric-code :location]]]]]]]}
|
||||
@@ -81,7 +81,7 @@
|
||||
:set-uri-params {:periods (mapv
|
||||
encode-period
|
||||
(:periods (:data db)))
|
||||
:clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) }
|
||||
:clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
|
||||
:db (-> db
|
||||
(dissoc :report)
|
||||
(update-in [:data :clients] #(into [] (filter seq %))))})))
|
||||
@@ -126,14 +126,14 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
(cond-> {:graphql {:token user
|
||||
:owns-state {:single ::page}
|
||||
:query-obj {:venia/queries [[:profit-and-loss-pdf
|
||||
{:client-ids (map :id (:clients (:data db)))
|
||||
{:client-ids (map (:comp :id :client) (:clients (:data db)))
|
||||
:include-deltas (:include-deltas (:data db))
|
||||
:column-per-location (:column-per-location (:data db))
|
||||
:periods (mapv encode-period (:periods (:data db)))}
|
||||
:periods (mapv #(select-keys % #{:start :end}) (:periods (:data db)))}
|
||||
[:url :name]]]}
|
||||
:on-success [::received-pdf]}
|
||||
:set-uri-params {:periods (mapv encode-period (:periods (:data db)))
|
||||
:clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) }
|
||||
:clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
|
||||
:db (dissoc db :report)})))
|
||||
|
||||
|
||||
@@ -223,10 +223,6 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
(fn [_]
|
||||
true))
|
||||
|
||||
(def pnl-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::change]
|
||||
:submit-event [::report-requested]
|
||||
:id ::form}))
|
||||
|
||||
(defn report-control-detail [{:keys [active box which]} children]
|
||||
(when (and @box
|
||||
@@ -243,191 +239,177 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
[:div.control
|
||||
[:a.button
|
||||
{:class (when (= selected-preset title) "is-active")
|
||||
:on-click (dispatch-event
|
||||
[::change
|
||||
[:periods]
|
||||
periods
|
||||
[:selected-preset] title])}
|
||||
:on-click (fn []
|
||||
(re-frame/dispatch-sync [::change
|
||||
[:periods]
|
||||
periods
|
||||
[:selected-preset] title])
|
||||
(re-frame/dispatch-sync [::change
|
||||
[:show-advanced?]
|
||||
false]))}
|
||||
title]]))
|
||||
|
||||
(defn report-controls [_]
|
||||
(let [!box (reagent/atom nil)
|
||||
(defn report-controls []
|
||||
(let [!box (atom nil)
|
||||
active (reagent/atom nil)]
|
||||
(fn [pnl-form]
|
||||
(let [{:keys [raw-field]} pnl-form
|
||||
{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
(fn []
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [periods selected-preset include-deltas column-per-location]} data]
|
||||
[:div.report-controls
|
||||
[:div.level.mb-2
|
||||
[:div.level-left
|
||||
[:div.level-item
|
||||
[buttons/dropdown {:on-click (fn [] (reset! active :clients))}
|
||||
[:span (str "Companies"
|
||||
(when-let [clients (:clients data)]
|
||||
(str " (" (str/join ", " (map :name clients)) ")")))]]
|
||||
[report-control-detail {:active active :box !box :which :clients}
|
||||
[:div {:style {:width "20em"}}
|
||||
[:h4.subtitle "Companies"]
|
||||
[raw-field
|
||||
[multi-field {:type "multi-field"
|
||||
:field [:clients]
|
||||
:template [[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"}]]}]]
|
||||
]]]
|
||||
[:div.level-item
|
||||
[buttons/dropdown {:on-click (fn [] (reset! active :range))}
|
||||
[:span (str "Range"
|
||||
(when selected-preset
|
||||
(str " (" selected-preset ")")))]]
|
||||
[report-control-detail {:active active :box !box :which :range}
|
||||
[:div
|
||||
[:h4.subtitle "Range"]
|
||||
[:div.field.is-grouped
|
||||
[:div.control
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
(raw-field
|
||||
[date-picker-friendly {:placeholder "End date"
|
||||
:type "date"
|
||||
:cljs-date? true
|
||||
:field [:thirteen-periods-end]}])]
|
||||
[period-preset-button {:title "13 periods"
|
||||
:periods (let [today (or (some-> (:thirteen-periods-end data))
|
||||
(local-today))]
|
||||
(into
|
||||
[{:start (t/plus (t/minus today (t/weeks (* 13 4)))
|
||||
(t/days 1))
|
||||
:end today
|
||||
:title "Total"}]
|
||||
(for [i (range 13)]
|
||||
{:start (t/plus (t/minus today (t/weeks (* (inc i) 4)))
|
||||
[form-builder/builder {:can-submit [::can-submit]
|
||||
:change-event [::change]
|
||||
:submit-event [::report-requested]
|
||||
:id ::form}
|
||||
[:div.report-controls
|
||||
[:div.level.mb-2
|
||||
[:div.level-left
|
||||
[:div.level-item
|
||||
[buttons/dropdown {:on-click (fn [] (reset! active :clients))}
|
||||
[:span (str "Companies"
|
||||
(when-let [clients (:clients data)]
|
||||
(str " (" (str/join ", " (map (comp :name :client) clients)) ")")))]]
|
||||
[report-control-detail {:active active :box !box :which :clients}
|
||||
[:div {:style {:width "20em"}}
|
||||
[:h4.subtitle "Companies"]
|
||||
[form-builder/raw-field-v2 {:field :clients}
|
||||
[com/multi-field-v2 {:new-text "Add another company"
|
||||
:template [[form-builder/raw-field-v2 {:field :client}
|
||||
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:style {:width "18em"}
|
||||
:entity->text :name}]]]
|
||||
:key-fn :id}]]]]]
|
||||
[:div.level-item
|
||||
[buttons/dropdown {:on-click (fn [] (reset! active :range))}
|
||||
[:span (str "Range"
|
||||
(when selected-preset
|
||||
(str " (" selected-preset ")")))]]
|
||||
[report-control-detail {:active active :box !box :which :range}
|
||||
[:div
|
||||
[:h4.subtitle "Range"]
|
||||
[:div.field.is-grouped
|
||||
[:div.control
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field :thirteen-periods-end}
|
||||
[date-picker {:placeholder "End date"
|
||||
:output :cljs-date}]]]
|
||||
[period-preset-button {:title "13 periods"
|
||||
:periods (let [today (or (some-> (:thirteen-periods-end data))
|
||||
(local-today))]
|
||||
(into
|
||||
[{:start (t/plus (t/minus today (t/weeks (* 13 4)))
|
||||
(t/days 1))
|
||||
:end (t/minus today (t/weeks (* i 4)))})))}]]]
|
||||
:end today
|
||||
:title "Total"}]
|
||||
(for [i (range 13)]
|
||||
{:start (t/plus (t/minus today (t/weeks (* (inc i) 4)))
|
||||
(t/days 1))
|
||||
:end (t/minus today (t/weeks (* i 4)))})))}]]]
|
||||
|
||||
[:div.control
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
(raw-field
|
||||
[date-picker-friendly {:placeholder "End date"
|
||||
:cljs-date? true
|
||||
:type "date"
|
||||
:field [:twelve-periods-end]}])]
|
||||
[period-preset-button {:title "12 months"
|
||||
:periods (let [end-date (or (some-> (:twelve-periods-end data))
|
||||
(local-today))
|
||||
this-month (t/local-date (t/year end-date)
|
||||
(t/month end-date)
|
||||
1)]
|
||||
(into
|
||||
[{:start (t/minus this-month (t/months 11))
|
||||
:end (t/minus (t/plus this-month (t/months 1))
|
||||
(t/days 1))
|
||||
:title "Total"}]
|
||||
(for [i (range 12)]
|
||||
{:start (t/minus this-month (t/months (- 11 i)))
|
||||
:end (t/minus (t/minus this-month (t/months (- 10 i)))
|
||||
(t/days 1))})))}]]]
|
||||
[:div.control
|
||||
[:div.field.has-addons
|
||||
[:div.control
|
||||
[form-builder/raw-field-v2 {:field :twelve-periods-end}
|
||||
[date-picker {:placeholder "End date"
|
||||
:output :cljs-date}]]]
|
||||
[period-preset-button {:title "12 months"
|
||||
:periods (let [end-date (or (some-> (:twelve-periods-end data))
|
||||
(local-today))
|
||||
this-month (t/local-date (t/year end-date)
|
||||
(t/month end-date)
|
||||
1)]
|
||||
(into
|
||||
[{:start (t/minus this-month (t/months 11))
|
||||
:end (t/minus (t/plus this-month (t/months 1))
|
||||
(t/days 1))
|
||||
:title "Total"}]
|
||||
(for [i (range 12)]
|
||||
{:start (t/minus this-month (t/months (- 11 i)))
|
||||
:end (t/minus (t/minus this-month (t/months (- 10 i)))
|
||||
(t/days 1))})))}]]]
|
||||
|
||||
[period-preset-button {:periods (let [last-sunday (loop [current (local-today)]
|
||||
(if (= 7 (t/day-of-week current))
|
||||
current
|
||||
(recur (t/minus current (t/period :days 1)))))]
|
||||
(and-last-year {:start (t/minus last-sunday (t/period :days 6))
|
||||
:end last-sunday}))
|
||||
:title "Last week"}]
|
||||
[period-preset-button {:periods (let [last-sunday (loop [current (local-today)]
|
||||
(if (= 7 (t/day-of-week current))
|
||||
current
|
||||
(recur (t/minus current (t/period :days 1)))))]
|
||||
(and-last-year {:start (t/minus last-sunday (t/period :days 6))
|
||||
:end last-sunday}))
|
||||
:title "Last week"}]
|
||||
|
||||
[period-preset-button {:periods (and-last-year {:start (loop [current (local-today)]
|
||||
(if (= 1 (t/day-of-week current))
|
||||
current
|
||||
(recur (t/minus current (t/period :days 1)))))
|
||||
:end (local-today)})
|
||||
:title "Week to date"}]
|
||||
[period-preset-button {:periods (and-last-year {:start (loop [current (local-today)]
|
||||
(if (= 1 (t/day-of-week current))
|
||||
current
|
||||
(recur (t/minus current (t/period :days 1)))))
|
||||
:end (local-today)})
|
||||
:title "Week to date"}]
|
||||
|
||||
[period-preset-button {:periods (and-last-year {:start (t/minus (t/local-date (t/year (local-today))
|
||||
(t/month (local-today))
|
||||
1)
|
||||
(t/period :months 1))
|
||||
:end (t/minus (t/local-date (t/year (local-today))
|
||||
(t/month (local-today))
|
||||
1)
|
||||
(t/period :days 1))})
|
||||
:title "Last month"}]
|
||||
[period-preset-button {:periods (and-last-year {:start (t/minus (t/local-date (t/year (local-today))
|
||||
(t/month (local-today))
|
||||
1)
|
||||
(t/period :months 1))
|
||||
:end (t/minus (t/local-date (t/year (local-today))
|
||||
(t/month (local-today))
|
||||
1)
|
||||
(t/period :days 1))})
|
||||
:title "Last month"}]
|
||||
|
||||
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today))
|
||||
(t/month (local-today))
|
||||
1)
|
||||
:end (local-today)})
|
||||
:title "Month to date"}]
|
||||
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today))
|
||||
(t/month (local-today))
|
||||
1)
|
||||
:end (local-today)})
|
||||
:title "Month to date"}]
|
||||
|
||||
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today)) 1 1)
|
||||
:end
|
||||
(local-today)})
|
||||
:title "Year to date"}]
|
||||
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today)) 1 1)
|
||||
:end
|
||||
(local-today)})
|
||||
:title "Year to date"}]
|
||||
|
||||
[period-preset-button {:periods [{:start (t/local-date (dec (t/year (local-today))) 1 1)
|
||||
:end (t/local-date (dec (t/year (local-today))) 12 31)}]
|
||||
:title "Last calendar year"}]
|
||||
[period-preset-button {:periods [{:start (t/local-date (dec (t/year (local-today))) 1 1)
|
||||
:end (t/local-date (dec (t/year (local-today))) 12 31)}]
|
||||
:title "Last calendar year"}]
|
||||
|
||||
[period-preset-button {:periods (and-last-year {:start (t/plus (t/minus (local-today) (t/period :years 1))
|
||||
(t/period :days 1))
|
||||
:end (local-today)})
|
||||
:title "Full year"}]]
|
||||
[:div
|
||||
[:div.field
|
||||
[:label.checkbox
|
||||
(raw-field
|
||||
[:input {:type "checkbox"
|
||||
:field [:show-advanced?]}])
|
||||
" Show Advanced"]]]
|
||||
(when (:show-advanced? data)
|
||||
(doall
|
||||
(for [[_ i] (map vector periods (range))]
|
||||
^{:key i}
|
||||
[:div.field.is-grouped
|
||||
[:div.control
|
||||
[:p.help "From"]
|
||||
(raw-field
|
||||
[date-picker-friendly {:type "date"
|
||||
:cljs-date? true
|
||||
:field [:periods i :start]}])]
|
||||
[period-preset-button {:periods (and-last-year {:start (t/plus (t/minus (local-today) (t/period :years 1))
|
||||
(t/period :days 1))
|
||||
:end (local-today)})
|
||||
:title "Full year"}]]
|
||||
[:div
|
||||
[form-builder/raw-field-v2 {:field :show-advanced?}
|
||||
[com/checkbox {:label "Show Advanced"}]]]
|
||||
(when (:show-advanced? data)
|
||||
[form-builder/raw-field-v2 {:field :periods}
|
||||
[com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :start}
|
||||
[date-picker {:output :cljs-date}]]
|
||||
[form-builder/raw-field-v2 {:field :end}
|
||||
[date-picker {:output :cljs-date}]]]}]])]]]
|
||||
|
||||
[:div.control
|
||||
[:p.help "To"]
|
||||
(raw-field
|
||||
[date-picker-friendly {:type "date"
|
||||
:cljs-date? true
|
||||
:field [:periods i :end]}])]])))]]]
|
||||
[:div.level-item
|
||||
[:div
|
||||
[switch-field {:id "include-deltas"
|
||||
:checked (boolean include-deltas)
|
||||
:on-change (fn [e]
|
||||
(re-frame/dispatch [::change
|
||||
[:include-deltas] (.-checked (.-target e))]))
|
||||
:label "Include deltas"
|
||||
:type "checkbox"}]]]
|
||||
[:div.level-item
|
||||
[:div
|
||||
[switch-field {:id "column-per-location"
|
||||
:checked (boolean column-per-location)
|
||||
:on-change (fn [e]
|
||||
(re-frame/dispatch [::change
|
||||
[:column-per-location] (.-checked (.-target e))]))
|
||||
:label "Column per location"
|
||||
:type "checkbox"}]]]]
|
||||
[:div.level-right
|
||||
[:div.buttons
|
||||
|
||||
[:div.level-item
|
||||
[:div
|
||||
[switch-field {:id "include-deltas"
|
||||
:checked (boolean include-deltas)
|
||||
:on-change (fn [e]
|
||||
(re-frame/dispatch [::change
|
||||
[:include-deltas] (.-checked (.-target e))]))
|
||||
:label "Include deltas"
|
||||
:type "checkbox"}]]]
|
||||
[:div.level-item
|
||||
[:div
|
||||
[switch-field {:id "column-per-location"
|
||||
:checked (boolean column-per-location)
|
||||
:on-change (fn [e]
|
||||
(re-frame/dispatch [::change
|
||||
[:column-per-location] (.-checked (.-target e))]))
|
||||
:label "Column per location"
|
||||
:type "checkbox"}]]]]
|
||||
[:div.level-right
|
||||
[:div.buttons
|
||||
(when @(re-frame/subscribe [::subs/is-admin?])
|
||||
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
|
||||
[:button.button.is-primary "Run"]]
|
||||
|
||||
(when @(re-frame/subscribe [::subs/is-admin?])
|
||||
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
|
||||
[:button.button.is-primary "Run"]]
|
||||
|
||||
]]
|
||||
[:div.report-control-detail {:ref (fn [el]
|
||||
(when-not @!box
|
||||
(reset! !box el)))}]]))))
|
||||
]]
|
||||
[:div.report-control-detail {:ref (fn [el]
|
||||
(when (not= @!box el)
|
||||
(reset! !box el)))}]]]))))
|
||||
|
||||
|
||||
|
||||
@@ -450,7 +432,7 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
report (l-reports/summarize-pnl pnl-data)
|
||||
table (rtable/concat-tables (concat (:summaries report) (:details report)))]
|
||||
[:div
|
||||
[:h1.title "Profit and Loss - " (str/join ", " (map :name (:clients args)))]
|
||||
[:h1.title "Profit and Loss - " (str/join ", " (map (comp :name :client) (:clients args)))]
|
||||
(when (:warning report)
|
||||
[:div.notification.is-warning.is-light
|
||||
(:warning report)])
|
||||
@@ -466,13 +448,11 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
|
||||
(defn profit-and-loss-content []
|
||||
(let [status @(re-frame/subscribe [::status/single ::page])
|
||||
{:keys [data report]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline]} pnl-form]
|
||||
{:keys [data report]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[:div
|
||||
(form-inline {}
|
||||
[:div
|
||||
[status/status-notification {:statuses [[::status/single ::page]]}]
|
||||
[report-controls pnl-form]])
|
||||
[:div
|
||||
[status/status-notification {:statuses [[::status/single ::page]]}]
|
||||
[report-controls]]
|
||||
[status/big-loader status]
|
||||
(when (and (not= :loading (:state status))
|
||||
report)
|
||||
@@ -493,9 +473,11 @@ NOTE: Please review the transactions we may have question for you here: https://
|
||||
(mapv (fn [period]
|
||||
{:start (str->date (:start period) standard)
|
||||
:end (str->date (:end period) standard)})))
|
||||
:clients (or (:clients qp)
|
||||
[(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]))])
|
||||
:include-deltas false})
|
||||
:clients (mapv (fn [c] {:client c :id (random-uuid)})
|
||||
(or (:clients qp)
|
||||
[(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]) )]))
|
||||
:include-deltas false
|
||||
:show-advanced? false})
|
||||
::track/register {:id ::ledger-params
|
||||
:subscription [::data-page/params ::ledger]
|
||||
:event-fn (fn [params] [::ledger-params-change params])}})))
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
[(keyword (str "border-" (name b))) "1px solid black"])
|
||||
)
|
||||
(into s))))
|
||||
(:colspan c) (assoc :colspan (:colspan c))
|
||||
(:colspan c) (assoc :col-span (:colspan c))
|
||||
(:align c) (assoc :align (:align c))
|
||||
(= :dollar (:format c)) (assoc :align :right)
|
||||
(= :percent (:format c)) (assoc :align :right)
|
||||
|
||||
@@ -17,19 +17,22 @@
|
||||
[vimsical.re-frame.fx.track :as track]))
|
||||
|
||||
(defn data-params->query-params [params]
|
||||
{:start (:start params 0)
|
||||
:per-page (:per-page params)
|
||||
:sort (:sort params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:vendor-id (:id (:vendor params))
|
||||
:payment-type (:payment-type params)
|
||||
:status (:status params)
|
||||
:exact-match-id (some-> (:exact-match-id params) str)
|
||||
:date-range (:date-range params)
|
||||
:amount-gte (:amount-gte (:amount-range params))
|
||||
:amount-lte (:amount-lte (:amount-range params))
|
||||
:check-number-like (str (:check-number-like params))
|
||||
:invoice-number (:invoice-number params)})
|
||||
(if (:exact-match-id params)
|
||||
{:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:exact-match-id (some-> (:exact-match-id params) str)}
|
||||
{:start (:start params 0)
|
||||
:per-page (:per-page params)
|
||||
:sort (:sort params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:vendor-id (:id (:vendor params))
|
||||
:payment-type (:payment-type params)
|
||||
:status (:status params)
|
||||
|
||||
:date-range (:date-range params)
|
||||
:amount-gte (:amount-gte (:amount-range params))
|
||||
:amount-lte (:amount-lte (:amount-range params))
|
||||
:check-number-like (str (:check-number-like params))
|
||||
:invoice-number (:invoice-number params)}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::params-change
|
||||
|
||||
@@ -15,18 +15,21 @@
|
||||
(re-frame/reg-event-fx
|
||||
::params-change
|
||||
[with-user]
|
||||
(fn [{:keys [user db ]}[_ params]]
|
||||
(fn [{:keys [user]}[_ params]]
|
||||
{:graphql {:token user
|
||||
:owns-state {:single [::data-page/page ::page]}
|
||||
:query-obj {:venia/queries [[:expected_deposit_page
|
||||
{:start (:start params 0)
|
||||
:sort (:sort params)
|
||||
:per-page (:per-page params)
|
||||
:exact-match-id (some-> (:exact-match-id params) str)
|
||||
:total-gte (:amount-gte (:total-range params))
|
||||
:total-lte (:amount-lte (:total-range params))
|
||||
:date-range (:date-range params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))}
|
||||
(if (:exact-match-id params)
|
||||
{:exact-match-id (some-> (:exact-match-id params) str)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))}
|
||||
{:start (:start params 0)
|
||||
:sort (:sort params)
|
||||
:per-page (:per-page params)
|
||||
:exact-match-id (some-> (:exact-match-id params) str)
|
||||
:total-gte (:amount-gte (:total-range params))
|
||||
:total-lte (:amount-lte (:total-range params))
|
||||
:date-range (:date-range params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))})
|
||||
[[:expected-deposits [:id :total :fee :location :date :status
|
||||
[:totals [:date :count :amount]]
|
||||
[:transaction [:id :date]]
|
||||
@@ -42,13 +45,13 @@
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::unmounted
|
||||
(fn [{:keys [db]} _]
|
||||
(fn [_ _]
|
||||
{:dispatch [::data-page/dispose ::page]
|
||||
::track/dispose {:id ::params}}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::mounted
|
||||
(fn [{:keys [db]} _]
|
||||
(fn [_ _]
|
||||
{::track/register {:id ::params
|
||||
:subscription [::data-page/params ::page]
|
||||
:event-fn (fn [params]
|
||||
|
||||
@@ -1,25 +1,15 @@
|
||||
(ns auto-ap.views.pages.pos.form
|
||||
(:require
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.utils :refer [dollars=]]
|
||||
[auto-ap.views.components.dropdown :refer [drop-down]]
|
||||
[auto-ap.views.components.expense-accounts-field :as expense-accounts-field :refer [expense-accounts-field recalculate-amounts]]
|
||||
[auto-ap.views.components.layouts :as layouts]
|
||||
[auto-ap.views.components.money-field :refer [money-field]]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.utils :refer [date->str date-picker dispatch-event standard with-user]]
|
||||
[cljs-time.core :as c]
|
||||
[clojure.spec.alpha :as s]
|
||||
[clojure.string :as str]
|
||||
[re-frame.core :as re-frame]))
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{:keys [data status]} _]
|
||||
false))
|
||||
(:require
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.layouts :as layouts]
|
||||
[auto-ap.views.components.money-field :refer [money-field]]
|
||||
[auto-ap.views.utils
|
||||
:refer [date->str date-picker dispatch-event standard]]
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
|
||||
(re-frame/reg-event-db
|
||||
::editing
|
||||
@@ -27,70 +17,56 @@
|
||||
(let [which (update which :date #(date->str % standard))]
|
||||
(forms/start-form db ::form which))))
|
||||
|
||||
(def sales-order-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::forms/changed]
|
||||
:submit-event [::saving ]
|
||||
:id ::form}))
|
||||
|
||||
(defn form [{:keys [can-change-amount?] :as params}]
|
||||
(defn form []
|
||||
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
|
||||
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field raw-field error-notification submit-button ]} sales-order-form]
|
||||
(with-meta
|
||||
(form-inline (assoc params :title "Sales order")
|
||||
[:<>
|
||||
(when-not @(re-frame/subscribe [::subs/client])
|
||||
(field [:span "Client"
|
||||
[:span.has-text-danger " *"]]
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:entity->text :name
|
||||
:type "typeahead-v3"
|
||||
:field [:client]
|
||||
:disabled true}]))
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/builder {:submit-event [::saving ]
|
||||
:id ::form}
|
||||
[form-builder/section {:title "Sales Order"}
|
||||
(when-not @(re-frame/subscribe [::subs/client])
|
||||
[form-builder/field-v2 {:field :client}
|
||||
"Client"
|
||||
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
|
||||
:entity->text :name
|
||||
:disabled true}]])
|
||||
|
||||
|
||||
(field "Date"
|
||||
[date-picker {:class-name "input"
|
||||
:class "input"
|
||||
:type "date"
|
||||
:disabled true
|
||||
:field [:date]}])
|
||||
(field "Total"
|
||||
[money-field {:type "money"
|
||||
:field [:total]
|
||||
:disabled true}])
|
||||
(field "Tax"
|
||||
[money-field {:type "money"
|
||||
:field [:tax]
|
||||
:disabled true}])
|
||||
(field "Discount"
|
||||
[money-field {:type "money"
|
||||
:field [:discount]
|
||||
:disabled true}])
|
||||
[form-builder/field-v2 {:field :date}
|
||||
"Date"
|
||||
[date-picker {:output :cljs-date
|
||||
:disabled true}]]
|
||||
[form-builder/field-v2 {:field :total}
|
||||
"Total"
|
||||
[money-field {:disabled true}]]
|
||||
[form-builder/field-v2 {:field :tax}
|
||||
"Tax"
|
||||
[money-field {:disabled true}]
|
||||
[form-builder/field-v2 {:field :discount}
|
||||
"Discount"
|
||||
[money-field {:disabled true}]]]
|
||||
|
||||
(field "Returns"
|
||||
[money-field {:type "money"
|
||||
:field [:returns]
|
||||
:disabled true}])
|
||||
[form-builder/field-v2 {:field :returns}
|
||||
"Returns"
|
||||
[money-field {:disabled true}]]
|
||||
|
||||
(field "Service Charge"
|
||||
[money-field {:type "money"
|
||||
:field [:service-charge]
|
||||
:disabled true}])
|
||||
[form-builder/field-v2 {:field :service-charge}
|
||||
"Service Charge"
|
||||
[money-field {:disabled true}]]
|
||||
|
||||
(field "Tip"
|
||||
[money-field {:type "money"
|
||||
:field [:tip]
|
||||
:disabled true}])
|
||||
[form-builder/field-v2 {:field :tip}
|
||||
"Tip"
|
||||
[money-field {:disabled true}]]
|
||||
|
||||
[:h1.subtitle.is-4 "Charges"]
|
||||
[:ul
|
||||
(for [charge (:charges data)]
|
||||
[:li (:type-name charge) ": " (:total charge)])]
|
||||
[form-builder/section {:title "Charges"}
|
||||
[:ul
|
||||
(for [charge (:charges data)]
|
||||
^{:key (:id charge)}
|
||||
[:li (:type-name charge) ": " (:total charge)])]]
|
||||
|
||||
[:h1.subtitle.is-4 "Line Items"]
|
||||
[:ul
|
||||
(for [line-item (:line-items data)]
|
||||
[:li (:item-name line-item) ": " (:total line-item) [:span.tag (:category line-item)]])]])
|
||||
{:key (:id data)}))])
|
||||
[form-builder/section {:title "Line Items"}
|
||||
[:ul
|
||||
(for [line-item (:line-items data)]
|
||||
^{:key (:item-name line-item)}
|
||||
[:li (:item-name line-item) ": " (:total line-item) [:span.tag (:category line-item)]])]]]])])
|
||||
|
||||
|
||||
@@ -112,7 +112,6 @@
|
||||
{:id ::manual-import
|
||||
:events #{::manual/import-completed}
|
||||
:event-fn (fn [[_ result]]
|
||||
(println result)
|
||||
[::status/info ::manual-import
|
||||
(str "Successfully "
|
||||
(str/join ", "
|
||||
|
||||
@@ -3,13 +3,10 @@
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.button-radio :refer [button-radio]]
|
||||
[auto-ap.views.components.expense-accounts-field
|
||||
:as expense-accounts-field
|
||||
:refer [expense-accounts-field]]
|
||||
:refer [expense-accounts-field-v2]]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.pages.transactions.common
|
||||
:refer [data-params->query-params]]
|
||||
@@ -19,7 +16,11 @@
|
||||
[reagent.core :as r]
|
||||
[vimsical.re-frame.fx.track :as track]
|
||||
[auto-ap.events :as events]
|
||||
[vimsical.re-frame.cofx.inject :as inject]))
|
||||
[vimsical.re-frame.cofx.inject :as inject]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[malli.core :as m]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.views.components :as com]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
@@ -105,45 +106,41 @@
|
||||
(fn []
|
||||
{::track/dispose {:id ::vendor-change}}))
|
||||
|
||||
(def code-form (forms/vertical-form {:submit-event [::code-selected]
|
||||
:change-event [::changed]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
|
||||
(def bulk-update-schema
|
||||
(m/schema
|
||||
[:map
|
||||
[:vendor schema/reference]]))
|
||||
(defn form-content [_]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline field]} code-form]
|
||||
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
|
||||
[form-builder/builder {:submit-event [::code-selected]
|
||||
:change-event [::changed]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}
|
||||
|
||||
(form-inline {}
|
||||
[:<>
|
||||
(field "Vendor"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:auto-focus true
|
||||
:field [:vendor]}])
|
||||
|
||||
[form-builder/field-v2 {:field :vendor}
|
||||
"Vendor"
|
||||
[com/search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:auto-focus true}]]
|
||||
|
||||
(field "Approval Status"
|
||||
[button-radio
|
||||
{:type "button-radio"
|
||||
:field [:transaction-approval-status]
|
||||
:options [[:unapproved "Unapproved"]
|
||||
[:requires-feedback "Client Review"]
|
||||
[:approved "Approved"]
|
||||
[:excluded "Excluded from Ledger"]]}])
|
||||
[form-builder/field-v2 {:field [:transaction-approval-status]}
|
||||
"Approval Status"
|
||||
[com/button-radio-input
|
||||
{:options [[:unapproved "Unapproved"]
|
||||
[:requires-feedback "Client Review"]
|
||||
[:approved "Approved"]
|
||||
[:excluded "Excluded from Ledger"]]}]]
|
||||
|
||||
(with-meta
|
||||
(field nil
|
||||
[expense-accounts-field {:type "expense-accounts"
|
||||
:descriptor "account asssignment"
|
||||
:percentage-only? true
|
||||
:client (:client data)
|
||||
:locations (into ["Shared"] @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]))
|
||||
:max 100
|
||||
:field [:accounts]}])
|
||||
{:key (some-> data :vendor :id str)})
|
||||
])))
|
||||
[form-builder/raw-field-v2 {:field :accounts}
|
||||
[expense-accounts-field-v2 {:descriptor "account asssignment"
|
||||
:percentage-only? true
|
||||
:client (:client data)
|
||||
:locations (into ["Shared"] @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]))
|
||||
:max 100}]]]))
|
||||
(defn form [_]
|
||||
(r/create-class
|
||||
{:display-name "transaction-bulk-update-form"
|
||||
|
||||
@@ -23,25 +23,28 @@
|
||||
[:bank-account [:name :yodlee-account-id :current-balance]]])
|
||||
|
||||
(defn data-params->query-params [params]
|
||||
{:start (:start params 0)
|
||||
:per-page (:per-page params)
|
||||
:sort (:sort params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:vendor-id (:id (:vendor params))
|
||||
:date-range (:date-range params)
|
||||
:account-id (:id (:account params))
|
||||
:bank-account-id (:id (:bank-account params))
|
||||
:amount-gte (:amount-gte (:amount-range params))
|
||||
:exact-match-id (some-> (:exact-match-id params) str)
|
||||
:unresolved (:unresolved params)
|
||||
:potential-duplicates (:potential-duplicates params)
|
||||
:location (:location params)
|
||||
:import-batch-id (some-> (:import-batch-id params) str)
|
||||
:amount-lte (:amount-lte (:amount-range params))
|
||||
:description (:description params)
|
||||
:approval-status (condp = @(re-frame/subscribe [::subs/active-page])
|
||||
:transactions nil
|
||||
:unapproved-transactions :unapproved
|
||||
:requires-feedback-transactions :requires-feedback
|
||||
:excluded-transactions :excluded
|
||||
:approved-transactions :approved)})
|
||||
(if (:exact-match-id params)
|
||||
{:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:exact-match-id (some-> (:exact-match-id params) str)}
|
||||
{:start (:start params 0)
|
||||
:per-page (:per-page params)
|
||||
:sort (:sort params)
|
||||
:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:vendor-id (:id (:vendor params))
|
||||
:date-range (:date-range params)
|
||||
:account-id (:id (:account params))
|
||||
:bank-account-id (:id (:bank-account params))
|
||||
:amount-gte (:amount-gte (:amount-range params))
|
||||
:exact-match-id (some-> (:exact-match-id params) str)
|
||||
:unresolved (:unresolved params)
|
||||
:potential-duplicates (:potential-duplicates params)
|
||||
:location (:location params)
|
||||
:import-batch-id (some-> (:import-batch-id params) str)
|
||||
:amount-lte (:amount-lte (:amount-range params))
|
||||
:description (:description params)
|
||||
:approval-status (condp = @(re-frame/subscribe [::subs/active-page])
|
||||
:transactions nil
|
||||
:unapproved-transactions :unapproved
|
||||
:requires-feedback-transactions :requires-feedback
|
||||
:excluded-transactions :excluded
|
||||
:approved-transactions :approved)}))
|
||||
|
||||
@@ -1,25 +1,31 @@
|
||||
(ns auto-ap.views.pages.transactions.form
|
||||
(:require
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.forms :as forms]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.button-radio :refer [button-radio]]
|
||||
[auto-ap.views.components :as com]
|
||||
[auto-ap.views.components.expense-accounts-field
|
||||
:as expense-accounts-field
|
||||
:refer [expense-accounts-field]]
|
||||
:refer [expense-accounts-field-v2]]
|
||||
[auto-ap.views.components.layouts :as layouts]
|
||||
[auto-ap.views.components.typeahead :refer [typeahead-v3]]
|
||||
[auto-ap.views.components.typeahead.vendor
|
||||
:refer [search-backed-typeahead]]
|
||||
[auto-ap.views.pages.transactions.common :refer [transaction-read]]
|
||||
[auto-ap.views.utils
|
||||
:refer [->$ date->str dispatch-event pretty with-user]]
|
||||
:refer [->$ date->str date-picker dispatch-event pretty with-user]]
|
||||
[clojure.string :as str]
|
||||
[malli.core :as m]
|
||||
[re-frame.core :as re-frame]
|
||||
[react :as react]
|
||||
[reagent.core :as r]
|
||||
[vimsical.re-frame.fx.track :as track]
|
||||
[auto-ap.events :as events]))
|
||||
[vimsical.re-frame.fx.track :as track]))
|
||||
|
||||
(def schema
|
||||
(m/schema [:map
|
||||
[:vendor schema/reference]
|
||||
[:accounts expense-accounts-field/schema]
|
||||
[:approval-status schema/approval-status]]))
|
||||
|
||||
;; SUBS
|
||||
(re-frame/reg-sub
|
||||
@@ -44,13 +50,6 @@
|
||||
accounts)}}
|
||||
transaction-read]}]}))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
:<- [::forms/form ::form]
|
||||
(fn [{:keys [status]} _]
|
||||
(not= :loading status)))
|
||||
|
||||
|
||||
;; EVENTS
|
||||
|
||||
(re-frame/reg-event-db
|
||||
@@ -211,10 +210,6 @@
|
||||
|
||||
;; VIEWS
|
||||
|
||||
(def transaction-form (forms/vertical-form {:can-submit [::can-submit]
|
||||
:change-event [::changed]
|
||||
:submit-event [::saving ]
|
||||
:id ::form}))
|
||||
|
||||
(defn potential-transaction-rule-matches-box [{:keys [potential-transaction-rule-matches]}]
|
||||
(let [states @(re-frame/subscribe [::status/multi ::matching])]
|
||||
@@ -303,7 +298,6 @@
|
||||
|
||||
(defonce ^js/React.Context current-tab-context ( react/createContext "default"))
|
||||
(def ^js/React.Provider CurrentTabProvider (. current-tab-context -Provider))
|
||||
#_(println "Provider is" Provider)
|
||||
(def ^js/React.Consumer CurrentTabConsumer (. current-tab-context -Consumer))
|
||||
|
||||
(defn tabs [props & _]
|
||||
@@ -340,122 +334,106 @@
|
||||
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form])}
|
||||
(let [{:keys [data] } @(re-frame/subscribe [::forms/form ::form])
|
||||
locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))])
|
||||
{:keys [form-inline field error-notification submit-button ]} transaction-form
|
||||
is-admin? @(re-frame/subscribe [::subs/is-admin?])
|
||||
is-power-user? @(re-frame/subscribe [::subs/is-power-user?])
|
||||
|
||||
should-disable-for-client? (and (not (or is-admin? is-power-user?))
|
||||
(not= :requires-feedback (:original-status data)))
|
||||
is-already-matched? (:payment data)]
|
||||
(with-meta
|
||||
(form-inline {:title "Transaction"}
|
||||
[:<>
|
||||
[form-builder/builder {:change-event [::changed]
|
||||
:submit-event [::saving ]
|
||||
:id ::form
|
||||
:schema schema}
|
||||
[form-builder/section {:title "Transaction"}
|
||||
[:<>
|
||||
|
||||
(when (and @(re-frame/subscribe [::subs/is-admin?])
|
||||
(get-in data [:yodlee-merchant]))
|
||||
[:div.control
|
||||
[:p.help "Merchant"]
|
||||
[:input.input {:type "text"
|
||||
:disabled true
|
||||
:value (str (get-in data [:yodlee-merchant :name])
|
||||
" - "
|
||||
(get-in data [:yodlee-merchant :yodlee-id]))}]])
|
||||
(when is-admin?
|
||||
|
||||
(when is-admin?
|
||||
(field "Matched Rule"
|
||||
[:input.input {:type "text"
|
||||
:field [:matched-rule :note]
|
||||
:disabled "disabled"}]))
|
||||
(field "Amount"
|
||||
[:input.input {:type "text"
|
||||
:field [:amount]
|
||||
:disabled "disabled"}])
|
||||
(field "Description"
|
||||
[:input.input {:type "text"
|
||||
:field [:description-original]
|
||||
:disabled "disabled"}])
|
||||
[form-builder/field-v2 {:field [:matched-rule :note] }
|
||||
"Matched Rule"
|
||||
[:input.input {:type "text" :disabled "disabled"}]])
|
||||
[form-builder/field-v2 {:field :amount}
|
||||
"Amount"
|
||||
[:input.input {:type "text"
|
||||
:disabled "disabled"}]]
|
||||
[form-builder/field-v2 {:field [:description-original]}
|
||||
"Description"
|
||||
[:input.input {:type "text"
|
||||
:disabled "disabled"}]]
|
||||
|
||||
(field "Date"
|
||||
[:input.input {:type "text"
|
||||
:field [:date]
|
||||
:disabled "disabled"}])
|
||||
[form-builder/field-v2 {:field [:date]}
|
||||
"Date"
|
||||
[date-picker {
|
||||
:disabled "disabled"}]]
|
||||
|
||||
|
||||
(when (and (:payment data)
|
||||
(or is-admin? is-power-user?))
|
||||
[:p.notification.is-info.is-light>div.level>div.level-left
|
||||
[:div.level-item "This transaction is linked to a payment "]
|
||||
[:div.level-item [:button.button.is-warning {:on-click (dispatch-event [::unlink])} "Unlink"]]])
|
||||
[tabs {:default-tab :details}
|
||||
(when
|
||||
(and (seq (:potential-transaction-rule-matches data))
|
||||
(not (:matched-rule data))
|
||||
is-admin?)
|
||||
[tab {:title "Transaction Rule" :key :transaction-rule}
|
||||
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]])
|
||||
(when
|
||||
(and (seq (:potential-autopay-invoices-matches data))
|
||||
(not is-already-matched?)
|
||||
(or is-admin? is-power-user?))
|
||||
[tab {:title "Autopay Invoices" :key :autopay-invoices}
|
||||
[potential-autopay-invoices-matches-box {:potential-autopay-invoices-matches (:potential-autopay-invoices-matches data)}]])
|
||||
(when (and (:payment data)
|
||||
(or is-admin? is-power-user?))
|
||||
[:p.notification.is-info.is-light>div.level>div.level-left
|
||||
[:div.level-item "This transaction is linked to a payment "]
|
||||
[:div.level-item [:button.button.is-warning {:on-click (dispatch-event [::unlink])} "Unlink"]]])
|
||||
[tabs {:default-tab :details}
|
||||
(when
|
||||
(and (seq (:potential-transaction-rule-matches data))
|
||||
(not (:matched-rule data))
|
||||
is-admin?)
|
||||
[tab {:title "Transaction Rule" :key :transaction-rule}
|
||||
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]])
|
||||
(when
|
||||
(and (seq (:potential-autopay-invoices-matches data))
|
||||
(not is-already-matched?)
|
||||
(or is-admin? is-power-user?))
|
||||
[tab {:title "Autopay Invoices" :key :autopay-invoices}
|
||||
[potential-autopay-invoices-matches-box {:potential-autopay-invoices-matches (:potential-autopay-invoices-matches data)}]])
|
||||
|
||||
(when
|
||||
(and (seq (:potential-unpaid-invoices-matches data))
|
||||
(not is-already-matched?)
|
||||
(or is-admin? is-power-user?))
|
||||
[tab {:title "Unpaid Invoices" :key :unpaid-invoices}
|
||||
[potential-unpaid-invoices-matches-box {:potential-unpaid-invoices-matches (:potential-unpaid-invoices-matches data)}]])
|
||||
(when
|
||||
(and (seq (:potential-payment-matches data))
|
||||
(not is-already-matched?)
|
||||
(or is-admin? is-power-user?))
|
||||
[tab {:title "Payment" :key :payment}
|
||||
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]])
|
||||
(when
|
||||
(and (seq (:potential-unpaid-invoices-matches data))
|
||||
(not is-already-matched?)
|
||||
(or is-admin? is-power-user?))
|
||||
[tab {:title "Unpaid Invoices" :key :unpaid-invoices}
|
||||
[potential-unpaid-invoices-matches-box {:potential-unpaid-invoices-matches (:potential-unpaid-invoices-matches data)}]])
|
||||
(when
|
||||
(and (seq (:potential-payment-matches data))
|
||||
(not is-already-matched?)
|
||||
(or is-admin? is-power-user?))
|
||||
[tab {:title "Payment" :key :payment}
|
||||
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]])
|
||||
|
||||
[tab {:title "Details" :key :details}
|
||||
[:div
|
||||
(field "Vendor"
|
||||
[search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:type "typeahead-v3"
|
||||
:auto-focus true
|
||||
:field [:vendor]
|
||||
:disabled (or (boolean (:payment data))
|
||||
should-disable-for-client?)}])
|
||||
(with-meta
|
||||
(field nil
|
||||
[expense-accounts-field
|
||||
{:type "expense-accounts"
|
||||
:field [:accounts]
|
||||
:max (Math/abs (js/parseFloat (:amount data)))
|
||||
:descriptor "credit account"
|
||||
:client (:client data)
|
||||
:disabled (or (boolean (:payment data))
|
||||
should-disable-for-client?)
|
||||
:locations locations}])
|
||||
{:key (str (:id (:vendor data)))})
|
||||
(field "Approval Status"
|
||||
[button-radio
|
||||
{:type "button-radio"
|
||||
:field [:approval-status]
|
||||
:options [[:unapproved "Unapproved"]
|
||||
[:requires-feedback "Client Review"]
|
||||
[:approved "Approved"]
|
||||
[:excluded "Excluded from Ledger"]]
|
||||
:disabled should-disable-for-client?}])
|
||||
[tab {:title "Details" :key :details}
|
||||
[:div
|
||||
[form-builder/field-v2 {:field :vendor}
|
||||
"Vendor"
|
||||
[com/search-backed-typeahead {:search-query (fn [i]
|
||||
[:search_vendor
|
||||
{:query i}
|
||||
[:name :id]])
|
||||
:auto-focus true
|
||||
:disabled (or (boolean (:payment data))
|
||||
should-disable-for-client?)}]]
|
||||
[form-builder/raw-field-v2 {:field :accounts}
|
||||
[expense-accounts-field-v2
|
||||
{:max (Math/abs (js/parseFloat (:amount data)))
|
||||
:descriptor "credit account"
|
||||
:client (:client data)
|
||||
:disabled (or (boolean (:payment data))
|
||||
should-disable-for-client?)
|
||||
:locations locations}]]
|
||||
[form-builder/field-v2 {:field :approval-status}
|
||||
"Approval Status"
|
||||
[com/button-radio-input
|
||||
{:options [[:unapproved "Unapproved"]
|
||||
[:requires-feedback "Client Review"]
|
||||
[:approved "Approved"]
|
||||
[:excluded "Excluded from Ledger"]]
|
||||
:disabled should-disable-for-client?}]]
|
||||
|
||||
(field "Forecasted-transaction"
|
||||
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))])
|
||||
:entity->text :identifier
|
||||
:type "typeahead-v3"
|
||||
:field [:forecast-match]}])
|
||||
(error-notification)
|
||||
(when-not should-disable-for-client?
|
||||
(submit-button "Save"))]]]])
|
||||
{:key (:id data)}))])
|
||||
[form-builder/field-v2 {:field [:forecast-match]}
|
||||
"Forecasted-transaction"
|
||||
[com/entity-typeahead {:entities @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))])
|
||||
:entity->text :identifier}]]
|
||||
[form-builder/error-notification]
|
||||
(when-not should-disable-for-client?
|
||||
[form-builder/submit-button "Save"])]]]]]])])
|
||||
|
||||
(defn form [_]
|
||||
(r/create-class
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.modal :as modal]
|
||||
[auto-ap.views.utils :refer [dispatch-event with-user]]
|
||||
[re-frame.core :as re-frame]))
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.forms.builder :as form-builder]
|
||||
[auto-ap.schema :as schema]
|
||||
[malli.core :as m]))
|
||||
|
||||
(re-frame/reg-sub
|
||||
::can-submit
|
||||
@@ -13,22 +16,19 @@
|
||||
(fn [{ {:keys [data]} :data}]
|
||||
(not-empty data)))
|
||||
|
||||
(def import-form (forms/vertical-form {:submit-event [::save]
|
||||
:change-event [::forms/change ::form]
|
||||
:can-submit [::can-submit]
|
||||
:id ::form}))
|
||||
(def schema
|
||||
(m/schema [:map [:data schema/not-empty-string]]))
|
||||
|
||||
(defn form [{import-completed-event :import-completed}]
|
||||
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form])
|
||||
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} import-form]
|
||||
(defn form []
|
||||
[form-builder/builder {:submit-event [::try-save]
|
||||
:id ::form
|
||||
:schema schema}
|
||||
|
||||
(form-inline {}
|
||||
[:div.field
|
||||
[:label.label
|
||||
"Yodlee manual import table"]
|
||||
[:div.control
|
||||
[raw-field
|
||||
[:textarea.textarea {:field [:data]}]]]])))
|
||||
[form-builder/field-v2 {:required? true
|
||||
:field :data}
|
||||
"Yodlee manual import table"
|
||||
[:textarea.textarea ]]
|
||||
[form-builder/hidden-submit-button]])
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::opening
|
||||
@@ -38,19 +38,16 @@
|
||||
:confirm {:value "Import"
|
||||
:status-from [::status/single ::form]
|
||||
:class "is-primary"
|
||||
:on-click (dispatch-event [::save])
|
||||
:can-submit [::can-submit]
|
||||
:on-click (dispatch-event [::try-save])
|
||||
:close-event [::status/completed ::form]}}]
|
||||
:db (-> db
|
||||
(forms/start-form ::form
|
||||
{:client-id (:id @(re-frame/subscribe [::subs/client]))
|
||||
:data ""}))}))
|
||||
|
||||
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::import-completed
|
||||
(fn [{:keys [db]} [_ {:keys [imported errors] :as result}]]
|
||||
(fn [_ _]
|
||||
{:dispatch [::modal/modal-closed ]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
@@ -66,6 +63,15 @@
|
||||
:uri (str "/api/transactions/batch-upload")
|
||||
:on-success [::import-completed]}})))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::try-save
|
||||
[(forms/in-form ::form)]
|
||||
(fn [{:keys [db]}]
|
||||
(if (not (m/validate schema (:data db)))
|
||||
{:dispatch-n [[::status/error ::form [{:message "Please correct any errors and try again"}]]
|
||||
[::forms/attempted-submit ::form]]}
|
||||
{:dispatch [::save]})))
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,32 +1,35 @@
|
||||
(ns auto-ap.views.pages.transactions.table
|
||||
(:require [auto-ap.events :as events]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.dropdown
|
||||
:refer
|
||||
[drop-down drop-down-contents]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.views.pages.transactions.form :as edit]
|
||||
[auto-ap.views.utils
|
||||
:refer
|
||||
[action-cell-width date->str dispatch-event dispatch-event-with-propagation nf pretty with-role]]
|
||||
[goog.string :as gstring]
|
||||
[re-frame.core :as re-frame]
|
||||
[auto-ap.views.components.buttons :as buttons]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[auto-ap.routes :as routes]))
|
||||
(:require
|
||||
[auto-ap.events :as events]
|
||||
[auto-ap.routes :as routes]
|
||||
[auto-ap.status :as status]
|
||||
[auto-ap.subs :as subs]
|
||||
[auto-ap.views.components.buttons :as buttons]
|
||||
[auto-ap.views.components.dropdown
|
||||
:refer [drop-down drop-down-contents]]
|
||||
[auto-ap.views.components.grid :as grid]
|
||||
[auto-ap.views.pages.data-page :as data-page]
|
||||
[auto-ap.views.pages.transactions.form :as edit]
|
||||
[auto-ap.views.utils
|
||||
:refer [action-cell-width
|
||||
date->str
|
||||
dispatch-event-with-propagation
|
||||
nf
|
||||
pretty
|
||||
with-role]]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[re-frame.core :as re-frame]))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::editing-matches-found
|
||||
(fn [{:keys [db]} [_ which matches]]
|
||||
(fn [_ [_ which matches]]
|
||||
{:dispatch
|
||||
[::edit/editing which (:potential-payment-matches matches) (:potential-autopay-invoices-matches matches) (:potential-unpaid-invoices-matches matches) (:potential-transaction-rule-matches matches)]}))
|
||||
|
||||
(re-frame/reg-event-fx
|
||||
::editing-matches-failed
|
||||
(fn [{:keys [db]} [_ which payment-matches]]
|
||||
(fn [_ [_ which payment-matches]]
|
||||
{:dispatch
|
||||
[::edit/editing which payment-matches]}))
|
||||
|
||||
@@ -71,12 +74,10 @@
|
||||
(fn [{table-params :db} [_ params :as z]]
|
||||
{:db (merge table-params params)}))
|
||||
|
||||
(defn table [{:keys [id data-page check-boxes?]}]
|
||||
(defn table [{:keys [data-page check-boxes?]}]
|
||||
(let [selected-client @(re-frame/subscribe [::subs/client])
|
||||
{:keys [data status params]} @(re-frame/subscribe [::data-page/page data-page])
|
||||
states @(re-frame/subscribe [::status/multi ::edits])
|
||||
is-power-user? @(re-frame/subscribe [::subs/is-power-user?])
|
||||
is-admin? @(re-frame/subscribe [::subs/is-admin?])]
|
||||
{:keys [data params]} @(re-frame/subscribe [::data-page/page data-page])
|
||||
states @(re-frame/subscribe [::status/multi ::edits])]
|
||||
[grid/grid {:data-page data-page
|
||||
:column-count (if selected-client 6 7)
|
||||
:check-boxes? check-boxes?}
|
||||
@@ -94,7 +95,7 @@
|
||||
[grid/sortable-header-cell {:sort-key "status" :sort-name "Status" :style {:width "7em"}} "Status"]
|
||||
[grid/header-cell {:style {:width (action-cell-width 3)}}]]]
|
||||
[grid/body
|
||||
(for [{:keys [client account vendor approval-status payment expected-deposit status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)]
|
||||
(for [{:keys [client vendor payment expected-deposit status bank-account description-original date amount id yodlee-merchant ] :as i} (:data data)]
|
||||
^{:key id}
|
||||
[grid/row {:class (:class i) :id id :entity i}
|
||||
(when-not selected-client
|
||||
|
||||
@@ -51,7 +51,8 @@
|
||||
(re-frame/reg-event-fx
|
||||
::unmounted
|
||||
(fn [{:keys [db]} _]
|
||||
{:dispatch [::data-page/dispose :invoices]
|
||||
{:dispatch-n [[::data-page/dispose :invoices]
|
||||
[::forms/form-closing ::form/form]]
|
||||
::forward/dispose [{:id ::updated}
|
||||
{:id ::checks-printed}]
|
||||
::track/dispose [{:id ::params}]}))
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
[re-frame.core :as re-frame]
|
||||
[react-transition-group :as react-transition-group]
|
||||
#_{:clj-kondo/ignore [:unused-namespace]}
|
||||
[react-datepicker :as react-datepicker]
|
||||
[reagent.core :as reagent]
|
||||
[reagent.core :as r]
|
||||
[react :as react]
|
||||
@@ -64,7 +63,7 @@
|
||||
(def login-url
|
||||
(let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com"
|
||||
redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"))]
|
||||
(str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile")))
|
||||
(str "https://accounts.google.com/o/oauth2/auth?access_type=online&client_id=" client-id "&redirect_uri=" redirect-uri "&response_type=code&max_auth_age=0&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.email+https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fuserinfo.profile")))
|
||||
|
||||
(defn dispatch-value-change [event]
|
||||
(fn [e]
|
||||
@@ -107,26 +106,6 @@
|
||||
(when d
|
||||
(format/parse f d)))
|
||||
|
||||
(defn dispatch-date-change [event]
|
||||
(fn [e]
|
||||
(re-frame/dispatch (conj event
|
||||
(if (str/blank? e)
|
||||
e
|
||||
(date->str (t/from-default-time-zone (c/from-date e)) standard))))))
|
||||
|
||||
(defn dispatch-cljs-date-change [event]
|
||||
(fn [e]
|
||||
(re-frame/dispatch (conj event
|
||||
(if (str/blank? e)
|
||||
e
|
||||
(c/to-local-date e))))))
|
||||
|
||||
;; TODO inline on-changes causes each field to be rerendered each time. When we fix this
|
||||
;; let's make sure that we find away not to trigger a re-render for every component any time any form field
|
||||
;; changes
|
||||
(defmulti do-bind (fn [_ {:keys [type]}]
|
||||
type))
|
||||
|
||||
(defn with-keys [children]
|
||||
(map-indexed (fn [i c] ^{:key i} c) children))
|
||||
|
||||
@@ -151,358 +130,33 @@
|
||||
(first children)
|
||||
[:span])])))
|
||||
|
||||
(defn appearing-group []
|
||||
(let [children (r/children (r/current-component))]
|
||||
(into [transition-group {:exit true
|
||||
:enter true}
|
||||
(for [child children]
|
||||
^{:key (:key (meta child))}
|
||||
[transition
|
||||
{:timeout 200
|
||||
:exit true
|
||||
:in true #_ (= current-stack- (:key (meta child)))}
|
||||
(clj->js (fn [state]
|
||||
(r/as-element
|
||||
[:div {:style {
|
||||
:transition "opacity 150ms ease-in-out"
|
||||
:opacity (cond
|
||||
(= "entered" state)
|
||||
1.0
|
||||
|
||||
(defn multi-field [{:keys [value]} ]
|
||||
(let [value-repr (reagent/atom (mapv
|
||||
(fn [x]
|
||||
(assoc x :key (random-uuid) :new? false))
|
||||
value))]
|
||||
(fn [{:keys [template on-change allow-change? disable-new? disable-remove?]} ]
|
||||
(let [value @value-repr
|
||||
already-has-new-row? (= [:key :new?] (keys (last value)))
|
||||
value (if (or already-has-new-row? disable-new?)
|
||||
value
|
||||
(conj value {:key (random-uuid)
|
||||
:new? true}))]
|
||||
[:div {:style {:margin-bottom "0.25em"}}
|
||||
(for [[i override] (map vector (range) value)
|
||||
:let [is-disabled? (if (= false allow-change?)
|
||||
(not (boolean (:new? override)))
|
||||
nil)]
|
||||
]
|
||||
^{:key (:key override)}
|
||||
[:div.level {:style {:margin-bottom "0.25em"}}
|
||||
[:div.level-left {:style {:padding "0.5em 1em"}
|
||||
:class (cond
|
||||
(and (= i (dec (count value)))
|
||||
(:new? override))
|
||||
"has-background-light"
|
||||
(= "entering" state)
|
||||
0.0
|
||||
|
||||
(:new? override)
|
||||
"has-background-info-light"
|
||||
:else
|
||||
"")}
|
||||
(let [template (if (fn? template)
|
||||
(template override)
|
||||
template)]
|
||||
|
||||
[:<> (for [[idx template] (map vector (range ) template)]
|
||||
^{:key idx}
|
||||
(= "exiting" state)
|
||||
0.0
|
||||
|
||||
[:div.level-item
|
||||
(update template 1 assoc
|
||||
:value (let [value (get-in override (get-in template [1 :field])) ;; TODO this is really ugly to support maps or strings
|
||||
value (if (map? value)
|
||||
(dissoc value :key :new?)
|
||||
value)]
|
||||
(if (= value {})
|
||||
nil
|
||||
value))
|
||||
:disabled (or is-disabled? (get-in template [1 :disabled]))
|
||||
:on-change (fn [e]
|
||||
(reset! value-repr
|
||||
(into []
|
||||
(filter (fn [r]
|
||||
(not= [:key :new?] (keys r)))
|
||||
(assoc-in value
|
||||
(into [i] (get-in template [1 :field]))
|
||||
(let [this-value (if (and e (.. e -target))
|
||||
(.. e -target -value )
|
||||
e)]
|
||||
(if (map? this-value)
|
||||
(update this-value :key (fnil identity (random-uuid)))
|
||||
this-value)) ))))
|
||||
(on-change (mapv
|
||||
(fn [v]
|
||||
(dissoc v :new? :key))
|
||||
@value-repr))))])
|
||||
])
|
||||
(when-not disable-remove?
|
||||
[:div.level-item
|
||||
[:a.button.level-item
|
||||
{:disabled is-disabled?
|
||||
:on-click (fn []
|
||||
(when-not is-disabled?
|
||||
(reset! value-repr (into []
|
||||
(filter (fn [{:keys [key ]}]
|
||||
(not= key (:key override)))
|
||||
(filter (fn [r]
|
||||
(not= [:key :new?] (keys r)))
|
||||
value))))
|
||||
|
||||
(on-change (mapv
|
||||
(fn [v]
|
||||
(dissoc v :new? :key))
|
||||
@value-repr))))}
|
||||
[:span.icon [:span.icon-remove]]]])
|
||||
]])]))))
|
||||
|
||||
(defmethod do-bind "select" [dom {:keys [field allow-nil? subscription event class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (dispatch-value-change (conj event field))
|
||||
|
||||
:value (or (get-in subscription field) "")
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)
|
||||
options (if allow-nil?
|
||||
(with-keys (conj rest [:option {:value nil}]))
|
||||
(with-keys rest))]
|
||||
(into [dom (dissoc keys :allow-nil?)] options)))
|
||||
|
||||
|
||||
(defmethod do-bind "radio" [dom {:keys [field subscription event class value spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (dispatch-value-change (conj event field))
|
||||
:checked (= (get-in subscription field) value)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field ))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "checkbox" [dom {:keys [field subscription event class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (dispatch-event (-> event
|
||||
(conj field)
|
||||
(conj (not (get-in subscription field)))))
|
||||
:checked (boolean (get-in subscription field))
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field ))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "typeahead" [dom {:keys [field text-field event text-event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [selected text-value]
|
||||
(re-frame/dispatch (conj (conj event field) selected))
|
||||
(when text-field
|
||||
(re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "multi-field" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [value]
|
||||
(re-frame/dispatch (conj (conj event field) value)))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
|
||||
(defmethod do-bind "typeahead-entity" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [selected]
|
||||
(re-frame/dispatch (conj (conj event field) selected))
|
||||
#_(when text-field
|
||||
(re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "typeahead-v3" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [selected]
|
||||
(re-frame/dispatch (conj (conj event field) selected))
|
||||
#_(when text-field
|
||||
(re-frame/dispatch (conj (conj (or text-event event) text-field) text-value))))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "date" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
selected (get-in subscription field)
|
||||
|
||||
selected (cond (string? selected)
|
||||
(c/to-date (t/to-default-time-zone (t/from-default-time-zone (str->date selected standard))))
|
||||
|
||||
(instance? goog.date.DateTime selected)
|
||||
(c/to-date (t/to-default-time-zone (t/from-default-time-zone selected)))
|
||||
|
||||
(instance? goog.date.Date selected)
|
||||
(c/to-date selected)
|
||||
|
||||
:else
|
||||
selected )
|
||||
keys (assoc keys
|
||||
:on-change (if (:cljs-date? keys)
|
||||
(dispatch-cljs-date-change (conj event field))
|
||||
(dispatch-date-change (conj event field)))
|
||||
:selected selected
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "date2" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
selected (get-in subscription field)
|
||||
keys (assoc keys
|
||||
:on-change (fn [v]
|
||||
(re-frame/dispatch (-> event (conj field) (conj v))))
|
||||
|
||||
:value selected
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "expense-accounts" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:value (get-in subscription field)
|
||||
:event (conj event field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
|
||||
(defmethod do-bind "button-radio" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:value (get-in subscription field)
|
||||
:on-change (fn [v]
|
||||
(re-frame/dispatch (-> event (conj field) (conj v))))
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :event :subscription :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "number" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [e]
|
||||
(.preventDefault e)
|
||||
(re-frame/dispatch (-> event
|
||||
(conj field)
|
||||
(conj (let [val (.. e -target -value)]
|
||||
(cond (and val
|
||||
(re-matches #"[\-]?(\d+)(\.\d{2})?" val))
|
||||
(js/parseFloat val)
|
||||
|
||||
(str/blank? val )
|
||||
nil
|
||||
|
||||
:else
|
||||
val))))))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "textarea->table" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [x]
|
||||
(re-frame/dispatch (-> event
|
||||
(conj field)
|
||||
(conj x))))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind "money" [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (fn [x]
|
||||
(re-frame/dispatch (-> event
|
||||
(conj field)
|
||||
(conj x))))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defmethod do-bind :default [dom {:keys [field event subscription class spec] :as keys} & rest]
|
||||
(let [field (if (keyword? field) [field] field)
|
||||
event (if (keyword? event) [event] event)
|
||||
keys (assoc keys
|
||||
:on-change (dispatch-value-change (conj event field))
|
||||
:value (get-in subscription field)
|
||||
:class (str class
|
||||
(when (and spec (not (s/valid? spec (get-in subscription field))))
|
||||
" is-danger")))
|
||||
keys (dissoc keys :field :subscription :event :spec)]
|
||||
(into [dom keys] (with-keys rest))))
|
||||
|
||||
(defn bind-field [all]
|
||||
(apply do-bind all))
|
||||
|
||||
|
||||
|
||||
(defn horizontal-field [label & controls]
|
||||
[:div.field.is-horizontal
|
||||
(when label
|
||||
[:div.field-label
|
||||
label
|
||||
])
|
||||
(into
|
||||
[:div.field-body]
|
||||
(with-keys (map (fn [x] [:div.field x]) controls)))])
|
||||
|
||||
(def date-picker
|
||||
(reagent/adapt-react-class (.-default react-datepicker)))
|
||||
|
||||
(defn date-picker-friendly [params]
|
||||
[date-picker (assoc params
|
||||
:class-name "input"
|
||||
:disabled-keyboard-navigation true
|
||||
:start-open false
|
||||
:class "input"
|
||||
:format-week-number (fn [] "")
|
||||
:previous-month-button-label ""
|
||||
:next-month-button-label ""
|
||||
:next-month-label ""
|
||||
:type "date")])
|
||||
(= "exited" state)
|
||||
0.0)}}
|
||||
child])))])])))
|
||||
|
||||
(defn coerce-date [d]
|
||||
(cond (and (string? d)
|
||||
@@ -524,7 +178,7 @@
|
||||
:else
|
||||
nil ))
|
||||
|
||||
(defn date-picker-optional-internal [params]
|
||||
(defn date-picker-internal [params]
|
||||
|
||||
(let [[text set-text ] (react/useState (some-> params :value coerce-date (date->str standard)))
|
||||
[value set-value ] (react/useState (some-> params :value coerce-date))
|
||||
@@ -561,12 +215,14 @@
|
||||
(swap-external-value (some-> (.. e -target -value) coerce-date))))
|
||||
|
||||
:on-blur (fn []
|
||||
(swap-external-value (some-> text coerce-date)))
|
||||
(swap-external-value (some-> text coerce-date))
|
||||
(when (:on-blur params)
|
||||
((:on-blur params))))
|
||||
:type "date" :placeholder "12/1/2021")]
|
||||
]]))
|
||||
|
||||
(defn date-picker-optional []
|
||||
[:f> date-picker-optional-internal
|
||||
(defn date-picker []
|
||||
[:f> date-picker-internal
|
||||
(r/props (r/current-component))])
|
||||
|
||||
(defn local-now []
|
||||
@@ -654,3 +310,25 @@
|
||||
|
||||
:else
|
||||
x))
|
||||
|
||||
|
||||
(defn parse-jwt [jwt]
|
||||
(when-let [json (some-> jwt
|
||||
(str/split #"\.")
|
||||
second
|
||||
base64/decodeString)]
|
||||
(js->clj (.parse js/JSON json) :keywordize-keys true)))
|
||||
|
||||
(defn coerce-float [f]
|
||||
(cond (str/blank? f)
|
||||
nil
|
||||
|
||||
(float? f)
|
||||
f
|
||||
|
||||
(and (string? f)
|
||||
(not (js/Number.isNaN (js/parseFloat f))))
|
||||
(js/parseFloat f)
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
Reference in New Issue
Block a user