merged many changes.

This commit is contained in:
2022-07-23 14:41:33 -07:00
77 changed files with 2967 additions and 3986 deletions

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

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

View File

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

View File

@@ -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)]

View File

@@ -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

View File

@@ -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

View File

@@ -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
]))

View File

@@ -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? %))))

View File

@@ -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]

View File

@@ -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

View File

@@ -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))})))

View File

@@ -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]))

View File

@@ -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]))})

View File

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

View File

@@ -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)]))

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

View File

@@ -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

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

View File

@@ -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"}]]]]])

View File

@@ -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

View File

@@ -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]))

View File

@@ -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}]]]]]])

View File

@@ -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}))}))

View File

@@ -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"]])]])

View File

@@ -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

View File

@@ -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]]))

View File

@@ -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))])

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

View File

@@ -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}]]]]])

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

View File

@@ -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 "<="}]]]]]]
)

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

View File

@@ -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) "")

View File

@@ -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]}}]}))

View File

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

View File

@@ -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]]))))

View File

@@ -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 []

View File

@@ -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"]]]]))

View File

@@ -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]

View File

@@ -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}))

View File

@@ -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"}])]]])]]]))

View File

@@ -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]))})])

View File

@@ -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

View File

@@ -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

View File

@@ -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]}}]}))

View File

@@ -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]})))

View File

@@ -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]}])

View File

@@ -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

View File

@@ -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]}])}))

View File

@@ -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"]]]]))

View File

@@ -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]}))

View File

@@ -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"]]]])])

View File

@@ -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]}))

View File

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

View File

@@ -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}))

View File

@@ -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])}})))

View File

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

View File

@@ -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

View File

@@ -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]

View File

@@ -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)]])]]]])])

View File

@@ -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 ", "

View File

@@ -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"

View File

@@ -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)}))

View File

@@ -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

View File

@@ -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]})))

View File

@@ -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

View File

@@ -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}]}))

View File

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