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

69
package-lock.json generated
View File

@@ -9,14 +9,15 @@
"version": "1.0.0", "version": "1.0.0",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.5",
"downshift": "^6.1.3", "downshift": "^6.1.3",
"dropzone": "^4.3.0", "dropzone": "^4.3.0",
"minisearch": "^3.0.2", "minisearch": "^3.0.2",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.1", "react": "^17.0.1",
"react-datepicker": "^4.8.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-plaid-link": "^3.2.1", "react-plaid-link": "^3.2.1",
"react-popper": "^2.3.0",
"react-prop-types": "^0.4.0", "react-prop-types": "^0.4.0",
"react-signature-canvas": "^1.0.3", "react-signature-canvas": "^1.0.3",
"react-signature-pad": "0.0.6", "react-signature-pad": "0.0.6",
@@ -583,18 +584,6 @@
"d3-time": "1" "d3-time": "1"
} }
}, },
"node_modules/date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw==",
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/decimal.js-light": { "node_modules/decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@@ -1320,23 +1309,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/react-datepicker": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz",
"integrity": "sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==",
"dependencies": {
"@popperjs/core": "^2.9.2",
"classnames": "^2.2.6",
"date-fns": "^2.24.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.12.0",
"react-popper": "^2.2.5"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
@@ -1365,19 +1337,6 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"node_modules/react-onclickoutside": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
"integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-plaid-link": { "node_modules/react-plaid-link": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-plaid-link/-/react-plaid-link-3.2.1.tgz", "resolved": "https://registry.npmjs.org/react-plaid-link/-/react-plaid-link-3.2.1.tgz",
@@ -2596,11 +2555,6 @@
"d3-time": "1" "d3-time": "1"
} }
}, },
"date-fns": {
"version": "2.28.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.28.0.tgz",
"integrity": "sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw=="
},
"decimal.js-light": { "decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@@ -3163,19 +3117,6 @@
"object-assign": "^4.1.1" "object-assign": "^4.1.1"
} }
}, },
"react-datepicker": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.8.0.tgz",
"integrity": "sha512-u69zXGHMpxAa4LeYR83vucQoUCJQ6m/WBsSxmUMu/M8ahTSVMMyiyQzauHgZA2NUr9y0FUgOAix71hGYUb6tvg==",
"requires": {
"@popperjs/core": "^2.9.2",
"classnames": "^2.2.6",
"date-fns": "^2.24.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.12.0",
"react-popper": "^2.2.5"
}
},
"react-dom": { "react-dom": {
"version": "17.0.1", "version": "17.0.1",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-17.0.1.tgz",
@@ -3201,12 +3142,6 @@
"resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz", "resolved": "https://registry.npmjs.org/react-lifecycles-compat/-/react-lifecycles-compat-3.0.4.tgz",
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==" "integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
}, },
"react-onclickoutside": {
"version": "6.12.2",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.12.2.tgz",
"integrity": "sha512-NMXGa223OnsrGVp5dJHkuKxQ4czdLmXSp5jSV9OqiCky9LOpPATn3vLldc+q5fK3gKbEHvr7J1u0yhBh/xYkpA==",
"requires": {}
},
"react-plaid-link": { "react-plaid-link": {
"version": "3.2.1", "version": "3.2.1",
"resolved": "https://registry.npmjs.org/react-plaid-link/-/react-plaid-link-3.2.1.tgz", "resolved": "https://registry.npmjs.org/react-plaid-link/-/react-plaid-link-3.2.1.tgz",

View File

@@ -7,14 +7,15 @@
"test": "test" "test": "test"
}, },
"dependencies": { "dependencies": {
"@popperjs/core": "^2.11.5",
"downshift": "^6.1.3", "downshift": "^6.1.3",
"dropzone": "^4.3.0", "dropzone": "^4.3.0",
"minisearch": "^3.0.2", "minisearch": "^3.0.2",
"prop-types": "^15.7.2", "prop-types": "^15.7.2",
"react": "^17.0.1", "react": "^17.0.1",
"react-datepicker": "^4.8.0",
"react-dom": "^17.0.1", "react-dom": "^17.0.1",
"react-plaid-link": "^3.2.1", "react-plaid-link": "^3.3.2",
"react-popper": "^2.3.0",
"react-prop-types": "^0.4.0", "react-prop-types": "^0.4.0",
"react-signature-canvas": "^1.0.3", "react-signature-canvas": "^1.0.3",
"react-signature-pad": "0.0.6", "react-signature-pad": "0.0.6",

View File

@@ -19,6 +19,7 @@
[bidi "2.1.6"] [bidi "2.1.6"]
[ring/ring-defaults "0.3.2" :exclusions [ring ring/ring-core]] [ring/ring-defaults "0.3.2" :exclusions [ring ring/ring-core]]
[mount "0.1.16"] [mount "0.1.16"]
[metosin/malli "0.8.9"]
[tolitius/yang "0.1.23"] [tolitius/yang "0.1.23"]
[ring "1.8.2" :exclusions [commons-codec [ring "1.8.2" :exclusions [commons-codec
commons-io commons-io

View File

@@ -10954,15 +10954,8 @@ span[data-tooltip].has-tooltip-primary-two {
} }
.typeahead-menu { .typeahead-menu {
position: absolute;
display: inline-block;
width: 100%;
top: 100%;
left: 0;
z-index: 10000; z-index: 10000;
overflow: auto; min-width: 200px;
float: left;
min-width: 160px;
padding: 5px 0; padding: 5px 0;
margin: 2px 0 0; margin: 2px 0 0;
list-style: none; list-style: none;
@@ -10981,6 +10974,12 @@ span[data-tooltip].has-tooltip-primary-two {
overflow: auto; overflow: auto;
} }
.typeahead input[disabeld] {
background-color: whitesmoke;
border-color: whitesmoke;
box-shadow: none;
}
.typeahead-suggestion { .typeahead-suggestion {
display: block; display: block;
overflow: visible; overflow: visible;

File diff suppressed because one or more lines are too long

View File

@@ -220,7 +220,7 @@ nav.navbar .navbar-item.is-active {
margin: 0 -50px; margin: 0 -50px;
padding-left: 50px; padding-left: 50px;
} }
.aside .main .icon { .aside .main .menu-item .icon {
font-size: 19px; font-size: 19px;
padding-right: 30px; padding-right: 30px;
color: #A0A0A0; color: #A0A0A0;
@@ -462,3 +462,10 @@ table.balance-sheet th.total {
.modal-card-foot { .modal-card-foot {
flex-wrap: wrap; flex-wrap: wrap;
} }
.typeahead input[disabeld] {
background-color: whitesmoke !important;
border-color: whitesmoke !important;
box-shadow: none;
}

View File

@@ -75,32 +75,25 @@ $fullhd-enabled: false;
} }
.typeahead-menu { .typeahead-menu {
position:absolute; z-index: 10000;
display: inline-block; min-width: 200px;
width: 100%; padding: 5px 0;
top: 100%; margin: 2px 0 0;
left: 0; list-style: none;
z-index: 10000; font-size: 14px;
overflow: auto; text-align: left;
float: left; background-color: $white;
min-width: 160px; border: 1px solid #cccccc;
padding: 5px 0; border: 1px solid rgba(0, 0, 0, 0.15);
margin: 2px 0 0; border-radius: 4px;
list-style: none; -webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
font-size: 14px; box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
text-align: left; /* background-clip: padding-box; */
background-color: $white;
border: 1px solid #cccccc;
border: 1px solid rgba(0, 0, 0, 0.15);
border-radius: 4px;
-webkit-box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
/* background-clip: padding-box; */
} }
.modal-card { .modal-card {
overflow: auto; overflow: auto;
} }
.typeahead-suggestion { .typeahead-suggestion {
display: block; display: block;
overflow: visible; overflow: visible;
@@ -171,3 +164,5 @@ tbody tr.live-added {
.button.is-outlined { .button.is-outlined {
border-width: 2.5px; border-width: 2.5px;
} }

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/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/vendor [:db/id :vendor/name]
:transaction/matched-rule [:db/id :transaction-rule/note] :transaction/matched-rule [:db/id :transaction-rule/note]
:transaction/forecast-match [:db/id :forecasted-transaction/identifier]
:transaction/accounts [:transaction-account/amount :transaction/accounts [:transaction-account/amount
:db/id :db/id
:transaction-account/location :transaction-account/location

View File

@@ -234,7 +234,7 @@
:user :user
{:fields {:id {:type :id} {:fields {:id {:type :id}
:name {:type 'String} :name {:type 'String}
:role {:type 'String} :role {:type :role}
:clients {:type '(list :client)}}} :clients {:type '(list :client)}}}
:account_client_override :account_client_override
@@ -415,7 +415,7 @@
:edit_user :edit_user
{:fields {:id {:type :id} {:fields {:id {:type :id}
:name {:type 'String} :name {:type 'String}
:role {:type 'String} :role {:type :role}
:clients {:type '(list String)}}} :clients {:type '(list String)}}}
:add_contact :add_contact
@@ -520,6 +520,11 @@
:applicability {:values [{:enum-value :global} :applicability {:values [{:enum-value :global}
{:enum-value :optional} {:enum-value :optional}
{:enum-value :customized}]} {: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} :account_type {:values [{:enum-value :dividend}
{:enum-value :expense} {:enum-value :expense}
{:enum-value :asset} {:enum-value :asset}

View File

@@ -2,25 +2,34 @@
(:require (:require
[auto-ap.datomic [auto-ap.datomic
:refer [add-sorter-fields apply-pagination apply-sort-3 conn merge-query]] :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] [auto-ap.plaid.core :as p]
[clj-time.coerce :as coerce] [clj-time.coerce :as coerce]
[clj-time.core :as time] [clj-time.core :as time]
[clojure.tools.logging :as log]
[com.walmartlabs.lacinia.util :refer [attach-resolvers]] [com.walmartlabs.lacinia.util :refer [attach-resolvers]]
[datomic.api :as d])) [datomic.api :as d]))
(defn plaid-link-token [context value args] (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)))] (let [client-code (:client/code (d/pull (d/db conn) [:client/code] (:client_id value)))]
{:token (p/get-link-token client-code)})) {:token (p/get-link-token client-code)}))
(defn link-plaid [context value args] (defn link-plaid [context value args]
(assert-admin (:id context))
(when-not (:client_code value) (when-not (:client_code value)
(throw (ex-info "Client not provided" {:validation-error "Client not provided."}))) (throw (ex-info "Client not provided" {:validation-error "Client not provided."})))
(when-not (:public_token value) (when-not (:public_token value)
(throw (ex-info "Public token not provided" {:validation-error "public token not provided"}))) (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))) (let [access-token (:access_token (p/exchange-public-token (:public_token value) (:client_code value)))
account-result (p/get-accounts access-token ) account-result (p/get-accounts access-token )
item {:plaid-item/client [:client/code (:client_code value)] item {:plaid-item/client [:client/code (:client_code value)]
@@ -40,7 +49,8 @@
:plaid-item/_accounts "plaid-item"} :plaid-item/_accounts "plaid-item"}
balance (assoc :plaid-account/balance balance))))) balance (assoc :plaid-account/balance balance)))))
(into [item]))) (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 (def default-read '[:db/id
@@ -54,7 +64,6 @@
:plaid-account/name]}]) :plaid-account/name]}])
(defn raw-graphql-ids [db args] (defn raw-graphql-ids [db args]
(println args)
(let [query (cond-> {:query {:find [] (let [query (cond-> {:query {:find []
:in ['$] :in ['$]
:where []} :where []}
@@ -63,6 +72,11 @@
(:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]} (:sort args) (add-sorter-fields {"external-id" ['[?e :plaid-item/external-id ?sort-external-id]]}
args) 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) (:client-id args)
(merge-query {:query {:in '[?client-id] (merge-query {:query {:in '[?client-id]
:where ['[?e :plaid-item/client ?client-id]]} :where ['[?e :plaid-item/client ?client-id]]}
@@ -93,7 +107,7 @@
(defn get-plaid-item-page [context args value] (defn get-plaid-item-page [context args value]
(assert-admin (:id context))
(let [args (assoc args :id (:id context)) (let [args (assoc args :id (:id context))
[plaid-items cnt] (get-graphql (<-graphql (assoc args :id (:id context))))] [plaid-items cnt] (get-graphql (<-graphql (assoc args :id (:id context))))]
{:plaid_items (->> plaid-items {:plaid_items (->> plaid-items

View File

@@ -1,59 +1,49 @@
(ns auto-ap.graphql.transaction-rules (ns auto-ap.graphql.transaction-rules
(:require [auto-ap.datomic (:require
:refer [auto-ap.datomic
[audit-transact merge-query remove-nils replace-nils-with-retract uri conn]] :refer [audit-transact
[auto-ap.datomic.transaction-rules :as tr] conn
[auto-ap.datomic.transactions :as d-transactions] merge-query
[auto-ap.graphql.utils remove-nils
:refer replace-nils-with-retract]]
[->graphql [auto-ap.datomic.transaction-rules :as tr]
<-graphql [auto-ap.datomic.transactions :as d-transactions]
assert-admin [auto-ap.graphql.utils
ident->enum-f :refer [->graphql
limited-clients <-graphql
result->page assert-admin
snake->kebab]] ident->enum-f
[auto-ap.rule-matching :as rm] limited-clients
[auto-ap.utils :refer [dollars=]] result->page
[clj-time.coerce :as coerce] snake->kebab]]
[clojure.set :as set] [auto-ap.rule-matching :as rm]
[clojure.string :as str] [auto-ap.utils :refer [dollars=]]
[clojure.tools.logging :as log] [clj-time.coerce :as c]
[datomic.api :as d] [clojure.set :as set]
[clj-time.coerce :as c]) [clojure.string :as str]
(:import java.time.temporal.ChronoField)) [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)) (let [args (assoc args :id (:id context))
[journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))] [journal-entries journal-entries-count] (tr/get-graphql (<-graphql args))]
(result->page (->> journal-entries (result->page (->> journal-entries
(map (ident->enum-f :transaction-rule/transaction-approval-status))) (map (ident->enum-f :transaction-rule/transaction-approval-status)))
journal-entries-count :transaction_rules args))) 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))) (if (= "admin" (:user/role (:id context)))
(let [all-rules (tr/get-all) (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))) (map ->graphql (rm/get-matching-rules transaction all-rules)))
nil)) 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]}] (defn transaction-rule-account->entity [{:keys [id account_id percentage location]}]
(remove-nils #:transaction-rule-account {:percentage percentage (remove-nils #:transaction-rule-account {:percentage percentage
:db/id id :db/id id
:account account_id :account account_id
:location location})) :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)) (assert-admin (:id context))
(let [existing-transaction-rule (tr/get-by-id transaction_rule_id)] (let [existing-transaction-rule (tr/get-by-id transaction_rule_id)]
(when-not (:transaction-rule/description existing-transaction-rule) (when-not (:transaction-rule/description existing-transaction-rule)
@@ -62,10 +52,9 @@
(audit-transact [[:db/retractEntity transaction_rule_id]] (:id context)) (audit-transact [[:db/retractEntity transaction_rule_id]] (:id context))
transaction_rule_id)) 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)) (assert-admin (:id context))
(let [existing-transaction (tr/get-by-id id) (let [existing-transaction (tr/get-by-id id)
deleted (deleted-accounts existing-transaction accounts)
account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts)) account-total (reduce + 0 (map (fn [x] (:percentage x)) accounts))
_ (when-not (dollars= 1.0 account-total) _ (when-not (dollars= 1.0 account-total)
(let [error (str "Account total (" account-total ") does not reach 100%")] (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")] (let [error (str "You must provide a description or a yodlee merchant")]
(throw (ex-info error {:validation-error error})))) (throw (ex-info error {:validation-error error}))))
_ (doseq [a accounts _ (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) client (d/entity (d/db conn) client_id)
]] ]]
(when (and location (not= location (:location a))) (when (and location (not= location (:location a)))
@@ -120,7 +109,7 @@
(defn tr [z x] (defn tr [z x]
(re-find (re-pattern 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 (d/query
(cond-> {:query {:find ['(pull ?e [* {:transaction/client [:client/name] (cond-> {:query {:find ['(pull ?e [* {:transaction/client [:client/name]
@@ -204,7 +193,7 @@
(map ->graphql)) (map ->graphql))
conj []))) 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) (assert-admin id)
(-test-transaction-rule id #:transaction-rule {:description description (-test-transaction-rule id #:transaction-rule {:description description
:client (when client_id {:db/id client_id}) :client (when client_id {:db/id client_id})
@@ -217,6 +206,6 @@
true 15)) 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) (assert-admin id)
(-test-transaction-rule id (tr/get-by-id transaction_rule_id) false count)) (-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.datomic.users :as d-users]
[auto-ap.graphql.utils :refer [->graphql assert-admin]])) [auto-ap.graphql.utils :refer [->graphql assert-admin]]))
(def role->datomic-role {":none" :user-role/none (def role->datomic-role {:none :user-role/none
":admin" :user-role/admin :admin :user-role/admin
":power_user" :user-role/power-user :power_user :user-role/power-user
":manager" :user-role/manager :manager :user-role/manager
":user" :user-role/user}) :user :user-role/user})
(defn edit-user [context {:keys [edit_user] :as args} value] (defn edit-user [context {:keys [edit_user] :as args} value]
(assert-admin (:id context)) (assert-admin (:id context))

View File

@@ -128,7 +128,7 @@
(if (str/includes? q "&") (if (str/includes? q "&")
(str "\"" q "\"~0.8") (str "\"" q "\"~0.8")
(let [parts (-> q (let [parts (-> q
(str/replace #"[\[\]\+\*]" "") (str/replace #"[\[\]\+\*\-]" "")
(str/split #"\s+")) (str/split #"\s+"))
exacts (butlast parts) exacts (butlast parts)
partial (last parts)] partial (last parts)]

View File

@@ -108,7 +108,7 @@
(add-shutdown-hook! shutdown-mount) (add-shutdown-hook! shutdown-mount)
(start-server :port 9000 :bind "0.0.0.0" #_#_:handler (cider-nrepl-handler)) (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))) (apply mount/start-without without)))
(comment (comment

View File

@@ -14,11 +14,9 @@
"rules" :admin-rules "rules" :admin-rules
"accounts" :admin-accounts "accounts" :admin-accounts
"import-batches" :admin-import-batches "import-batches" :admin-import-batches
"reminders" :admin-reminders
"vendors" :admin-vendors "vendors" :admin-vendors
"excel-import" :admin-excel-import "excel-import" :admin-excel-import
"yodlee2" :admin-yodlee2 "yodlee2" :admin-yodlee2}
"plaid" :admin-plaid}
"invoices/" {"" :invoices "invoices/" {"" :invoices
"import" :import-invoices "import" :import-invoices
"unpaid" :unpaid-invoices "unpaid" :unpaid-invoices
@@ -33,6 +31,7 @@
"requires-feedback" :requires-feedback-transactions "requires-feedback" :requires-feedback-transactions
"excluded" :excluded-transactions} "excluded" :excluded-transactions}
"reports/" {"" :reports} "reports/" {"" :reports}
"plaid" :plaid
"ledger/" {"" :ledger "ledger/" {"" :ledger
"profit-and-loss" :profit-and-loss "profit-and-loss" :profit-and-loss
"balance-sheet" :balance-sheet "balance-sheet" :balance-sheet

View File

@@ -2,21 +2,4 @@
(:require [clojure.spec.alpha :as s] (:require [clojure.spec.alpha :as s]
[auto-ap.entities.shared :as shared])) [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 %) :ldt? #(instance? org.joda.time.LocalDate %)
:str? (s/and string? #(re-matches date-regex %))))) :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? (s/def ::required-identifier (s/and string?
#(not (str/blank? %)))) #(not (str/blank? %))))

View File

@@ -530,8 +530,8 @@
[title a b (and (:value a) (:value b) [title a b (and (:value a) (:value b)
{:border (:border b) {:border (:border b)
:format :dollar :format :dollar
:value (- (:value a) :value (- (or (:value a) 0.0)
(:value b))})])))) (or (:value b) 0.0))})]))))
(defn summarize-balance-sheet [pnl-data] (defn summarize-balance-sheet [pnl-data]
(let [pnl-datas (map (fn [p] (let [pnl-datas (map (fn [p]

View File

@@ -17,7 +17,8 @@
(re-frame/reg-fx (re-frame/reg-fx
:redirect :redirect
(fn [uri] (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 (re-frame/reg-fx
:set-uri-params :set-uri-params

View File

@@ -2,13 +2,13 @@
(:require (:require
[auto-ap.db :as db] [auto-ap.db :as db]
[auto-ap.routes :as routes] [auto-ap.routes :as routes]
[auto-ap.subs :as subs]
[auto-ap.utils :refer [by]] [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] [bidi.bidi :as bidi]
[clojure.string :as str] [clojure.string :as str]
[goog.crypt.base64 :as b64] [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] (defn jwt->data [token]
(js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." )))))) (js->clj (.parse js/JSON (b64/decodeString (second (str/split token #"\." ))))))
@@ -142,15 +142,26 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::set-active-route ::set-active-route
(fn [{:keys [db]} [_ handler params route-params]] (fn [{:keys [db]} [_ handler params route-params]]
(cond
(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))) (and (not= "admin" (:user/role (parse-jwt (:user db))))
{:redirect "/login" (str/includes? (name handler) "admin"))
:db (assoc db :active-route :login {:redirect (bidi/path-for routes/routes :index)
:page-failure nil)} :db (assoc db :active-route :index
:active-page :index
:menu nil
:page-failure nil)}
:else
{:db (-> db {:db (-> db
(assoc :active-route handler (assoc :active-route handler
:page-failure nil :page-failure nil
:menu nil
:query-params params :query-params params
:route-params route-params) :route-params route-params)
(auto-ap.views.pages.data-page/dispose-all))}))) (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] (:require [re-frame.core :as re-frame]
[re-frame.interceptor :as i] [re-frame.interceptor :as i]
[auto-ap.status :as status] [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 (re-frame/reg-sub
::form ::form
(fn [db [_ x]] (fn [db [_ x]]
(get (-> db ::forms) x))) (update (get (-> db ::forms) x)
:visited (fn [v]
(or v #{})))))
(re-frame/reg-sub (re-frame/reg-sub
::field ::field
@@ -28,12 +31,20 @@
([db form data] ([db form data]
(start-form db form data nil)) (start-form db form data nil))
([db form data complete-listener] ([db form data complete-listener]
(assoc-in db [::forms form] {:error nil (-> db
:active? true (assoc-in [::forms form] {:error nil
:id (random-uuid) :active? true
:status nil :id (random-uuid)
:data data :visited #{}
:complete-listener complete-listener}))) :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] (defn triggers-saved [form data-key]
(i/->interceptor (i/->interceptor
@@ -75,6 +86,28 @@
db db
(partition 2 path-pairs)))) (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] (defn change-handler [form customize-fn]
(fn [db [_ & path-pairs]] (fn [db [_ & path-pairs]]
@@ -92,6 +125,7 @@
(re-frame/reg-event-db (re-frame/reg-event-db
::save-error ::save-error
(fn [db [_ form result]] (fn [db [_ form result]]
(println result)
(-> db (-> db
(assoc-in [::forms form :status] :error) (assoc-in [::forms form :status] :error)
(assoc-in [::forms form :error] (or (:message (first result)) (assoc-in [::forms form :error] (or (:message (first result))
@@ -138,68 +172,3 @@
(assoc-in [::forms id :error] nil))) (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 (ns auto-ap.forms.builder
(:require (:require
[auto-ap.views.utils :refer [bind-field]]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[react :as react] [react :as react]
[reagent.core :as r] [reagent.core :as r]
[auto-ap.forms :as forms] [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")) (defonce ^js/React.Context form-context (react/createContext "default"))
(def ^js/React.Provider Provider (. form-context -Provider)) (def ^js/React.Provider Provider (. form-context -Provider))
@@ -15,104 +16,231 @@
(def ^js/React.Provider FormScopeProvider (. form-scope-context -Provider)) (def ^js/React.Provider FormScopeProvider (. form-scope-context -Provider))
(def ^js/React.Consumer FormScopeConsumer (. form-scope-context -Consumer)) (def ^js/React.Consumer FormScopeConsumer (. form-scope-context -Consumer))
(defn builder [{:keys [can-submit data-sub change-event submit-event id fullwidth?] :as z}] (defn valid-field? [problems field-path]
(let [data-sub (or data-sub [::forms/form id]) (not (get-in (me/humanize problems) field-path)))
change-event (or change-event [::forms/change id])
{:keys [data error] form-key :id} @(re-frame/subscribe data-sub) (defn spec-error-message [problems field-path error-messages]
status @(re-frame/subscribe [::status/single id])] (-> (me/humanize problems
(r/create-element Provider #js {:value #js {:can-submit @(re-frame/subscribe can-submit) {:errors (merge (-> me/default-errors
:change-event change-event (assoc ::m/missing-key {:error/message "Required"}
:submit-event submit-event ::m/invalid-type {:error/fn
:error error (fn [a b]
:status status (if (nil? (:value a))
:id id "Required"
:data data "Invalid"))}))
:fullwidth? fullwidth?}} 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 (r/as-element
^{:key form-key} ^{:key form-key}
[:form {:on-submit (fn [e] [:form {:on-submit (fn [e]
(when (.-stopPropagation e) (when (.-stopPropagation e)
(.stopPropagation e) (.stopPropagation e)
(.preventDefault e)) (.preventDefault e))
(when can-submit (if (and schema (not (m/validate schema data)))
(re-frame/dispatch-sync (vec (conj submit-event {})))))} (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)))}] (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))] (let [[child] (r/children (r/current-component))]
[:> Consumer {} (consume Consumer
(fn [consume-form] ["visited" "attempted-submit?" "data" "on-change" "change-event" "blur-event" "problems"]
(r/as-element (fn [visited attempted-submit? data on-change change-event blur-event problems]
[:> FormScopeConsumer {} (consume FormScopeConsumer
(fn [form-scope] ["scope"]
(r/as-element (fn [scope]
[bind-field (-> child (update child 1 (fn [child-props]
(update-in [1 :field] (fn [f] (let [scope (or scope [])
(cond full-field-path (cond
(sequential? f) (sequential? field)
(into form-scope f) (into scope field)
f field
(conj form-scope f) (conj scope field)
:else :else
nil))) nil)
visited? (get visited full-field-path)
(assoc-in [1 :subscription] (aget consume-form "data")) value (get-in data full-field-path)]
(assoc-in [1 :event] (aget consume-form "change-event")))]))]))])) (-> 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]}] (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/as-element (into [:<>]
(r/children (r/current-component)))))) (r/children (r/current-component))))))
(defn vertical-control [{:keys [is-small? required?]}] (defn vertical-control [{:keys [is-small? required?]}]
(let [[label & children] (r/children (r/current-component))] (let [[label & children] (r/children (r/current-component))]
[:> Consumer {} (consume Consumer
(fn [consume] ["fullwidth?"]
(r/as-element (fn [fullwidth?]
[:div.field [:div.field
(if (aget consume "fullwidth?") (if fullwidth?
[:p.help label] [:p.help label]
[:label.label [:label.label
(if required? (if required?
[:span label [:span.has-text-danger " *"]] [:span label [:span.has-text-danger " *"]]
label)]) label)])
(into [:div.control ] children)]))])) (into [:div.control ] children)]))))
(defn field [] (defn field-v2 []
(let [props (r/props (r/current-component)) (let [props (r/props (r/current-component))
[label child] (r/children (r/current-component))] [label child] (r/children (r/current-component))]
[:> Consumer {} (consume Consumer
(fn [consume] ["fullwidth?"]
(r/as-element (fn [fullwidth?]
[:div.field [:div.field
(when label (when label
(if (aget consume "fullwidth?") (if fullwidth?
[:p.help label] [:p.help label]
[:label.label [:label.label
(if (:required? props) (if (:required? props)
[:span label [:span.has-text-danger " *"]] [:span label [:span.has-text-danger " *"]]
label)])) label)]))
[:div.control [raw-field {} child]]]))])) [:div.control [raw-field-v2 props child]]
[:div
(defn horizontal-control [] [raw-error-v2 {:field (:field props)}]]]))))
(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]]))
(defn section [{:keys [title]}] (defn section [{:keys [title]}]
[:<> [:<>
@@ -123,36 +251,37 @@
(defn submit-button [{:keys [class]}] (defn submit-button [{:keys [class]}]
(let [[child] (r/children (r/current-component))] (let [[child] (r/children (r/current-component))]
[:> Consumer {} (consume
(fn [consume] Consumer
(let [status (aget consume "status") ["status" "can-submit" "fullwidth?"]
can-submit (aget consume "can-submit") (fn [status can-submit fullwidth?]
fullwidth? (aget consume "fullwidth?")] [:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
(r/as-element (not can-submit))
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status) :class (cond-> (or class [])
(not can-submit)) (status/class-for status) (into (status/class-for status))
:class (cond-> (or class []) fullwidth? (conj "is-fullwidth")) }
(status/class-for status) (conj (status/class-for status)) child]))))
fullwidth? (conj "is-fullwidth")) }
child])))]))
(defn hidden-submit-button [] (defn hidden-submit-button []
[:> Consumer {} (consume Consumer ["status" "can-submit"]
(fn [consume] (fn [status can-submit]
(let [status (aget consume "status") [:div {:style {:display "none"}}
can-submit (aget consume "can-submit")] [:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
(r/as-element (not can-submit))}]])))
[:div {:style {:display "none"}}
[:button.button.is-medium.is-primary {:disabled (or (status/disabled-for status)
(not can-submit))}]])))])
(defn error-notification [] (defn error-notification []
(let [[child] (r/children (r/current-component))] (consume Consumer ["error" "status"]
[:> Consumer {} (fn [error status]
(fn [consume] (println status)
(r/as-element (cond error
(when-let [error (aget consume "error")] ^{:key error}
^{:key error} [:div.has-text-danger.animated.fadeInUp {} 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]] [cemerick.url :refer [url]]
[re-frame.core :as re-frame])) [re-frame.core :as re-frame]))
(defn- parse-url [url] (defn parse-url [url]
(println "parsing url" url) (println "parsing url" url)
(bidi/match-route routes/routes url)) (bidi/match-route routes/routes url))
(defn- dispatch-route [matched-route] (defn dispatch-route [matched-route]
(println "Matched 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)])) (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 (ns auto-ap.subs
(:require [re-frame.core :as re-frame] (:require [re-frame.core :as re-frame]
[auto-ap.utils :refer [by]] [auto-ap.utils :refer [by]]
[auto-ap.views.utils :refer [parse-jwt]]
[clojure.string :as str] [clojure.string :as str]
[goog.crypt.base64 :as base64] [goog.crypt.base64 :as base64]
[minisearch :as ms])) [minisearch :as ms]))
@@ -22,6 +23,12 @@
(when (:user db) (when (:user db)
(sort-by :name (vals (:clients 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 (re-frame/reg-sub
::all-accounts ::all-accounts
(fn [db] (fn [db]
@@ -135,8 +142,7 @@
(re-frame/reg-sub (re-frame/reg-sub
::user ::user
(fn [db] (fn [db]
(when (:user db) (parse-jwt (:user db))))
(js->clj (.parse js/JSON (base64/decodeString (second (str/split (:user db) #"\.")))) :keywordize-keys true))))
(re-frame/reg-sub (re-frame/reg-sub
::active-route ::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 (ns auto-ap.views.components.address
(:require [auto-ap.entities.address :as address] (:require
[auto-ap.views.utils :refer [dispatch-value-change dispatch-event bind-field horizontal-field]] [auto-ap.entities.address :as address]
[auto-ap.forms.builder :as form-builder])) [auto-ap.forms.builder :as form-builder]
[auto-ap.views.components.level :as level]))
(defn address-field [{:keys [event field subscription]}] (defn address2-field [{:keys [value on-change]}]
[:span [form-builder/virtual-builder {:value (or value {})
[horizontal-field :on-change on-change}
nil [:div
[:div.control [form-builder/field-v2 {:field :street1}
[: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
[:p.help "Street Address"] [:p.help "Street Address"]
[form-builder/raw-field [:input.input.is-expanded {:type "text"
[:input.input.is-expanded {:type "text" :placeholder "1700 Pennsylvania Ave"}]]
:placeholder "1700 Pennsylvania Ave" [form-builder/raw-field-v2 {:field :street2}
:field [:street1] [:input.input.is-expanded {:type "text"
:spec ::address/street1}]]]] :placeholder "Suite 400"}]]
[level/left-stack
[horizontal-field [form-builder/field-v2 {:field :city}
nil [:p.help "City"]
[:div.control [:input.input.is-expanded {:type "text"
[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"
:placeholder "Cupertino" :placeholder "Cupertino"
:field [:city] :field [:city]
:spec ::address/city}]]] :spec ::address/city}]]
[:div.control [form-builder/field-v2 {:field :state}
[:p.help "State"] [:p.help "State"]
[form-builder/raw-field [:input.input {:type "text"
[:input.input {:type "text" :placeholder "CA"
:placeholder "CA" :size 2
:field [:state] :max-length "2"}]]
:spec ::address/state [form-builder/field-v2 {:field :zip}
:size 2 [:p.help "Zip"]
:max-length "2"}]]] [:input.input {:type "text"
[:div.control :placeholder "95014"}]]]]])
[:p.help "Zip"]
[form-builder/raw-field
[:input.input {:type "text"
:field [:zip]
:spec ::address/zip
:placeholder "95014"}]]]]])

View File

@@ -1,15 +1,11 @@
(ns auto-ap.views.components.admin.side-bar (ns auto-ap.views.components.admin.side-bar
(:require [re-frame.core :as re-frame] (:require
[reagent.core :as r] [auto-ap.routes :as routes]
[clojure.string :as str] [auto-ap.subs :as subs]
[clojure.spec.alpha :as s] [auto-ap.views.utils :refer [active-when]]
[cljs-time.core :as c] [bidi.bidi :as bidi]
[goog.string :as gstring] [re-frame.core :as re-frame]
[bidi.bidi :as bidi] [reagent.core :as r]))
[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]))
(defn admin-side-bar [params ] (defn admin-side-bar [params ]
(let [ap @(re-frame/subscribe [::subs/active-page])] (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 "icon icon-saving-bank-1" :style {:font-size "25px"}}]
[:span {:class "name"} "Yodlee 2 Link"]]] [: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 ]] [: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"] [:p.menu-label "Import"]
[:ul.menu-list [:ul.menu-list
[:li.menu-item [:li.menu-item

View File

@@ -1,8 +1,6 @@
(ns auto-ap.views.components.bank-account-filter (ns auto-ap.views.components.bank-account-filter
(:require (:require
[clojure.spec.alpha :as s] [auto-ap.views.utils :refer [->$]]
[auto-ap.entities.invoice :as invoice]
[auto-ap.views.utils :refer [bind-field ->$]]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[re-frame.core :as re-frame])) [re-frame.core :as re-frame]))

View File

@@ -1,57 +1,51 @@
(ns auto-ap.views.components.date-range-filter (ns auto-ap.views.components.date-range-filter
(:require (:require
[clojure.spec.alpha :as s] [auto-ap.views.utils :refer [date-picker date->str local-now standard]]
[auto-ap.entities.invoice :as invoice]
[auto-ap.views.utils :refer [bind-field date-picker-optional date->str local-now standard]]
[cljs-time.core :as t] [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 [_] (defn set-value [on-change-event v]
(re-frame/dispatch (into on-change-event [[:start] start]) ) (re-frame/dispatch (conj on-change-event v)))
(re-frame/dispatch (into on-change-event [[:end] end]))))
(defn date-range-filter [{:keys [value on-change-event]}] (defn date-range-filter [{:keys [value on-change-event]}]
[:div [form-builder/virtual-builder {:value (or value {})
[:div.field.has-addons :on-change (fn [v]
[:p.control [:a.button.is-small {:on-click (set-value on-change-event v))}
(dispatch-change on-change-event
(date->str (t/minus (local-now) (t/period :days 7)) standard) [:div
(date->str (local-now) standard))} [:div.field.has-addons
"Week" ]] [:p.control [:a.button.is-small {:on-click
[:p.control [:a.button.is-small {:on-click #(set-value on-change-event
(dispatch-change on-change-event {:start (date->str (t/minus (local-now) (t/period :days 7)) standard)
(date->str (t/minus (local-now) (t/period :months 1)) standard) :end (date->str (local-now) standard)})}
(date->str (local-now) standard))} "Week" ]]
"Month" ]] [:p.control [:a.button.is-small {:on-click
[:p.control [:a.button.is-small {:on-click #(set-value on-change-event
(dispatch-change on-change-event {:start (date->str (t/minus (local-now) (t/period :months 1)) standard)
(date->str (t/minus (local-now) (t/period :years 1)) standard) :end (date->str (local-now) standard)})}
(date->str (local-now) standard))} "Month" ]]
"Year"]] [:p.control [:a.button.is-small {:on-click
[:p.control [:a.button.is-small {:on-click
(dispatch-change on-change-event #(set-value on-change-event
nil {:start (date->str (t/minus (local-now) (t/period :years 1)) standard)
nil)} :end (date->str (local-now) standard)})}
"All"]]] "Year"]]
[:div.field.has-addons [:p.control [:a.button.is-small {:on-click
[:div.control #(set-value on-change-event nil)}
[bind-field "All"]]]
[date-picker-optional [:div.field.has-addons
{:event on-change-event [:div.control
:type "date2"
:placeholder "Start" [form-builder/raw-field-v2 {:field :start}
:class "is-small" [date-picker
:field [:start] {:placeholder "Start"
:subscription value :class "is-small"
:output :text}]]] :output :text}]]]
[:div.control [:div.control
[bind-field [form-builder/raw-field-v2 {:field :end}
[date-picker-optional [date-picker
{:event on-change-event {:class "is-small"
:type "date2" :placeholder "End"
:class "is-small" :output :text}]]]]]])
:placeholder "End"
:field [:end]
:subscription value
:output :text}]]]]])

View File

@@ -2,7 +2,6 @@
(:require (:require
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs]
[auto-ap.utils :refer [by]] [auto-ap.utils :refer [by]]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]] [auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]]
@@ -10,17 +9,14 @@
[auto-ap.views.utils :refer [dispatch-event with-user]] [auto-ap.views.utils :refer [dispatch-event with-user]]
[clojure.string :as str] [clojure.string :as str]
[goog.string :as gstring] [goog.string :as gstring]
[re-frame.core :as re-frame])) [re-frame.core :as re-frame]
[auto-ap.forms.builder :as form-builder]
(re-frame/reg-sub [auto-ap.views.components :as com]))
::can-submit
(fn [db]
true))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::try-save ::try-save
[(forms/in-form ::form)] [(forms/in-form ::form)]
(fn [{:keys [db]} [_ id ]] (fn [{:keys [db]} _]
(let [{{:keys [ total]} :invoice (let [{{:keys [ total]} :invoice
:keys [expense-accounts]} (:data db) :keys [expense-accounts]} (:data db)
expense-accounts (vals expense-accounts) expense-accounts (vals expense-accounts)
@@ -33,6 +29,7 @@
%)) %))
(reduce + 0)) (reduce + 0))
does-add-up? (< (Math/abs (- expense-accounts-total (js/parseFloat total))) 0.001)] does-add-up? (< (Math/abs (- expense-accounts-total (js/parseFloat total))) 0.001)]
(if (and does-add-up? (if (and does-add-up?
(every? :new-amount expense-accounts)) (every? :new-amount expense-accounts))
@@ -44,7 +41,7 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::save ::save
[with-user (forms/in-form ::form)] [with-user (forms/in-form ::form)]
(fn [{:keys [db user] } [_ id]] (fn [{:keys [db user] } _]
(let [{{:keys [id]} :invoice (let [{{:keys [id]} :invoice
:keys [expense-accounts]} (:data db) :keys [expense-accounts]} (:data db)
expense-accounts (vals expense-accounts)] expense-accounts (vals expense-accounts)]
@@ -85,16 +82,10 @@
(fn [db [_ x]] (fn [db [_ x]]
(update-in db [:data :expense-accounts] dissoc 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 [] (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) expense-accounts (:expense-accounts data)
{:keys [total] :or {total 0} {:keys [locations] :as client} :client} (:invoice 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) multi-location? (> (count locations) 1)
expense-accounts-total (->> expense-accounts expense-accounts-total (->> expense-accounts
vals vals
@@ -108,72 +99,71 @@
[:div [:div
[:div [:div
[:a.button.is-outlined {:on-click (dispatch-event [::add-split])} "Add split"]] [:a.button.is-outlined {:on-click (dispatch-event [::add-split])} "Add split"]]
(form-inline {} [form-builder/builder {:submit-event [::try-save]
[:table.table :id ::form}
[:thead [:table.table
[:tr [:thead
[:th {:style {:width "500px"}} "Expense Account"] [:tr
(when multi-location? [:th {:style {:width "500px"}} "Expense Account"]
[:th {:style {:width "200px"}} "Location"]) (when multi-location?
[:th {:style {:width "200px"}} "Original Amount"] [:th {:style {:width "200px"}} "Location"])
[:th {:style {:width "300px"}} "Amount"] [:th {:style {:width "200px"}} "Original Amount"]
[:th {:style {:width "5em"}}]]] [:th {:style {:width "300px"}} "Amount"]
[:tbody [:th {:style {:width "5em"}}]]]
(doall (for [[id expense-account] expense-accounts] [:tbody
^{:key id} (doall (for [[id _] expense-accounts]
[:tr ^{:key id}
[:td.expandable [:div.control [:tr
(raw-field [:td.expandable [:div.control
[search-backed-typeahead {:search-query (fn [i] [form-builder/raw-field-v2 {:field [:expense-accounts id :account]}
[:search_account [search-backed-typeahead {:search-query (fn [i]
{:query i [:search_account
:client-id (:id client)} {:query i
[:name :id :location]]) :client-id (:id client)}
:type "typeahead-v3" [:name :id :location]])}]]]]
:field [:expense-accounts id :account]}])]]
(when multi-location? (when multi-location?
[:td [:td
(if-let [forced-location (get-in expense-accounts [id :account :location])] (if-let [forced-location (get-in expense-accounts [id :account :location])]
[:div.select [:div.select
[:select {:disabled "disabled" :value forced-location} [:option {:value forced-location} forced-location]]] [:select {:disabled "disabled" :value forced-location} [:option {:value forced-location} forced-location]]]
[:div.select [:div.select
(raw-field
[:select {:type "select" [form-builder/raw-field-v2 {:field [:expense-accounts id :location]}
:field [:expense-accounts id :location] [com/select-field {:options (map (fn [l] [l l])
:spec (set locations)} locations)
(map (fn [l] ^{:key l} [:option {:value l} l]) locations)])])]) :allow-nil? true}]]])])
[:td [:td
(str "$" (get-in expense-accounts [id :amount]))] (str "$" (get-in expense-accounts [id :amount]))]
[:td [:td
[:div.control [:div.control
[:div.field.has-addons.is-extended [:div.field.has-addons.is-extended
[:p.control [:a.button.is-static "$"]] [:p.control [:a.button.is-static "$"]]
[:p.control [:p.control
(raw-field [form-builder/raw-field-v2 {:field [:expense-accounts id :new-amount-temp]}
[:input.input {:type "number" [:input.input {:type "number"
:field [:expense-accounts id :new-amount-temp] :style {:text-align "right"}
: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-blur (dispatch-event [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])]) :on-key-down (fn [e ]
:on-key-down (fn [e ] (if (= 13 (.-keyCode e))
(if (= 13 (.-keyCode e)) (do
(do (re-frame/dispatch [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])])
(re-frame/dispatch [::forms/change ::form [:expense-accounts id :new-amount] (get-in expense-accounts [id :new-amount-temp])]) true)
true) false))
false)) :max (:total data)
:max (:total data) :step "0.01"}]]]]]]
:step "0.01"}])]]]] [:td [:a.button {:on-click (dispatch-event [::remove-expense-account-split id])} [:i.fa.fa-times]]]]))
[:td [:a.button {:on-click (dispatch-event [::remove-expense-account-split id])} [:i.fa.fa-times]]]])) [:tr
[:tr [:td.no-border { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Invoice total: "]
[: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 ) )]]
[:td.no-border { :style { :text-align "right"} } (str (gstring/format "$%.2f" total ) )]] [:tr
[:tr [:td { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Account total: "]
[: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 ) )]]
[:td { :style { :text-align "right"} } (str (gstring/format "$%.2f" expense-accounts-total ) )]] [:tr
[:tr [:td.no-border { :col-span (if multi-location? "3" "2") :style { :text-align "right"} } "Difference: "]
[: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) ) )]]]]
[: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 (re-frame/reg-event-fx
::show ::show
@@ -184,11 +174,10 @@
:status-from [::status/single ::form] :status-from [::status/single ::form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::try-save]) :on-click (dispatch-event [::try-save])
:can-submit [::can-submit]
:close-event [::status/completed ::form]}}] :close-event [::status/completed ::form]}}]
:db (-> db :db (-> db
(forms/start-form ::form (forms/start-form ::form
{:expense-accounts (by :id {:expense-accounts (by :id
(:expense-accounts i)) (:expense-accounts i))
:invoice i}))})) :invoice i}))}))

View File

@@ -1,10 +1,18 @@
(ns auto-ap.views.components.expense-accounts-field (ns auto-ap.views.components.expense-accounts-field
(:require (:require
[auto-ap.views.utils :refer [->$ bind-field dispatch-event]] [auto-ap.forms.builder :as form-builder]
[auto-ap.views.components.typeahead.vendor :refer [search-backed-typeahead]] [auto-ap.schema :as schema]
[clojure.string :as str] [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] [goog.string :as gstring]
[re-frame.core :as re-frame])) [malli.core :as m]))
(defn can-replace-with-default? [accounts] (defn can-replace-with-default? [accounts]
(and (or (not (seq accounts)) (and (or (not (seq accounts))
@@ -46,27 +54,6 @@
;; EVENTS ;; 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] (defn recalculate-amounts [expense-accounts total]
(mapv (mapv
(fn [ea] (fn [ea]
@@ -76,143 +63,116 @@
(* (/ (js/parseFloat (:amount-percentage ea)) 100.0) total))))) (* (/ (js/parseFloat (:amount-percentage ea)) 100.0) total)))))
expense-accounts)) 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 ;; 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 (def schema (m/schema [:sequential [:map
[:div.column [:id :string]
[:h1.subtitle.is-4.is-inline (str/capitalize descriptor) "s"] [: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? (when-not percentage-only?
[:p.help "Remaining " (->$ (- max-value (reduce + 0 (map (comp js/parseFloat :amount) expense-accounts))))])] (let [total (reduce + 0 (map (or :amount 0.0) expense-accounts))]
[:div.column.is-narrow [:<>
(when-not disabled [:div.tag "Total: " (->$ total) ]
[:p.buttons [:div.tag {:class (if (dollars-0? (- max-value total))
[:a.button {:on-click (dispatch-event [::spread-evenly event expense-accounts max-value])} "Spread evenly"] ["is-primary" "is-light"]
[:a.button {:on-click (dispatch-event [::add-expense-account event expense-accounts locations])} "Add"]])]] ["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)] (into [appearing-group]
^{:key id} (for [[index {:keys [account id amount amount-mode]}] (map vector (range) expense-accounts)]
[:div.box ^{:key id}
[:div.columns [:div.card {:style {:margin-bottom "2em"}}
[:div.column [:div.card-header
[:h1.subtitle.is-6 (cond (and account (not percentage-only?)) [:p.card-header-title "Expense Account"]
(str (:name account) " - " (when-not disabled
location ": " [:div.card-header-icon {:on-click (fn []
(gstring/format "$%.2f" (or amount 0) )) (on-change (into [] (filter #(not= id (:id %)) expense-accounts))))}
[:a.delete ]])]
account [:div.card-content
(str (:name account) " - " [:div.field
location ": %" [:div.columns
amount-percentage) [:div.column
[:div.control.is-fullwidth
:else [form-builder/field-v2 {:required? true
[:i "New " descriptor])]] :field [index :account]}
[:div.column.is-narrow "Account"
(when-not disabled [search-backed-typeahead {:search-query (fn [i]
[:a.delete {:on-click (dispatch-event [::remove-expense-account event expense-accounts id])}])]] [:search_account
{:query i
[:div.field :client-id (:id client)}
[:div.columns [:name :id :location]])
[:div.column :disabled disabled}]]]]
[:p.help "Account"] [:div.column.is-narrow
[:div.control.is-fullwidth [form-builder/field-v2 {:required? true
[bind-field :field [index :location]}
^{:key (:id client)} "Location"
[search-backed-typeahead {:search-query (fn [i] [com/select-field {:options (if (:location account)
[:search_account [[(:location account) (:location account)]]
{:query i (map (fn [l] [l l])
:client-id (:id client)} locations))
[:name :id :location]]) :disabled (boolean (:location account))
:type "typeahead-v3" :allow-nil? true}]]]]]
:field [index :account] (if percentage-only?
[form-builder/raw-field-v2 {:field [index :amount-percentage]}
:disabled disabled [percentage-field {}]]
:event [::expense-account-changed event expense-accounts max-value] [left-stack
:subscription expense-accounts}]]]] [:div.field.has-addons
[:div.column.is-narrow [form-builder/raw-field-v2 {:field [index :amount-mode]}
[:p.help "Location"] [button-radio/button-radio {:options [["$" "Amount"]
[:div.control ["%" "Percent"]]}]]
(if-let [forced-location (:location account)] (if (= "$" amount-mode)
[:div.select [form-builder/raw-field-v2 {:field [index :amount]}
[:select {:disabled "disabled" :style {:width "5em"} :value forced-location} [:option {:value forced-location} forced-location]]] [money-field {}]
[:div.select ]
[bind-field [form-builder/raw-field-v2 {:field [index :amount-percentage]}
[:select {:type "select" [percentage-field {}]])]
:disabled (boolean (or (:location account) (when (= "%" amount-mode)
disabled)) [:div.tag.is-primary.is-light (gstring/format "$%.2f" (or amount 0) )])])]]))
:style {:width "5em"} (when-not disabled
:field [index :location] [:p.buttons
:allow-nil? true [:a.button {:on-click (fn []
:spec (set locations) (on-change
:event [::expense-account-changed event expense-accounts max-value] (recalculate-amounts (mapv
:subscription expense-accounts} (fn [ea]
(map (fn [l] ^{:key l} [:option {:value l} l]) locations)]]])]]]] (assoc ea :amount-percentage (* 100.0 (/ 1 (count expense-accounts)))))
expense-accounts)
[:div.field max-value))
[:p.help "Amount"] )} "Spread evenly"]
[:div.control [:a.button {:on-click
[:div.field.has-addons.is-extended (fn []
[:p.control [:span.select (on-change (conj value {:id (str "new-" (random-uuid))
[bind-field :amount-mode "%"
[:select {:type "select" :location (if (= 1 (count locations))
:disabled (or disabled percentage-only?) (first locations)
:field [index :amount-mode] nil)})))}
:allow-nil? false "Add"]])]])
: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"}]])]]]]])])

View File

@@ -20,28 +20,31 @@
[auto-ap.views.pages.data-page :as data-page])) [auto-ap.views.pages.data-page :as data-page]))
(defn data-params->query-params [params] (defn data-params->query-params [params]
{:exact-match-id (some-> params :exact-match-id str) (if (:exact-match-id params)
:start (:start params 0) {:exact-match-id (some-> params :exact-match-id str)
:sort (:sort params) :client-id (:id @(re-frame/subscribe [::subs/client]))}
:per-page (:per-page params) {: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)) :vendor-id (:id (:vendor params))
:date-range (:date-range params) :date-range (:date-range params)
:due-range (:due-range params) :due-range (:due-range params)
:amount-gte (:amount-gte (:amount-range params)) :amount-gte (:amount-gte (:amount-range params))
:amount-lte (:amount-lte (:amount-range params)) :amount-lte (:amount-lte (:amount-range params))
:location (:location params) :location (:location params)
:unresolved (:unresolved params) :unresolved (:unresolved params)
:scheduled-payments (:scheduled-payments params) :scheduled-payments (:scheduled-payments params)
:invoice-number-like (:invoice-number-like params) :invoice-number-like (:invoice-number-like params)
:client-id (:id @(re-frame/subscribe [::subs/client])) :client-id (:id @(re-frame/subscribe [::subs/client]))
:import-status (:import-status params) :import-status (:import-status params)
:status (condp = @(re-frame/subscribe [::subs/active-page]) :status (condp = @(re-frame/subscribe [::subs/active-page])
:invoices nil :invoices nil
:import-invoices nil :import-invoices nil
:unpaid-invoices :unpaid :unpaid-invoices :unpaid
:paid-invoices :paid :paid-invoices :paid
:voided-invoices :voided)}) :voided-invoices :voided)}))
(defn query [params] (defn query [params]
{:venia/queries [[:invoice_page {:venia/queries [[:invoice_page

View File

@@ -3,44 +3,53 @@
[auto-ap.events :as events] [auto-ap.events :as events]
[auto-ap.routes :as routes] [auto-ap.routes :as routes]
[auto-ap.subs :as subs] [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.modal :as modal]
[auto-ap.views.components.vendor-dialog :as vendor-dialog] [auto-ap.views.components.vendor-dialog :as vendor-dialog]
[auto-ap.views.utils [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] [bidi.bidi :as bidi]
[clojure.string :as str] [clojure.string :as str]
[re-frame.core :as re-frame] [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 ] (defn navbar-drop-down [{:keys [ class]} _]
(let [toggle-fn (fn [] (re-frame/dispatch [::events/toggle-menu id]))] (let [!child (r/atom nil)]
(r/create-class {:component-did-mount (fn [] (.addEventListener js/document "click" toggle-fn)) (r/create-class
:component-will-unmount (fn [] (.removeEventListener js/document "click" toggle-fn)) {:reagent-render (fn [{:keys [header id]} child]
:reagent-render (let [menu-active? @(re-frame/subscribe [::subs/menu-active? id])]
(fn [children] [:div { :class (str "navbar-item has-dropdown " (when menu-active? "is-active " ) " " class)
children)}))) :ref (fn [n]
(reset! !child n))
(defn navbar-drop-down [{:keys [ header id class]} child] :tab-index 0
(r/create-class :onBlur (fn [e]
{:reagent-render (fn [{:keys [header id]} child] (js/setTimeout (fn []
(let [menu-active? @(re-frame/subscribe [::subs/menu-active? id])] (println @!child)
[:div { :class (str "navbar-item has-dropdown " (when menu-active? "is-active " ) " " class)} (println (.-activeElement js/document))
[:a {:class "navbar-link login" :on-click (fn [e] (when-not (.contains @!child (.-activeElement js/document))
(.preventDefault e) (re-frame/dispatch [::events/toggle-menu id])))
(.stopPropagation e) 2))
(re-frame/dispatch [::events/toggle-menu id]) }
true)} header] [:a {:class "navbar-link login" :on-click (fn [e]
[appearing {:visible? menu-active? :enter-class "appear" :exit-class "disappear" :timeout 200} (.preventDefault e)
[:div {:class "navbar-dropdown"} (.stopPropagation e)
[navbar-drop-down-contents {:id id} (re-frame/dispatch [::events/toggle-menu id])
[:div child]]]]]))})) true)} header]
[appearing {:visible? menu-active? :enter-class "appear" :exit-class "disappear" :timeout 200}
[:div {:class "navbar-dropdown"}
[:div child]]]]))})))
(defn login-dropdown [] (defn login-dropdown []
(let [user (re-frame/subscribe [::subs/user])] (let [user (re-frame/subscribe [::subs/user])]
(if @user (if @user
[navbar-drop-down {:header [:span [:span.icon [:i.fa.fa-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 [:div
[:a {:class "navbar-item" [:a {:class "navbar-item"
:href (bidi/path-for routes/routes :reports)} "My company"] :href (bidi/path-for routes/routes :reports)} "My company"]
@@ -60,8 +69,8 @@
(re-frame/reg-sub (re-frame/reg-sub
::matching-clients ::matching-clients
:<- [::subs/clients] :<- [::subs/clients]
:<- [::client-search] :<- [::forms/form ::client-search]
(fn [[clients {client-search :value}]] (fn [[clients {{client-search :value} :data}]]
(if (empty? client-search) (if (empty? client-search)
clients clients
(if-let [exact-match (first (filter (if-let [exact-match (first (filter
@@ -76,21 +85,46 @@
(str/includes? (str/lower-case (:name client)) (str/lower-case client-search)))) (str/includes? (str/lower-case (:name client)) (str/lower-case client-search))))
clients))))) clients)))))
(re-frame/reg-event-db
::client-search-changed (re-frame/reg-event-fx
[(re-frame/path [::client-search])] ::client-searched
(fn [client-search [_ path value]] [(re-frame/inject-cofx ::inject/sub [::matching-clients])]
(assoc-in client-search path value))) (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] (defn navbar [ap]
(let [navbar-menu-shown? (r/atom false)] (let [navbar-menu-shown? (r/atom false)]
(fn [ap] (fn [ap]
(let [user (re-frame/subscribe [::subs/user]) (let [user (re-frame/subscribe [::subs/user])
client (re-frame/subscribe [::subs/client]) clients (re-frame/subscribe [::subs/clients])
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])
is-initial-loading @(re-frame/subscribe [::subs/is-initial-loading?])] is-initial-loading @(re-frame/subscribe [::subs/is-initial-loading?])]
[:nav {:class "navbar has-shadow is-fixed-top is-grey"} [:nav {:class "navbar has-shadow is-fixed-top is-grey"}
@@ -133,33 +167,8 @@
[:div.navbar-end [:div.navbar-end
(when (> (count @clients) 1) (when (> (count @clients) 1)
[navbar-drop-down {:header (str "Company: " (if @client (:name @client) [client-dropdown]
"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])]])])]
(when-not is-initial-loading (when-not is-initial-loading
[login-dropdown])] [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?]} ] (defn appearing-side-bar [{:keys [visible?]} ]
[appearing {:visible? visible? :enter-class "slide-in-right" :exit-class "slide-out-right" :timeout 500} [appearing {:visible? visible? :enter-class "slide-in-right" :exit-class "slide-out-right" :timeout 500}
@@ -179,7 +184,7 @@
(into [:div.sub-main {} ] (into [:div.sub-main {} ]
(r/children (r/current-component)))]]) (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]) (let [ap @(re-frame/subscribe [::subs/active-page])
client @(re-frame/subscribe [::subs/client])] client @(re-frame/subscribe [::subs/client])]
[:div [:div
@@ -200,7 +205,6 @@
(when right-side-bar (when right-side-bar
right-side-bar) right-side-bar)
] ]
#_[footer]
[:div [:div
bottom] bottom]
[:div#dz-hidden]])) [:div#dz-hidden]]))

View File

@@ -5,7 +5,7 @@
[react :as react])) [react :as react]))
(def good-$ #"^\-?[0-9]+(\.[0-9][0-9])?$") (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 (let [[ parsed-amount set-parsed-amount] (react/useState {:parsed value
:raw (cond :raw (cond
(str/blank? value) (str/blank? value)
@@ -40,6 +40,7 @@
[:div.control.has-icons-left [:div.control.has-icons-left
[:input.input {:type "text" [:input.input {:type "text"
:disabled disabled :disabled disabled
:placeholder placeholder
:class class :class class
:on-change (fn [e] :on-change (fn [e]
(let [raw (.. e -target -value) (let [raw (.. e -target -value)
@@ -58,7 +59,9 @@
(set-parsed-amount {:raw "" (set-parsed-amount {:raw ""
:parsed nil}) :parsed nil})
(on-change nil))) (on-change nil))
(when on-blur
(on-blur)))
:min min :min min
:max max :max max
:step "0.01" :step "0.01"
@@ -69,3 +72,6 @@
(defn money-field [] (defn money-field []
[:f> -money-field (r/props (r/current-component))]) [: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 (ns auto-ap.views.components.number-filter
(:require (:require
[clojure.spec.alpha :as s] [re-frame.core :as re-frame]
[auto-ap.entities.invoice :as invoice] [auto-ap.forms.builder :as form-builder]
[auto-ap.views.utils :refer [bind-field date-picker date->str local-now standard]] [auto-ap.views.components :as com]))
[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]}] (defn number-filter [{:keys [value on-change-event]}]
[:div.field [form-builder/virtual-builder {:value (or value {})
[:div.control :on-change (fn [v]
[:div.columns (re-frame/dispatch (conj on-change-event v)))}
[:div.column
[bind-field [:div.columns
[:input.input {:type "number" [:div.column
:placeholder ">=" [:div.control
:field [:amount-gte] [form-builder/raw-field-v2 {:field :amount-gte}
:step "0.01" [com/money-input {:placeholder ">="}]]]]
:event on-change-event [:div.column
:subscription value}]]] [:div.control
[:div.column [form-builder/raw-field-v2 {:field :amount-lte}
[bind-field [com/money-input {:placeholder "<="}]]]]]]
[:input.input {:type "number"
:placeholder "<="
:field [:amount-lte]
:event on-change-event
:step "0.01"
:subscription value}]]]]]]
) )

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]] [downshift :as ds :refer [useCombobox]]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[auto-ap.views.utils :refer [with-user]] [auto-ap.views.utils :refer [with-user]]
[react-popper :refer [usePopper] :as react-popper]
[reagent.core :as r]
[clojure.string :as str] [clojure.string :as str]
[react :as react])) [react :as react]))
(set! *warn-on-infer* true) (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 (re-frame/reg-event-fx
::search-completed ::search-completed
(fn [_ [_ set-items set-loading-status result]] (fn [_ [_ set-items set-loading-status result]]
@@ -61,9 +83,8 @@
:time 250 :time 250
:key ::input-value-settled}}))) :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} (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 prop-value :value}]
:as i}]
(let [[items set-items] (react/useState (or entities (let [[items set-items] (react/useState (or entities
[])) []))
[focused set-focus] (react/useState (boolean auto-focus)) [focused set-focus] (react/useState (boolean auto-focus))
@@ -113,13 +134,16 @@
focused focused
(conj "is-focused") (conj "is-focused")
)} )
}
(when selectedItem (when selectedItem
^{:key "hidden"} ^{:key "hidden"}
[:div.level-item [:div.level-item
[:div.control [:div.control
[:div.tags.has-addons [:div.tags.has-addons
[:span.tag (:name selectedItem)] [:span.tag (if entity->text
(entity->text selectedItem)
(:name selectedItem))]
(when name (when name
[:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}]) [:input {:type "hidden" :name name :value (:id (js->clj selectedItem :keywordize-keys true))}])
(when-not disabled (when-not disabled
@@ -142,22 +166,25 @@
:disabled disabled :disabled disabled
:onFocus #(set-focus true) :onFocus #(set-focus true)
:onBlur #(set-focus false) :onBlur #(do (set-focus false)
(when on-blur
(on-blur)))
:autoFocus (if auto-focus :autoFocus (if auto-focus
"autoFocus" "autoFocus"
"")}))]]] "")}))]]]
[:div {:class (when (and isOpen (seq items)) [:div (js->clj (getMenuProps))
"typeahead-menu")} (when (and isOpen (seq items))
[:ul (js->clj (getMenuProps)) [popper {:class "typeahead-menu"}
(when (and isOpen (seq items)) [:ul
(for [[index item] (map vector (range) (js->clj items :keywordize-keys true))] (when (and isOpen (seq items))
^{:key item} (for [[index item] (map vector (range) (js->clj items :keywordize-keys true))]
[:li.typeahead-suggestion (assoc (js->clj (getItemProps #js {:item item :index index})) ^{:key item}
:class (when (= index highlightedIndex) [:li.typeahead-suggestion (assoc (js->clj (getItemProps #js {:item item :index index}))
"typeahead-highlighted")) :class (when (= index highlightedIndex)
(if entity->text "typeahead-highlighted"))
(entity->text item) (if entity->text
(:name item))]))]]]])) (entity->text item)
(:name item))]))]])]]]))
(defn search-backed-typeahead [{:keys [search-query] :as props}] (defn search-backed-typeahead [{:keys [search-query] :as props}]
[:div [:div
@@ -174,11 +201,10 @@
:on-input-change :on-input-change
(fn [input set-items] (fn [input set-items]
(if entities-by-id (if entities-by-id
(do (set-items
(set-items (into-array
(into-array (->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} )
(->> (.search entity-index (or (aget input "inputValue") "") #js {:fuzzy 0.2} ) (take 10))))
(take 10)))))
(set-items (into-array (set-items (into-array
(take 10 (filter (fn [x] (str/includes? (or (some-> (entity->text x) str/lower-case) "") (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 (ns auto-ap.views.components.vendor-dialog
(:require (:require
[auto-ap.entities.contact :as contact]
[auto-ap.entities.vendors :as entity]
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder] [auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.views.components.level :refer [left-stack]]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.address :refer [address2-field]] [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.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 :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor [auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]] :refer [search-backed-typeahead]]
[auto-ap.views.pages.admin.vendors.common :as common] [auto-ap.views.pages.admin.vendors.common :as common]
[auto-ap.views.utils [auto-ap.views.utils
:refer [dispatch-event multi-field str->int with-is-admin? with-user]] :refer [dispatch-event str->int with-is-admin? with-user]]
[clojure.spec.alpha :as s] [malli.core :as m]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r])) [reagent.core :as r]))
;; Remaining cleanup todos: ;; Remaining cleanup todos:
;; test minification ;; test minification
(re-frame/reg-sub (def terms-override-schema (m/schema [:map
::can-submit [:client schema/reference]
:<- [::forms/form ::vendor-form] [:terms :int]]))
(fn [form]
(s/valid? ::entity/vendor (:data form))))
(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 (re-frame/reg-event-fx
::save-complete ::save-complete
@@ -39,13 +78,13 @@
::save ::save
[with-user with-is-admin? (forms/triggers-loading ::vendor-form) (forms/in-form ::vendor-form)] [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} _] (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 (let [query [:upsert-vendor
{:vendor (cond-> {:id id {:vendor (cond-> {:id id
:name name :name name
:print-as print-as :print-as print-as
:terms (or (str->int terms) :terms (or terms
nil) nil)
:default-account-id (:id default-account) :default-account-id (:id default-account)
:address address :address address
:primary-contact primary-contact :primary-contact primary-contact
@@ -75,12 +114,11 @@
(comp :id :client) (comp :id :client)
automatically-paid-when-due) automatically-paid-when-due)
:legal-entity-first-name legal-entity-first-name :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-last-name legal-entity-last-name
:legal-entity-tin legal-entity-tin :legal-entity-tin legal-entity-tin
:legal-entity-tin-type (some-> legal-entity-tin-type clojure.core/name not-empty keyword) :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]] common/default-read]]
{ :graphql { :graphql
{:token user {:token user
@@ -88,7 +126,10 @@
:query-obj {:venia/operation :query-obj {:venia/operation
{:operation/type :mutation {:operation/type :mutation
:operation/name "UpsertVendor"} :venia/queries [{:query/data query}]} :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 [] (defn pull-left []
(into [:div {:style {:position "relative" (into [:div {:style {:position "relative"
@@ -103,10 +144,8 @@
[form-builder/vertical-control {:is-small? true} [form-builder/vertical-control {:is-small? true}
"Name" "Name"
[:div.control.has-icons-left [:div.control.has-icons-left
[form-builder/raw-field [form-builder/raw-field-v2 {:field :name}
[:input.input.is-expanded {:type "text" [:input.input.is-expanded {:type "text"}]]
:field :name
:spec ::contact/name}]]
[:span.icon.is-small.is-left [:span.icon.is-small.is-left
[:i.fa.fa-user]]]] [:i.fa.fa-user]]]]
[form-builder/vertical-control {:is-small? true} [form-builder/vertical-control {:is-small? true}
@@ -115,201 +154,165 @@
[:div.control.has-icons-left [:div.control.has-icons-left
[:span.icon.is-small.is-left [:span.icon.is-small.is-left
[:i.fa.fa-envelope]] [:i.fa.fa-envelope]]
[form-builder/raw-field [form-builder/raw-field-v2 {:field :email}
[:input.input {:type "email" [:input.input {:type "email"}]]]]
:field :email
:spec ::contact/email}]]]]
[form-builder/vertical-control {:is-small? true} [form-builder/vertical-control {:is-small? true}
"Phone" "Phone"
[:div.control.has-icons-left [:div.control.has-icons-left
[form-builder/raw-field [form-builder/raw-field-v2 {:field :phone}
[:input.input {:type "phone" [:input.input {:type "phone"}]]
:field :phone
:spec ::contact/phone}]]
[:span.icon.is-small.is-left [:span.icon.is-small.is-left
[:i.fa.fa-phone]]]]]]]) [:i.fa.fa-phone]]]]]]])
(defn form-content [{:keys [data]}] (defn form-content [{:keys [data]}]
(let [is-admin? @(re-frame/subscribe [::subs/is-admin?]) (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] [form-builder/builder {:submit-event [::save]
:can-submit [::can-submit] :id ::vendor-form
:id ::vendor-form} :schema schema}
[form-builder/field [form-builder/field-v2 {:field :name
[:span "Name " [:span.has-text-danger "*"]] :required true}
[:input.input {:type "text" "Name"
:auto-focus true [:input.input {:auto-focus true}]]
:field :name
:spec ::entity/name}]]
[form-builder/field [form-builder/field-v2 {:field :print-as}
"Print Checks As" "Print Checks As"
[:input.input {:type "text" [:input.input]]
:field :print-as
:spec ::entity/print-as}]]
(when is-admin? (when is-admin?
[:div.field [form-builder/raw-field-v2 {:field :hidden}
[:label.checkbox [com/checkbox {:label "Hidden"}]])
[form-builder/raw-field
[:input {:type "checkbox"
:field [:hidden]
:spec ::entity/hidden}]]
" Hidden"]])
[form-builder/section {:title "Terms"} [form-builder/section {:title "Terms"}
[form-builder/field [form-builder/field-v2 {:field :terms}
"Terms" "Terms"
[:input.input {:type "number" [number-input ]]
:step "1"
:style {:width "4em"}
:field [:terms]
:size 3
:spec ::entity/terms}]]
(when is-admin? (when is-admin?
[form-builder/field [form-builder/field-v2 {:field [:terms-overrides]}
"Overrides" "Overrides"
[multi-field {:type "multi-field" [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:client]}
:field [:terms-overrides] [typeahead-v3 {:entities clients
:template [[typeahead-v3 {:entities clients :entity->text :name
:entity->text :name :style {:width "13em"}
:style {:width "13em"} :type "typeahead-v3"
:type "typeahead-v3" }]]
:field [:client]}] [form-builder/raw-field-v2 {:field :terms}
[:input.input {:type "number" [number-input]]]
:step "1" :schema [:sequential terms-override-schema]
:style {:width "4em"} :key-fn :id
:field [:terms] :next-key (random-uuid)
:size 3 :new-text "New Terms Override"}]])]
:spec ::entity/terms}]]}]])
]
(when is-admin? (when is-admin?
[form-builder/section {:title "Schedule payment when due"} [form-builder/section {:title "Schedule payment when due"}
[form-builder/field [form-builder/field-v2 {:field [:automatically-paid-when-due]}
"Client" "Client"
[multi-field {:type "multi-field" [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
:field [:automatically-paid-when-due] [typeahead-v3 {:entities clients
:template [[typeahead-v3 {:entities clients :entity->text :name
:entity->text :name :style {:width "13em"}}]]]
:style {:width "13em"} :schema [:sequential automatically-paid-schema]
:type "typeahead-v3" :key-fn :id
:field [:client]}]]}]]]) :next-key (random-uuid)
:new-text "Schedule another client"}]]])
(when is-admin? (when is-admin?
[form-builder/section {:title "Schedule payment on day of month"} [form-builder/section {:title "Schedule payment on day of month"}
[form-builder/field [form-builder/field-v2 {:field [:schedule-payment-dom]}
"Overrides" "Overrides"
[multi-field {:type "multi-field" [multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
:field [:schedule-payment-dom] [typeahead-v3 {:entities clients
:template [[typeahead-v3 {:entities clients :entity->text :name
:entity->text :name :style {:width "13em"}}]]
:style {:width "13em"} [form-builder/raw-field-v2 {:field :dom}
:type "typeahead-v3" [number-input]]]
:field [:client]}] :schema [:sequential schedule-payment-dom-schema]
[:input.input {:type "number" :key-fn :id
:step "1" :next-key (random-uuid)
:style {:width "4em"} :new-text "Schedule another client"}]]])
:field [:dom]
:size 3
:spec ::entity/terms}]]}]]])
[form-builder/section {:title "Expense Accounts"} [form-builder/section {:title "Expense Accounts"}
[form-builder/field [form-builder/field-v2 {:field :default-account
"Default *" :required? true}
"Default"
[search-backed-typeahead {:search-query (fn [i] [search-backed-typeahead {:search-query (fn [i]
[:search_account [:search_account
{:query i} {:query i}
[:name :id]]) [:name :id]])
:type "typeahead-v3" :style {:width "19em"}}]]
:style {:width "19em"}
:field [:default-account]}]]
(when is-admin? (when is-admin?
[form-builder/field [form-builder/field-v2 {:field [:account-overrides]}
"Overrides" "Overrides"
[multi-field {:type "multi-field" [multi-field-v2 {:template (fn [entity]
:field [:account-overrides] [[form-builder/raw-field-v2 {:field :client}
:template (fn [entity] [typeahead-v3 {:entities clients
[[typeahead-v3 {:entities clients :entity->text :name
:entity->text :name :style {:width "19em"}
:style {:width "19em"} }]]
:type "typeahead-v3" [form-builder/raw-field-v2 {:field :account}
:field [:client]}] [search-backed-typeahead {:search-query (fn [i]
[search-backed-typeahead {:search-query (fn [i] [:search_account
[:search_account {:query i
{:query i :client_id (:id (:client entity))}
:client_id (:id (:client entity))} [:name :id]])
[:name :id]]) :style {:width "15em"}}]]])
:type "typeahead-v3" :schema [:sequential account-override-schema]
:style {:width "15em"} :key-fn :id
:field [:account]}]])}]])] :next-key (random-uuid)
:new-text "Add override"}]])]
[form-builder/with-scope {:scope [:address ]} [form-builder/section {:title "Address"}
[form-builder/section {:title "Address"} [:div {:style {:width "30em"}}
[:div {:style {:width "30em"}} [form-builder/raw-field-v2 {:field :address}
[address2-field]]]] [address2-field]]]]
[form-builder/section {:title "Contact"} [form-builder/section {:title "Contact"}
[contact-field {:name "Primary" [contact-field {:name "Primary"
:field [:primary-contact]}] :field [:primary-contact]}]
[contact-field {:name "Secondary" [contact-field {:name "Secondary"
:field [:secondary-contact]}]] :field [:secondary-contact]}]]
(when is-admin? (when is-admin?
[form-builder/section {:title "Legal Entity"} [form-builder/section {:title "Legal Entity"}
[form-builder/vertical-control [form-builder/vertical-control
"Name" "Name"
[left-stack [left-stack
[:div.control [:div.control
[form-builder/raw-field [form-builder/raw-field-v2 {:field :legal-entity-first-name}
[:input.input {:type "text" [:input.input {:type "text"
:placeholder "First Name" :placeholder "First Name"}]]]
:field [:legal-entity-first-name]
:spec ::contact/name}]]]
[:div.control [:div.control
[form-builder/raw-field [form-builder/raw-field-v2 {:field :legal-entity-middle-name}
[:input.input {:type "text" [:input.input {:type "text"
:placeholder "Middle Name" :placeholder "Middle Name"}]]]
:field [:legal-entity-middle-name]
:spec ::contact/name}]]]
[:div.control [:div.control
[form-builder/raw-field [form-builder/raw-field-v2 {:field :legal-entity-last-name}
[:input.input {:type "text" [:input.input {:type "text"
:placeholder "Last Name" :placeholder "Last Name"}]]]]]
:field [:legal-entity-last-name]
:spec ::contact/name}]]]]]
[form-builder/vertical-control [form-builder/vertical-control
"TIN" "TIN"
[left-stack [left-stack
[form-builder/raw-field [form-builder/raw-field-v2 {:field :legal-entity-tin}
[:input.input {:type "text" [:input.input {:type "text"
:placeholder "SSN or EIN" :placeholder "SSN or EIN"
:field [:legal-entity-tin]
:size "12" :size "12"
:spec ::contact/name}]] }]]
[:div.control [:div.control
[:div.select [form-builder/raw-field-v2 {:field :legal-entity-tin-type}
[form-builder/raw-field [com/select-field {:options [["ein" "EIN"]
[:select {:type "select" ["ssn" "SSN"]]
:field [:legal-entity-tin-type]} :allow-nil? true}]]]]]
[:option {:value nil} ""]
[:option {:value "ein"} "EIN"]
[:option {:value "ssn"} "SSN"]]]]]]]
[form-builder/vertical-control [form-builder/field-v2 {:field :legal-entity-1099-type}
"1099 Type" "1099 Type"
[:div.control [com/select-field {:options [["none" "Don't 1099"]
[:div.select ["misc" "Misc"]
[form-builder/raw-field ["landlord" "Landlord"]]
[:select {:type "select" :allow-nil? true}]]])
:field [:legal-entity-1099-type]}
[:option {:value nil} ""]
[:option {:value "none"} "Don't 1099"]
[:option {:value "misc"} "Misc"]
[:option {:value "landlord"} "Landlord"]]]]]]])
[form-builder/hidden-submit-button]])) [form-builder/hidden-submit-button]]))
(defn vendor-dialog [ ] (defn vendor-dialog [ ]
@@ -321,36 +324,32 @@
::vendor-selected ::vendor-selected
[with-user (forms/in-form ::select-vendor-form)] [with-user (forms/in-form ::select-vendor-form)]
(fn [{{:keys [data]} :db :keys [user]} _] (fn [{{:keys [data]} :db :keys [user]} _]
{:graphql {:token user (if (:vendor data)
:query-obj {:venia/queries [[:vendor-by-id {:graphql {:token user
{:id (:id (:vendor data))} :query-obj {:venia/queries [[:vendor-by-id
common/default-read]]} {:id (:id (:vendor data))}
:owns-state {:single ::select-vendor-form} common/default-read]]}
:on-success (fn [r] :owns-state {:single ::select-vendor-form}
[::started (:vendor-by-id r)])}})) :on-success (fn [r]
[::started (:vendor-by-id r)])}}
(re-frame/reg-sub {:dispatch-n [[::forms/attempted-submit ::select-vendor-form]
::can-submit-select-vendor-form [::status/error ::select-vendor-form [{:message "Please select a vendor."}]]]})))
:<- [::forms/field ::select-vendor-form [:vendor]]
(fn [vendor]
(if vendor
true
false)))
(defn select-vendor-form-content [] (defn select-vendor-form-content []
[form-builder/builder {:submit-event [::vendor-selected] [form-builder/builder {:submit-event [::vendor-selected]
:can-submit [::can-submit-select-vendor-form] :id ::select-vendor-form
:id ::select-vendor-form} :validation-error-string "Please select a vendor."
[form-builder/field :schema [:map
[:vendor schema/reference]]}
[form-builder/field-v2 {:field :vendor
:required? true}
"Vendor to edit" "Vendor to edit"
[search-backed-typeahead {:search-query (fn [i] [search-backed-typeahead {:search-query (fn [i]
[:search_vendor [:search_vendor
{:query i} {:query i}
[:name :id]]) [:name :id]])
:type "typeahead-v3" :style {:width "20em"}
:auto-focus true :auto-focus true}]]
:field [:vendor]}]]
[form-builder/hidden-submit-button]]) [form-builder/hidden-submit-button]])
@@ -360,6 +359,7 @@
(fn [{:keys [db]} [_ vendor]] (fn [{:keys [db]} [_ vendor]]
{:db (-> db (forms/start-form ::vendor-form (-> vendor {:db (-> db (forms/start-form ::vendor-form (-> vendor
(update :automatically-paid-when-due #(mapv (fn [apwd] (update :automatically-paid-when-due #(mapv (fn [apwd]
apwd
{:id (:id apwd) {:id (:id apwd)
:client apwd}) :client apwd})
%)) %))
@@ -370,11 +370,11 @@
{:title "Vendor" {:title "Vendor"
:body [vendor-dialog] :body [vendor-dialog]
:class "semi-wide" :class "semi-wide"
:status-from [::status/single ::vendor-form]
:confirm {:value "Save Vendor" :confirm {:value "Save Vendor"
:status-from [::status/single ::vendor-form] :status-from [::status/single ::vendor-form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::save]) :on-click (dispatch-event [::save])
:can-submit [::can-submit]
:close-event [::status/completed ::vendor-form]}}]})) :close-event [::status/completed ::vendor-form]}}]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@@ -388,5 +388,4 @@
:status-from [::status/single ::select-vendor-form] :status-from [::status/single ::select-vendor-form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::vendor-selected]) :on-click (dispatch-event [::vendor-selected])
:can-submit [::can-submit-select-vendor-form]
:close-event [::status/completed ::select-vendor-form]}}]})) :close-event [::status/completed ::select-vendor-form]}}]}))

View File

@@ -1,7 +1,3 @@
(ns auto-ap.views.components.vendor-filter (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]]))

View File

@@ -28,7 +28,7 @@
[auto-ap.views.pages.admin.users :refer [admin-users-page]] [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.import-batches :refer [import-batches-page]]
[auto-ap.views.pages.admin.yodlee2 :as yodlee2] [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)) (defmulti page (fn [active-page] active-page))
(defmethod page :unpaid-invoices [_] (defmethod page :unpaid-invoices [_]
@@ -112,8 +112,8 @@
(defmethod page :admin-yodlee2 [_] (defmethod page :admin-yodlee2 [_]
(yodlee2/admin-yodle-provider-accounts-page)) (yodlee2/admin-yodle-provider-accounts-page))
(defmethod page :admin-plaid [_] (defmethod page :plaid [_]
(plaid/admin-plaid-page)) (plaid/plaid-page))
(defmethod page :admin-accounts [_] (defmethod page :admin-accounts [_]
(admin-accounts-page)) (admin-accounts-page))
@@ -144,8 +144,9 @@
(let [ap (re-frame/subscribe [::subs/active-page]) (let [ap (re-frame/subscribe [::subs/active-page])
current-client @(re-frame/subscribe [::subs/client]) current-client @(re-frame/subscribe [::subs/client])
is-loading? @(re-frame/subscribe [::subs/is-initial-loading?])] is-loading? @(re-frame/subscribe [::subs/is-initial-loading?])]
(if is-loading? (when @ap
[loading-layout] (if is-loading?
[loading-layout]
[:div
^{:key (str @ap "-" current-client)} [page @ap]]))) [:div
^{:key (str @ap "-" current-client)} [page @ap]]))))

View File

@@ -73,8 +73,7 @@
[buttons/new-button {:name "Account" [buttons/new-button {:name "Account"
:class "is-primary" :class "is-primary"
:event [::account-form/editing :event [::account-form/editing
{:type :asset {:account-set "default"}]}]]
:account-set "default"}]}]]
[table/accounts-table {:data-page ::page}]]) [table/accounts-table {:data-page ::page}]])
(defn admin-accounts-page [] (defn admin-accounts-page []

View File

@@ -1,19 +1,21 @@
(ns auto-ap.views.pages.admin.accounts.form (ns auto-ap.views.pages.admin.accounts.form
(:require [auto-ap.entities.account :as entity] (:require
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.subs :as subs] [auto-ap.forms.builder :as form-builder]
[auto-ap.views.components.layouts :refer [side-bar]] [auto-ap.schema :as schema]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.subs :as subs]
[auto-ap.views.utils :refer [dispatch-event multi-field]] [auto-ap.views.components :as com]
[clojure.spec.alpha :as s] [auto-ap.views.components.layouts :refer [side-bar]]
[clojure.string :as str] [auto-ap.views.components.typeahead :refer [typeahead-v3]]
[re-frame.core :as re-frame])) [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 types [:dividend :expense :asset :liability :equity :revenue])
(def applicabilities [:global :optional :customized]) (def applicabilities [:global :optional :customized])
(re-frame/reg-sub (re-frame/reg-sub
::request ::request
:<- [::forms/form ::form] :<- [::forms/form ::form]
@@ -35,11 +37,7 @@
(:name client-override))}) (:name client-override))})
client-overrides)})) client-overrides)}))
(re-frame/reg-sub
::can-submit
:<- [::request]
(fn [request]
(s/valid? ::entity/account request)))
(re-frame/reg-event-db (re-frame/reg-event-db
::editing ::editing
@@ -51,103 +49,98 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::edited ::edited
[(forms/triggers-saved ::form :upsert-account)] [(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 (re-frame/reg-event-fx
::saving ::saving
(fn [{:keys [db]} _] [with-user (re-frame/inject-cofx ::inject/sub [::request]) ]
(when @(re-frame/subscribe [::can-submit]) (fn [{:keys [user] ::keys [request]} _]
(let [{{:keys [id type name numeric-code account-set]} :data :as data} @(re-frame/subscribe [::forms/form ::form])] (let [_ @(re-frame/subscribe [::forms/form ::form])]
{:db (forms/loading db ::form ) {:graphql
:graphql {:owns-state {:single ::form}
{:token (-> db :user) :token user
:query-obj {:venia/operation {:operation/type :mutation :query-obj {:venia/operation {:operation/type :mutation
:operation/name "UpsertAccount"} :operation/name "UpsertAccount"}
:venia/queries [{:query/data [:upsert-account :venia/queries [{:query/data [:upsert-account
{:account @(re-frame/subscribe [::request])} {:account request}
[:id :type :name :account-set :numeric-code :location :applicability [:client-overrides [:name :id [:client [:id :name]]]]]]}]} [:id :type :name :account-set :numeric-code :location :applicability [:client-overrides [:name :id [:client [:id :name]]]]]]}]}
:on-success [::edited] :on-success [::edited]
:on-error [::forms/save-error ::form]}})))) :on-error [::forms/save-error ::form]}})))
(def account-form (forms/vertical-form {:can-submit [::can-submit] (def account-customization-schema
:change-event [::forms/change ::form] (m/schema
:submit-event [::saving] [:map [:client schema/reference]
:id ::form})) [: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 [_] (defn form [_]
(let [{error :error account :data } @(re-frame/subscribe [::forms/form ::form]) (let [{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)}
[side-bar {:on-close (dispatch-event [::forms/form-closing ::form])} [side-bar {:on-close (dispatch-event [::forms/form-closing ::form])}
(form-inline {:title (if (:id account) [form-builder/builder {:change-event [::forms/change ::form]
"Edit account" :submit-event [::saving]
"Add account")} :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" [form-builder/field-v2 {:field :name}
[:input.input {:type "text" "Name"
:field :account-set [:input.input {:type "text"
:disabled (boolean (:id account)) :auto-focus (boolean (:id account))}]]
:spec ::entity/account-set}])
(field "Code" [form-builder/field-v2 {:field :type}
[:input.input {:type "text" "Account Type"
:field :numeric-code [com/select-field {:options (map (fn [l]
:disabled (boolean (:id account)) [l (str/capitalize (name l))])
:spec ::entity/numeric-code}]) types)
:allow-nil? true
(field "Name" :keywordize? true}]]
[: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)])])
(field "Location" [form-builder/field-v2 {:field :location}
[:input.input.known-field.location {:type "text" "Location"
:field :location [:input.input.known-field.location {:type "text"}]]
:spec ::entity/location}])
[:h2.subtitle "Client"] [form-builder/section {:title "Client"}
(field-holder "Applicability" [:h2.subtitle "Client"]
[:div.select [form-builder/field-v2 {:field :applicability}
(raw-field "Applicability"
[:select {:type "select" [com/select-field {:options (map (fn [l]
:field :applicability [l
:spec (set applicabilities)} (str/capitalize (name l))])
(map (fn [l] applicabilities)
[:option {:value (name l)} (str/capitalize (name l))]) applicabilities)])]) :allow-nil? true
(field "Customizations" :keywordize? true}]]
[multi-field {:type "multi-field" [form-builder/field-v2 {:field :client-overrides}
:field [:client-overrides] "Customizations"
:template [[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
:style {:width "13em"} [typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name :style {:width "13em"}
:type "typeahead-v3" :entity->text :name}]]
:field [:client]}] [form-builder/raw-field-v2 {:field :name}
[:input.input {:type "text" [:input.input {:type "text"
:style {:width "15em"} :style {:width "15em"}
:placeholder "Bubblegum" :placeholder "Bubblegum"}]]]
:field [:name]}] :key-fn :id
]}]) :schema [:sequential account-customization-schema]}]]]
(error-notification) [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]] [auto-ap.views.components.address :refer [address2-field]]
[react-signature-canvas] [react-signature-canvas]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [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 [auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]] :refer [search-backed-typeahead]]
[auto-ap.views.utils [auto-ap.views.utils
:refer [date->str :refer [date-picker
date-picker dispatch-event]]
date-picker-friendly
dispatch-event
horizontal-field
multi-field
standard]]
[bidi.bidi :as bidi] [bidi.bidi :as bidi]
[cljs-time.coerce :as coerce] [cljs-time.coerce :as coerce]
[cljs-time.core :as t] [cljs-time.core :as t]
[clojure.string :as str]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r] [reagent.core :as r]
[react-signature-canvas] [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 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] (defn upload-replacement-button [{:keys [on-change]} text]
(let [button (atom nil)] (let [button (atom nil)]
(r/create-class {:display-name "Upload button" (r/create-class {:display-name "Upload button"
@@ -104,14 +134,6 @@
[upload-replacement-button {:on-change on-change} "Upload signature"]]])) [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 (re-frame/reg-sub
::new-client-request ::new-client-request
:<- [::forms/form ::form] :<- [::forms/form ::form]
@@ -120,7 +142,8 @@
{:id (:id new-client-data), {:id (:id new-client-data),
:name (:name new-client-data) :name (:name new-client-data)
:code (:code new-client-data) ;; TODO add validation can't change :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-auth-token (:square-auth-token new-client-data)
:square-locations (map :square-locations (map
(fn [x] (fn [x]
@@ -135,15 +158,7 @@
:location (:location x)}) :location (:location x)})
(:ezcater-locations new-client-data)) (:ezcater-locations new-client-data))
:locked-until (cond (not (:locked-until new-client-data)) :locked-until (: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)
)
:locations (mapv :location (:locations new-client-data)) :locations (mapv :location (:locations new-client-data))
:matches (mapv :match (:matches new-client-data)) :matches (mapv :match (:matches new-client-data))
:location-matches (:location-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]}] :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 {:number number
:name name :name name
:check-number (when-not (str/blank? check-number) :check-number check-number
(js/parseInt check-number)) :numeric-code numeric-code
:numeric-code (when-not (str/blank? numeric-code)
(js/parseInt numeric-code))
:include-in-reports include-in-reports :include-in-reports include-in-reports
:start-date (cond (not start-date) :start-date start-date
nil
(instance? goog.date.Date start-date)
(date->str start-date standard)
:else
start-date
)
:type type :type type
:id id :id id
:sort-order i :sort-order i
:visible visible :visible visible
:locations (mapv :location locations) :locations (mapv :location locations)
:use-date-instead-of-post-date use-date-instead-of-post-date :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) :yodlee-account (:id yodlee-account)
:plaid-account (:id plaid-account) :plaid-account (:id plaid-account)
:intuit-bank-account (:id intuit-bank-account) :intuit-bank-account (:id intuit-bank-account)
@@ -204,7 +207,7 @@
:<- [::subs/route-params] :<- [::subs/route-params]
:<- [::subs/clients-by-id] :<- [::subs/clients-by-id]
(fn [[rp 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 (re-frame/reg-event-fx
@@ -220,13 +223,16 @@
{:id (:id sl) {:id (:id sl)
:square-location sl :square-location sl
:client-location (:client-location sl)})))) :client-location (:client-location sl)}))))
(update :locations #(mapv (fn [l] {:location l}) %)) (update :locations #(mapv (fn [l] {:location l
(update :matches #(mapv (fn [l] {:match l}) %)) :id (random-uuid)}) %))
(update :matches #(mapv (fn [l] {:match l
:id (random-uuid)}) %))
(update :bank-accounts (update :bank-accounts
(fn [bas] (fn [bas]
(mapv (fn [ba] (mapv (fn [ba]
(update ba :locations (fn [ls] (update ba :locations (fn [ls]
(map (fn [l] {:location l}) (map (fn [l] {:location l
:id (random-uuid)})
ls)))) ls))))
bas))))))})) bas))))))}))
@@ -375,188 +381,132 @@
(when active? (when active?
[:div.card-content [:div.card-content
[:label.label "General"] [:label.label "General"]
[horizontal-field [level/left-stack
nil
[:div.control [:div.control
[:p.help "Account Code"] [:p.help "Account Code"]
(if new? (if new?
[:div.field.has-addons.is-extended [:div.field.has-addons
[:p.control [:a.button.is-static (:code new-client) "-" ]] [:p.control [:a.button.is-static (:code new-client) "-" ]]
[:p.control [:p.control
[form-builder/raw-field [form-builder/raw-field-v2 {:field :code}
[:input.input {:type "code" [:input.input {:type "text"}]]]]
:field [:code]
:spec ::entity/code}]]]]
[:div.field [:p.control code]])] [:div.field [:p.control code]])]
[form-builder/field-v2 {:field :name}
[form-builder/field
"Nickname" "Nickname"
[:input.input {:placeholder "BOA Checking #1" [:input.input {:placeholder "BOA Checking #1"
:type "text" :type "text"}]]
:field [:name]}]] [form-builder/field-v2 {:field :numeric-code}
[horizontal-field "Numeric Code"
nil [com/number-input {:placeholder "20101"
[form-builder/field :style {:width "8em"}}]]
"Numeric Code" [form-builder/field-v2 {:field :start-date}
[:input.input {:placeholder "20101"
:type "text"
:field [:numeric-code]}]]]
[form-builder/field
"Start date" "Start date"
[date-picker {:class-name "input" [date-picker {:output :cljs-date}]]]
: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]}]]]
(when (#{:check ":check"} type ) (when (#{:check ":check"} type )
[:div [:div
[:label.label "Bank"] [:label.label "Bank"]
[horizontal-field [level/left-stack
nil [form-builder/field-v2 {:field :bank-name}
[form-builder/field
"Bank Name" "Bank Name"
[:input.input {:placeholder "Bank of America" [:input.input {:placeholder "Bank of America"
:type "text" :type "text"}]]
:field [:bank-name]}]] [form-builder/field-v2 {:field [:routing]}
[form-builder/field
"Routing #" "Routing #"
[:input.input {:placeholder "104819123" [:input.input {:placeholder "104819123"
:style {:width "9em"} :style {:width "9em"}
:type "text" :type "text"}]]
:field [:routing]}]] [form-builder/field-v2 {:field :bank-code}
[form-builder/field
"Bank code" "Bank code"
[:input.input {:placeholder "12/10123" [:input.input {:placeholder "12/10123"
:type "text" :type "text"}]]]
:field [:bank-code]}]]]
[horizontal-field [level/left-stack
nil [form-builder/field-v2 {:field :number}
[form-builder/field
"Account #" "Account #"
[:input.input {:placeholder "123456789" [:input.input {:placeholder "123456789"
:type "text" :type "text"
:style {:width "20em"} :style {:width "20em"}}]]
:field [:number]}]]
[form-builder/field [form-builder/field-v2 {:field :check-number}
"Check Number" "Check Number"
[:input.input {:placeholder "10000" [com/number-input {:style {:width "8em"}
:style {:width "6em"} :placeholder "10000"}]]]
:type "text"
:field [:check-number]}]]] [form-builder/field-v2 {:field :yodlee-account}
[form-builder/field
"Yodlee Account"
[:input.input {:placeholder "Yodlee Account #"
:type "text"
:field [:yodlee-account-id]}]]
[form-builder/field
"Yodlee Account (new)" "Yodlee Account (new)"
[typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)]) [typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)])
:entity->text (fn [m] (str (:name m) " - " (:number m))) :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
[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" "Intuit Bank Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts]) [typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
:entity->text (fn [m] (str (:name m))) :entity->text (fn [m] (str (:name m)))}]]
:type "typeahead-v3" [form-builder/field-v2 {:field :plaid-accounti}
:field [:intuit-bank-account]}]]
[form-builder/field
"Plaid Account" "Plaid Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)]) [typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)])
:entity->text (fn [m] (str (:name m))) :entity->text (fn [m] (str (:name m)))}]]])
:type "typeahead-v3"
:field [:plaid-account]}]]])
(when (#{:credit ":credit"} type ) (when (#{:credit ":credit"} type )
[:div [:div
[:label.label "Account"] [:label.label "Account"]
[horizontal-field [form-builder/field-v2 {:field :bank-name}
nil "Bank Name"
[form-builder/field [:input.input {:placeholder "Bank of America"
"Bank Name" :type "text"}]]
[:input.input {:placeholder "Bank of America"
:type "text"
:field [:bank-name]}]]]
[horizontal-field [form-builder/field-v2 {:field :number}
nil "Account #"
[form-builder/field [:input.input {:placeholder "123456789"
"Account #"
[:input.input {:placeholder "123456789"
:type "text"
:field [:number]}]]]
[form-builder/field
"Yodlee Account"
[:input.input {:placeholder "Yodlee Account #"
:type "text" :type "text"
:field [:yodlee-account-id]}]] :style {:width "20em"}}]]
[form-builder/field
[form-builder/field-v2 {:field :yodlee-account}
"Yodlee Account (new)" "Yodlee Account (new)"
[typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)]) [typeahead-v3 {:entities @(re-frame/subscribe [::yodlee-accounts (:id new-client)])
:entity->text (fn [m] (str (:name m) " - " (:number m))) :entity->text (fn [m] (str (:name m) " - " (:number m)))}]]
:type "typeahead-v3"
:field [:yodlee-account]}]] [form-builder/raw-field-v2 {:field :use-date-instead-of-post-date}
[:div.field [com/checkbox {:label "(Yodlee only) Use 'date' instead of 'postDate'"}]
[:label.checkbox [:input {:type "checkbox"
[form-builder/raw-field :field [:use-date-instead-of-post-date]}]]
[:input {:type "checkbox"
:field [:use-date-instead-of-post-date]}]] [form-builder/field-v2 {:field :intuit-bank-account}
" (Yodlee only) Use 'date' instead of 'postDate'"]]
[form-builder/field
"Intuit Bank Account" "Intuit Bank Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts]) [typeahead-v3 {:entities @(re-frame/subscribe [::subs/intuit-bank-accounts])
:entity->text (fn [m] (str (:name m))) :entity->text (fn [m] (str (:name m)))}]]
:type "typeahead-v3"
:field [:intuit-bank-account]}]] [form-builder/field-v2 {:field :plaid-account}
[form-builder/field
"Plaid Account" "Plaid Account"
[typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)]) [typeahead-v3 {:entities @(re-frame/subscribe [::plaid-accounts (:id new-client)])
:entity->text (fn [m] (str (:name m))) :entity->text (fn [m] (str (:name m)))}]]])
:type "typeahead-v3"
:field [:plaid-account]}]]])
[:div.field [:div.field
[:label.label "Locations"] [:label.label "Locations"]
[:div.control [:div.control
[:p.help "If this account is location-specific, add the valid locations"] [:p.help "If this account is location-specific, add the valid locations"]
[form-builder/raw-field [form-builder/raw-field-v2 {:field :locations}
[multi-field {:type "multi-field" [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :location}
:field [:locations] [com/select-field {:options (map (fn [l]
:template [[:select.select {:type "select" [(:location l) (:location l)])
:style {:width "7em"} (get-in new-client [:locations]))
:allow-nil? true :allow-nil? true
:field [:location] :style {:width "7em"}
:spec (set (map :location (get-in new-client [:locations])))} }]]]
[:<> :schema [:sequential location-schema]
[:option ""] :key-fn :id}]]]]
[:<> (map (fn [l] ^{:key (:location l)}
[:option {:value (:location l)} (:location l)]) [form-builder/raw-field-v2 {:field :include-in-reports}
(get-in new-client [:locations]))]]]]}]]]] [com/checkbox {:label "Include in reports"}]
[:div.field ]
[:label.checkbox ])
[form-builder/raw-field
[:input {:type "checkbox"
:field [:include-in-reports]}]]
" Include in reports"]]])
(when active? (when active?
[:footer.card-footer [:footer.card-footer
@@ -568,86 +518,87 @@
(defn general-section [] (defn general-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/section {:title "General"} [form-builder/section {:title "General"}
[form-builder/field [form-builder/field-v2 {:field :name}
"Name" "Name"
[:input.input {:type "text" [:input.input {:type "text"
:style {:width "20em"} :style {:width "20em"}}]]
:field [:name] [form-builder/field-v2 {:field :code}
:spec ::entity/name}]]
[form-builder/field
"Client code" "Client code"
[:input.input {:type "code" [:input.input {:type "code"
:style {:width "5em"} :style {:width "5em"}
:field :code
:disabled (boolean (:id new-client)) :disabled (boolean (:id new-client))
:spec ::entity/code}]] :spec ::entity/code}]]
[form-builder/field [form-builder/field-v2 {:field :locations}
"Locations" "Locations"
[multi-field {:type "multi-field" [com/multi-field-v2 {:allow-change? false
:field :locations :template [[form-builder/raw-field-v2 {:field :location}
:allow-change? false [:input.input {:max-length 2
:template [[:input.input {:field [:location] :style { :width "4em"}}]]]
:max-length 2 :disable-remove? true
:style { :width "4em"}}]]}]] :key-fn :id
:schema [:sequential location-schema]
:next-key (random-uuid)}]]
[:div.field [form-builder/vertical-control
[:label.label "Signature"] "Signature"
[signature {:signature-file (:signature-file new-client) [signature {:signature-file (:signature-file new-client)
:signature-data (:signature-data new-client) :signature-data (:signature-data new-client)
:on-change (fn [uri] :on-change (fn [uri]
(re-frame/dispatch [::forms/change ::form [:signature-data] uri]))}]] (re-frame/dispatch [::forms/change ::form [:signature-data] uri]))}]]
[form-builder/field [form-builder/field-v2 {:field :locked-until}
"Locked Until" "Locked Until"
[date-picker-friendly {:type "date" [date-picker {:output :cljs-date
:field [:locked-until] :style {:width "15em"}}]]]))
:style {:width "15em"}}]]]))
(defn contacts-section [] (defn contacts-section []
[form-builder/section {:title "Contacts"} [form-builder/section {:title "Contacts"}
[form-builder/field [form-builder/field-v2 {:field :emails}
"Emails (address/description)" "Emails (address/description)"
[multi-field {:type "multi-field" [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :email}
:field :emails [:input.input {:type "email"
:template [[:input.input {:type "email" :placeholder "tom@myspace.com"}]]
:field [:email] [form-builder/raw-field-v2 {:field :description}
:placeholder "tom@myspace.com" [:input.input {:type "text"
:spec ::entity/email}] :placeholder "Manager"}]]]
[:input.input {:type "text" :key-fn :id
:placeholder "Manager" :schema [:sequential email-schema]
:field [:description]}]]}]] :next-key (random-uuid)}]]
[form-builder/with-scope {:scope [:address ]} [form-builder/vertical-control
[:div.field "Address"
[:label.label "Address"] [:div {:style {:width "30em"}}
[:div.control [form-builder/raw-field-v2 {:field :address}
[:div {:style {:width "30em"}} [address2-field]]]]])
[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 [] (defn matching-section []
[form-builder/section {:title "Matching"} [form-builder/section {:title "Matching"}
[form-builder/field [form-builder/field-v2 {:field :matches}
"Name matches" "Name matches"
[multi-field {:type "multi-field" [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field [:match]}
:field :matches [:input.input {:placeholder "Harry's burger joint"
:template [[:input.input {:field [:match] :style { :width "15em"}}]]]
:placeholder "Harry's burger joint" :key-fn :id
:style { :width "15em"}}]]}]] :next-key (random-uuid)
:schema [:sequential name-match-schema]}]]
[form-builder/field [form-builder/field-v2 {:field :location-matches}
"Location Matches" "Location Matches"
[multi-field {:type "multi-field" [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :match}
:field :location-matches [:input.input {:placeholder "Downtown"
:template [[:input.input {:field [:match] :style { :width "15em"}}]]
:placeholder "Downtown" [form-builder/raw-field-v2 {:field :location}
:style { :width "15em"}}] [:input.input {:placeholder "DT"
[:input.input {:field [:location] :max-length 2
:placeholder "DT" :style { :width "4em"}}]]]
:max-length 2 :schema [:sequential location-match-schema]
:style { :width "4em"}}]]}]]]) :next-key (random-uuid)
:key-fn :id}]]])
(defn bank-accounts-section [] (defn bank-accounts-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
@@ -674,99 +625,82 @@
[form-builder/section {:title "Cash Flow"} [form-builder/section {:title "Cash Flow"}
[:label.label (str "Week A (" next-week-a ")")] [:label.label (str "Week A (" next-week-a ")")]
[left-stack [left-stack
[form-builder/field [form-builder/field-v2 {:field :week-a-credits}
"Regular Credits" "Regular Credits"
[:input.input {:type "number" [com/money-input]]
:style {:width "10em"} [form-builder/field-v2 {:field :week-a-debits}
:placeholder "500.00"
:field [:week-a-credits]
:step "0.01"}]]
[form-builder/field
"Regular Debits" "Regular Debits"
[:input.input {:type "number" [com/money-input]]]
:style {:width "10em"}
:placeholder "150.00"
:field [:week-a-debits]
:step "0.01"}]]]
[:label.label (str "Week B (" next-week-b ")")] [:label.label (str "Week B (" next-week-b ")")]
[left-stack [left-stack
[form-builder/field "Regular Credits" [form-builder/field-v2 {:field :week-b-credits}
[:input.input {:type "number" "Regular Credits"
:style {:width "10em"} [com/money-input]]
:placeholder "1000.00" [form-builder/field-v2 {:field :week-b-debits}
:field [:week-b-credits] "Regular Debits"
:step "0.01"}]] [com/money-input]]]
[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"]
[:div.control [form-builder/field-v2 {:field :forecasted-transactions}
[form-builder/raw-field "Forecasted transactions"
[multi-field {:type "multi-field"
:field :forecasted-transactions [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :identifier}
:template [[:input.input {:type "text" [:input.input {:type "text"
:placeholder "Identifier" :placeholder "Identifier"
:style {:width "10em"} :style {:width "10em"}}]]
:field [ :identifier]}] [form-builder/raw-field-v2 {:field :day-of-month}
[:input.input {:type "number" [com/number-input {:placeholder "DOM"}]]
:style {:width "8em"} [form-builder/raw-field-v2 {:field :amount
:placeholder "DOM" :placeholder "AMT"}
:step "1" [com/money-input]]]
:field [:day-of-month]}] :key-fn :id}]]]))
[:input.input {:type "number"
:placeholder "250.00"
:class "has-text-right"
:style {:width "7em"}
:field [:amount]
:step "0.01"}]]}]]]]]))
(defn square-section [] (defn square-section []
(let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])] (let [{new-client :data} @(re-frame/subscribe [::forms/form ::form])]
[form-builder/section {:title "Square Integration"} [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" [:input.input {:type "text"
:style {:width "40em"} :style {:width "40em"}}]]
:field [:square-auth-token]}]] [form-builder/field-v2 {:field :selected-square-locations}
[form-builder/field
"Square Locations" "Square Locations"
[multi-field {:type "multi-field" [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :square-location}
:field :selected-square-locations [typeahead-v3 {:entities (:square-locations new-client)
:template [[typeahead-v3 {:entities (:square-locations new-client) :entity->text :name
:entity->text :name :style {:width "15em"}}]]
:type "typeahead-v3" [form-builder/raw-field-v2 {:field :client-location}
:style {:width "15em"} [com/select-field {:options (map (fn [l]
:field [:square-location]}] [(:location l) (:location l)])
[:input.input {:type "text" (get-in new-client [:locations]))
:style {:width "4em"} :allow-nil? true
:field [:client-location] :style {:width "7em"}
:step "0.01"}]] }]]]
:disable-remove? true}]]])) :disable-remove? true
:key-fn :id
:schema [:sequential square-location-schema]}]]]))
(defn ezcater-section [] (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 [] (defn form-content []
(let [_ @(re-frame/subscribe [::client]) (let [_ @(re-frame/subscribe [::client])
@@ -774,10 +708,10 @@
^{:key (or (:id new-client) ^{:key (or (:id new-client)
"new")} "new")}
[form-builder/builder {:can-submit [::can-submit] [form-builder/builder {:submit-event [::save-new-client ]
:submit-event [::save-new-client ]
:id ::form :id ::form
:fullwidth? false} :fullwidth? false
:schema client-schema}
[general-section] [general-section]
[contacts-section] [contacts-section]

View File

@@ -1,168 +1,114 @@
(ns auto-ap.views.pages.admin.excel-import (ns auto-ap.views.pages.admin.excel-import
(:require [auto-ap.events :as all-events] (:require
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.subs :as subs] [auto-ap.forms.builder :as form-builder]
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] [auto-ap.schema :as schema]
[auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.components :as com]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
[auto-ap.views.utils :refer [bind-field dispatch-event]] [auto-ap.views.components.layouts :refer [side-bar-layout]]
[re-frame.core :as re-frame])) [auto-ap.views.utils :refer [with-user]]
[malli.core :as m]
(re-frame/reg-sub [re-frame.core :as re-frame]
::excel-import [reagent.core :as r]))
(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)))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::save ::save
[(forms/in-form ::excel-import)] [ with-user (forms/in-form ::form)]
(fn [{{excel-import-data :data :as excel-import-form} :db}] (fn [{:keys [db user]}]
(let [user @(re-frame/subscribe [::subs/token])] {:http {:token user
{:db (-> excel-import-form :method :post
(assoc :status :loading) :body (pr-str {:excel-rows (:excel-rows (:data db))})
(assoc :error nil)) :headers {"Content-Type" "application/edn"}
:http {:token user :uri (str "/api/invoices/upload-integreat")
:method :post :owns-state {:single ::form}
:body (pr-str excel-import-data) :on-success [::save-complete]}}))
:headers {"Content-Type" "application/edn"}
:uri (str "/api/invoices/upload-integreat")
:on-success [::save-complete]
:on-error [::forms/save-error ::excel-import]}})))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::save-complete ::save-complete
(fn [{:keys [db]} [_ rows]] (fn [{:keys [db]} [_ rows]]
{:db (cond->
(-> db {:db (assoc db ::result rows)}
(forms/save-succeeded ::excel-import) (seq (:vendors-not-found rows)) (assoc :dispatch [::forms/start-form ::create-vendors ]))))
(assoc-in [::excel-import :rows] rows))}))
(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 (re-frame/reg-event-fx
::create-vendors ::create-vendors
(fn [{:keys [db]}] [with-user (forms/in-form ::create-vendors)]
(let [excel-import (::excel-import db)] (fn [{:keys [user db]}]
(println (::expense-accounts db)) {:graphql {:token user
{:graphql {:token (:user db) :owns-state {:single ::create-vendors}
:query-obj {:venia/operation {:operation/type :mutation :query-obj {:venia/operation {:operation/type :mutation
:operation/name "UpsertVendor"} :operation/name "UpsertVendor"}
:venia/queries (map (fn [v ] :venia/queries
{:query/data [:upsert-vendor (for [[vendor-name {:keys [default-account]}] (:data db)]
{:vendor {:name v :default-account-id (-> db ::expense-accounts (get v) :default-account-id :id)}} {:query/data [:upsert-vendor
[:id :name]]}) {:vendor {:name vendor-name :default-account-id (:id default-account)}}
[:id :name]]})}
(get-in db [::excel-import :create-vendors]))} :on-success [::create-vendor-complete]}}))
:on-success [::create-vendor-complete]
:on-error [::create-vendor-error]}
:db (-> db
(assoc-in [::excel-import :saving-vendors?] true))})))
(re-frame/reg-event-db (re-frame/reg-event-db
::create-vendor-complete ::create-vendor-complete
(fn [db [_ data]] (fn [db _]
(-> db (dissoc db ::result )))
(update-in [::excel-import :rows :vendors-not-found]
(fn [v] (def missing-vendor-schema
(reduce disj v (get-in db [::excel-import :create-vendors])))) (m/schema [:map-of :string
(update-in [::excel-import] dissoc :create-vendors)))) [: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 [] (defn admin-excel-import-content []
[:div [:div
(let [{{:keys [vendors-not-found already-imported imported]} :rows (let [{:keys [vendors-not-found errors already-imported imported]} @(re-frame/subscribe [::result])]
: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]]]
[:div [:div
[:h1.title "Import Invoices from Integreat Excel"] [:h1.title "Import Invoices from Integreat Excel"]
(when (seq vendors-not-found) (when (seq vendors-not-found)
[:article.message.is-warning.is-paddingless [create-missing-vendors {:vendors vendors-not-found}]
[:div.message-header )
"Some vendors could not be found"] [form-builder/builder {:id ::form
:submit-event [::save]}
[:div.message-body [form-builder/raw-field-v2 {:field :excel-rows}
[:h2 "Check the vendors you want to create"] [:textarea.textarea {:rows "20"
[:div.columns :type "text"}]]
(for [[i vendor-group] (map vector (range) (partition-all (max 1 (/ (count vendors-not-found) 3)) vendors-not-found))] [form-builder/error-notification]
^{:key i} [form-builder/submit-button "Import"]]
[: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"]
[:div.is-clearfix] [:div.is-clearfix]
[:div.is-clearfix [:div.is-clearfix
[:p [:p
@@ -171,29 +117,49 @@
[:p [:p
(when already-imported (when already-imported
(str already-imported " rows already imported."))]] (str already-imported " rows already imported."))]]
(when-let [errors (:errors (:rows excel-import-data))] (when errors
[:div [:div
[:h3 (str "Import errors (" (min 100 (count errors)) " / " (count errors) " )")] [:h3 (str "Import errors (" (min 100 (count errors)) " / " (count errors) " )")]
[:table.table.is-fullwidth [:table.table.is-fullwidth
[:thead [:thead
[:th "Date"] [:td "Date"]
[:th "Invoice #"] [:td "Invoice #"]
[:th "Client"] [:td "Client"]
[:th "Vendor"] [:td "Vendor"]
[:th "Amount"] [:td "Amount"]
[:th "Errors"]] [:td "Errors"]]
(for [{:keys [raw-date invoice-number client vendor-name amount] row-errors :errors} (take 100 errors)] [:tbody
^{:key (str raw-date invoice-number client vendor-name amount)} (for [{:keys [raw-date invoice-number client vendor-name amount] row-errors :errors} (take 100 errors)]
[:tr ^{:key (str raw-date invoice-number client vendor-name amount)}
[:td raw-date] [:tr
[:td invoice-number] [:td raw-date]
[:td client] [:td invoice-number]
[:td vendor-name] [:td client]
[:td amount] [:td vendor-name]
[:td (map (fn [{:keys [info]}] ^{:key info} [:p info]) row-errors)]])]])])]) [: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 {}] [side-bar-layout {:side-bar [admin-side-bar {}]
:main [admin-excel-import-content]}]) :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]}] (defn table [{:keys [status data-page]}]
(let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page]) (let [{:keys [data]} @(re-frame/subscribe [::data-page/page data-page])
params @(re-frame/subscribe [::params]) params @(re-frame/subscribe [::params])
is-admin? @(re-frame/subscribe [::subs/is-admin?])
statuses @(re-frame/subscribe [::status/multi ::refresh])] statuses @(re-frame/subscribe [::status/multi ::refresh])]
[grid/grid {:data-page data-page [grid/grid {:data-page data-page
:column-count 5} :column-count 5}
@@ -105,5 +106,6 @@
[:li (:name a) [:div.tag (->$ (:balance a))]])]] [:li (:name a) [:div.tag (->$ (:balance a))]])]]
[grid/cell {} [grid/cell {}
[:div.buttons [:div.buttons
[buttons/fa-icon {:event [::delete-requested (:id c)] (when is-admin?
:icon "fa-times"}]]]])]]])) [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 (ns auto-ap.views.pages.admin.rules.form
(:require (:require
[auto-ap.entities.transaction-rule :as entity]
[auto-ap.events :as events] [auto-ap.events :as events]
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [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 [auto-ap.views.components.expense-accounts-field
:as 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.layouts :as layouts]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.level :refer [left-stack]]
[auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.pages.admin.rules.common :refer [default-read]] [auto-ap.views.pages.admin.rules.common :refer [default-read]]
[auto-ap.views.pages.admin.rules.results-modal :as results-modal] [auto-ap.views.pages.admin.rules.results-modal :as results-modal]
[auto-ap.views.utils :refer [dispatch-event with-user]] [auto-ap.views.utils :refer [coerce-float dispatch-event with-user]]
[clojure.spec.alpha :as s]
[clojure.string :as str] [clojure.string :as str]
[malli.core :as m]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r] [reagent.core :as r]
[vimsical.re-frame.cofx.inject :as inject] [vimsical.re-frame.cofx.inject :as inject]
@@ -29,7 +28,6 @@
::default-note ::default-note
:<- [::forms/form ::form] :<- [::forms/form ::form]
(fn [{{:keys [client description amount-lte amount-gte dom-lte dom-gte]} :data}] (fn [{{:keys [client description amount-lte amount-gte dom-lte dom-gte]} :data}]
(str/join " - " (filter (complement str/blank?) (str/join " - " (filter (complement str/blank?)
[(:code client) [(:code client)
description description
@@ -47,12 +45,6 @@
(when dom-lte (when dom-lte
(str "<" 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 (re-frame/reg-sub
::query ::query
:<- [::forms/form ::form] :<- [::forms/form ::form]
@@ -89,10 +81,6 @@
(assoc :bank-account-id (:id (:bank-account data))))} (assoc :bank-account-id (:id (:bank-account data))))}
default-read]}]})) default-read]}]}))
(defn ungraphql-transaction-rule [x]
(-> x
(update :amount-lte #(some-> % js/parseFloat))
(update :amount-gte #(some-> % js/parseFloat))))
(re-frame/reg-sub (re-frame/reg-sub
::test-query ::test-query
@@ -160,6 +148,9 @@
:accounts :accounts
:yodlee-merchant :yodlee-merchant
:transaction-approval-status]) :transaction-approval-status])
(update :amount-lte coerce-float)
(update :amount-gte coerce-float)
(update :accounts (fn [xs] (update :accounts (fn [xs]
(mapv #(-> % (mapv #(-> %
(assoc :amount-percentage (* (:percentage %) 100.0))) (assoc :amount-percentage (* (:percentage %) 100.0)))
@@ -185,18 +176,21 @@
:on-success [::changed [:vendor-preferences]]}]}))) :on-success [::changed [:vendor-preferences]]}]})))
(re-frame/reg-event-db (re-frame/reg-event-db
::changed ::changed
(forms/change-handler ::form (forms/change-handler ::form
(fn [data field value] (fn [data field value]
(cond (and (= [:vendor-preferences] field) (cond (and (= [:vendor-preferences] field)
value value
(expense-accounts-field/can-replace-with-default? (:accounts data))) (expense-accounts-field/can-replace-with-default? (:accounts data)))
[[:accounts] (expense-accounts-field/default-account (:accounts data) [[:accounts] (expense-accounts-field/default-account (:accounts data)
(:default-account value) (:default-account value)
(:total data) (:total data)
[])] [])]
:else
[])))) (= [:client] field)
[[:bank-account] nil]
:else
[]))))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::saving ::saving
@@ -205,9 +199,9 @@
{:graphql {:graphql
{:token user {:token user
:query-obj query :query-obj query
:owns-state {:single ::form}
:on-success (fn [result] :on-success (fn [result]
[::updated (:upsert-transaction-rule result)]) [::updated (:upsert-transaction-rule result)])}}))
:on-error [::forms/save-error ::form]}}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::test-clicked ::test-clicked
@@ -223,9 +217,8 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::updated ::updated
[(forms/triggers-stop ::form)] [(forms/triggers-stop ::form)]
(fn [{:keys [db]} [_ {:keys [rule-saved]} result]] (fn [{:keys [db]} _]
{:db (forms/start-form db ::form {:client @(re-frame/subscribe [::subs/client])}) {:db (forms/start-form db ::form {:client @(re-frame/subscribe [::subs/client])})}))
:dispatch (conj rule-saved (:upsert-transaction-rule result))}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::succeeded-test ::succeeded-test
@@ -238,10 +231,6 @@
;; VIEWS ;; VIEWS
(def rule-form (forms/vertical-form {:can-submit [::can-submit]
:change-event [::changed]
:submit-event [::saving ]
:id ::form}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::mounted ::mounted
@@ -261,133 +250,119 @@
{::track/dispose [{:id ::client} {::track/dispose [{:id ::client}
{:id ::vendor-change}]})) {: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 ])} [layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
(let [{:keys [data id]} @(re-frame/subscribe [::forms/form ::form]) (let [{:keys [data]} @(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])
default-note @(re-frame/subscribe [::default-note]) test-state @(re-frame/subscribe [::status/single ::test])]
test-state @(re-frame/subscribe [::status/single ::test])] [form-builder/builder {:change-event [::changed]
^{:key id} :submit-event [::saving ]
(form-inline (assoc params :title "New Transaction Rule") :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]) [form-builder/field-v2 {:field [:bank-account]}
:auto-focus true "Bank account"
:entity->text :name [com/entity-typeahead {:entities @(re-frame/subscribe [::subs/real-bank-accounts-for-client (:client data)])
:type "typeahead-v3" :entity->text :name}]]
:field [:client]
:spec ::entity/client}])
[form-builder/field-v2 {:field :description
(with-meta :required? true}
(field "Bank account" [:span "Description (" [:a {:href "https://regex101.com" :target "_new"} "regex tester"] ")" ]
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/real-bank-accounts-for-client (:client data)]) [:input.input {:type "text"}]]
: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)))})
(field "Yodlee Merchant" [:div.field
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/yodlee-merchants]) [:p.help "Amount"]
:entity->text #(str (:name %) " - " (:yodlee-id %)) [left-stack
:type "typeahead-v3" [form-builder/raw-field-v2 {:field :amount-gte}
:field [:yodlee-merchant]}]) [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"] ")" ] [:div.field
[:input.input {:type "text" [:p.help "Day of month"]
:field [:description] [left-stack
:spec ::entity/description}]) [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 [:h2.title.is-4 "Outcomes"]
[: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"}])]]]]
[:div.field [form-builder/field-v2 {:field :vendor}
[:p.help "Day of Month"] "Assign Vendor"
[:div.control [com/search-backed-typeahead {:search-query (fn [i]
[:div.columns [:search_vendor
[:div.column {:query i}
(raw-field [:name :id]])}]]
[: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"}])]]]]
[: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" [form-builder/field-v2 {:field :transaction-approval-status}
[search-backed-typeahead {:search-query (fn [i] "Approval Status"
[:search_vendor [com/button-radio-input
{:query i} {:options [[:unapproved "Unapproved"]
[:name :id]]) [:requires-feedback "Client Review"]
:type "typeahead-v3" [:approved "Approved"]
:field [:vendor]}]) [:excluded "Excluded from Ledger"]]}]]
(with-meta [form-builder/field-v2 {:field :note}
(field nil "Note"
[expense-accounts-field {:type "expense-accounts" [:input.input {:type "text"
:descriptor "account asssignment" :placeholder default-note}]]
: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))})
(field "Approval Status" [:div.is-divider]
[button-radio [form-builder/error-notification]
{:type "button-radio" [:div.columns
:field [:transaction-approval-status] [:div.column
:options [[:unapproved "Unapproved"] [:a.button.is-medium.is-fullwidth.is-outlined {:on-click (dispatch-event [::test-clicked])
[:requires-feedback "Client Review"] :disabled (status/disabled-for test-state)
[:approved "Approved"] :class (status/class-for test-state)}
[:excluded "Excluded from Ledger"]]}]) "Test Rule"]]
[:div.column
(field "Note" [form-builder/submit-button {:class ["is-fullwidth"]}
[:input.input {:type "text" "Save"]]]]])])
: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")]]]))])
(defn form [_] (defn form [_]
(r/create-class (r/create-class

View File

@@ -1,25 +1,15 @@
(ns auto-ap.views.pages.admin.users (ns auto-ap.views.pages.admin.users
(:require [re-frame.core :as re-frame] (:require
[reagent.core :as reagent] [auto-ap.effects.forward :as forward]
[clojure.string :as str] [auto-ap.status :as status]
[auto-ap.subs :as subs] [auto-ap.utils :refer [replace-by]]
[auto-ap.events :as events] [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
[auto-ap.entities.clients :as entity] [auto-ap.views.components.grid :as grid]
[auto-ap.views.components.address :refer [address-field]] [auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.pages.admin.users.form :as form]
[auto-ap.views.pages.admin.users.table :as table] [auto-ap.views.pages.admin.users.table :as table]
[auto-ap.views.components.layouts :refer [side-bar-layout]] [re-frame.core :as re-frame]
[auto-ap.views.utils :refer [login-url dispatch-value-change bind-field horizontal-field dispatch-event]] [reagent.core :as reagent]))
[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]))
(re-frame/reg-sub (re-frame/reg-sub
::params ::params

View File

@@ -1,129 +1,104 @@
(ns auto-ap.views.pages.admin.users.form (ns auto-ap.views.pages.admin.users.form
(:require [re-frame.core :as re-frame] (:require
[reagent.core :as reagent] [auto-ap.forms :as forms]
[clojure.string :as str] [auto-ap.forms.builder :as form-builder]
[auto-ap.subs :as subs] [auto-ap.schema :as schema]
[auto-ap.events :as events] [auto-ap.status :as status]
[auto-ap.entities.clients :as entity] [auto-ap.subs :as subs]
[auto-ap.views.components.address :refer [address-field]] [auto-ap.views.components :as com]
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] [auto-ap.views.components.modal :as modal]
[auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.utils :refer [dispatch-event with-user]]
[auto-ap.views.utils :refer [login-url dispatch-value-change bind-field horizontal-field dispatch-event with-user]] [malli.core :as m]
[auto-ap.views.components.grid :as grid] [re-frame.core :as re-frame]))
[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]))
(re-frame/reg-sub (def client-schema
::can-submit (m/schema [:map [:client schema/reference]]))
(fn [db]
true))
(re-frame/reg-event-db (def user-schema
::changed (m/schema
(forms/change-handler ::form [:map
(fn [data field value] [:name schema/not-empty-string]
[]))) [:role [:enum :none :user :manager :power_user :admin]]
[:clients {:optional true}
[:maybe
[:sequential client-schema]]]]))
(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)) %))))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::saving ::saving
[with-user (forms/in-form ::form)] [with-user (forms/in-form ::form)]
(fn [{:keys [db user]} [_]] (fn [{:keys [db user]} [_]]
{:graphql (if (m/validate user-schema (:data db))
{:token user {:graphql
:owns-state {:single ::form} {:token user
:query-obj {:venia/operation {:operation/type :mutation :owns-state {:single ::form}
:operation/name "EditUser"} :query-obj {:venia/operation {:operation/type :mutation
:venia/queries [{:query/data [:edit-user :operation/name "EditUser"}
{:edit-user (-> (:data db) :venia/queries [{:query/data [:edit-user
(update :clients #(map :id %)) {:edit-user (-> (:data db)
(select-keys #{:id :name :clients :role}))} (update :clients #(map (comp :id :client) %))
[:id :name :role [:clients [:id :name]]]]}]} (select-keys #{:id :name :clients :role}))}
:on-success [::saved]}})) [: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 (re-frame/reg-event-fx
::saved ::saved
(forms/triggers-stop ::form) (forms/triggers-stop ::form)
(fn [{:keys [db]} [_ {:keys [edit-user]}]] (fn [_ _]
{:dispatch [::modal/modal-closed]})) {:dispatch [::modal/modal-closed]}))
(def user-form (forms/vertical-form {:submit-event [::saving]
:change-event [::changed]
:can-submit [::can-submit]
:id ::form}))
(defn form [] (defn form []
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
{:keys [form-inline field raw-field error-notification submit-button]} user-form] clients @(re-frame/subscribe [::subs/clients])]
(form-inline {} [:<>
[:<> [form-builder/builder {:submit-event [::saving]
(field "Name" :id ::form
[:input.input {:type "text" :schema user-schema}
:field [:name] [form-builder/field-v2 {:required? true
:spec ::entity/name}]) :field :name}
[:div.field "Name"
[:p.help "Role"] [:input.input {:type "text"}]]
[:div.control [form-builder/field-v2 {:required? true
[:div.select :field :role}
[raw-field "Role"
[:select {:type "select" [com/select-field {:options [[:none "None"]
:field [:role]} [:user "User"]
[:option {:value ":none"} "None"] [:manager "Manager"]
[:option {:value ":user"} "User"] [:power_user "Power User"]
[:option {:value ":manager"} "Manager"] [:admin "Admin"]]
[:option {:value ":power_user"} "Power User"] :allow-nil? false
[:option {:value ":admin"} "Admin"]]]]]] :keywordize? true}]]
(when (#{":user" ":manager" ":power_user"} (:role data)) (when (#{:user :manager :power_user} (:role data))
[:div.field [form-builder/field-v2 {:field :clients}
[:p.help "Clients"] "Client"
[:div.control [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :client}
[:div.field.has-addons [com/entity-typeahead
[:div.control {:entities clients
[:div.select :entity->text :name
[raw-field :style {:width "13em"}}]]]
[:select {:type "select" :key-fn :id
:field [:adding-client]} :schema [:sequential client-schema]
[:option] :new-text "Grant access to client"}]])
(let [used-clients (set (map :id (:clients data)))]
(for [{:keys [id name] :as client} @(re-frame/subscribe [::subs/clients]) [form-builder/hidden-submit-button]]]))
:when (not (used-clients id))]
^{:key id} [:option {:value id} name]))]]]]
[:p.control
[:button.button.is-primary {:on-click (dispatch-event [::add-client])} "Add"]]]
[: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 (re-frame/reg-event-fx
::editing ::editing
(fn [{:keys [db]} [_ d]] (fn [{:keys [db]} [_ d]]
{:db (-> db {:db (-> db
(forms/start-form ::form d)) (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)) :dispatch [::modal/modal-requested {:title (str "Edit user " (:name d))
:body [form] :body [form]
:cancel? false :cancel? false
:confirm {:value "Save" :confirm {:value "Save"
:status-from [::status/single ::form] :status-from [::status/single ::form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::saving]) :on-click (dispatch-event [::saving])
:close-event [::status/completed ::form]}}]})) :close-event [::status/completed ::form]}}]}))

View File

@@ -1,65 +1,53 @@
(ns auto-ap.views.pages.admin.vendors.merge-dialog (ns auto-ap.views.pages.admin.vendors.merge-dialog
(:require (:require
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components :as com]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]]
[auto-ap.views.utils :refer [dispatch-event]] [auto-ap.views.utils :refer [dispatch-event]]
[malli.core :as m]
[re-frame.core :as re-frame])) [re-frame.core :as re-frame]))
(re-frame/reg-sub (def merge-schema
::can-submit (m/schema [:map
:<- [::forms/form ::form] [:from schema/reference]
(fn [{:keys [data]}] [:to schema/reference]]))
(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}))
(defn form [] (defn form []
(let [_ @(re-frame/subscribe [::forms/form ::form]) [form-builder/builder {:submit-event [::try-save]
{:keys [form-inline field]} merge-form] :id ::form
:schema merge-schema}
[form-builder/field-v2 {:field :from}
(form-inline {} "Form Vendor (will be deleted)"
[:<> [com/search-backed-typeahead {:search-query (fn [i]
(field "Form Vendor (will be deleted)" [:search_vendor
[search-backed-typeahead {:search-query (fn [i] {:query i}
[:search_vendor [:name :id]])
{:query i} :auto-focus true}]]
[:name :id]])
:type "typeahead-v3"
:auto-focus true
:field [:from]}])
(field "To Vendor" [form-builder/field-v2 {:field :to}
[search-backed-typeahead {:search-query (fn [i] "To Vendor"
[:search_vendor [com/search-backed-typeahead {:search-query (fn [i]
{:query i} [:search_vendor
[:name :id]]) {:query i}
:type "typeahead-v3" [:name :id]])}]]
:field [:to]}])]))) [form-builder/hidden-submit-button]])
(re-frame/reg-event-fx (re-frame/reg-event-fx
::show ::show
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
{:dispatch [::modal/modal-requested {:title "Merge Vendors" {:dispatch [::modal/modal-requested {:title "Merge Vendors"
:body [form] :body [form]
:confirm {:value "Merge" :confirm {:value "Merge"
:status-from [::status/single ::form] :status-from [::status/single ::form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::save]) :on-click (dispatch-event [::try-save])
:can-submit [::can-submit]
:close-event [::status/completed ::form]}}] :close-event [::status/completed ::form]}}]
:db (forms/start-form db ::form {})} :db (forms/start-form db ::form {})}))
))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::complete ::complete
@@ -81,3 +69,12 @@
{:from (:id from) :to (:id to)} []]}]} {:from (:id from) :to (:id to)} []]}]}
:on-success [::complete]}}))) :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 (ns auto-ap.views.pages.admin.yodlee2
(:require [re-frame.core :as re-frame] (:require
[auto-ap.forms :as forms] [auto-ap.effects.forward :as forward]
[reagent.core :as reagent] [auto-ap.status :as status]
[clojure.string :as str] [auto-ap.subs :as subs]
[cljs-time.format :as f] [auto-ap.views.components.admin.side-bar :refer [admin-side-bar]]
[cljs-time.core :as time] [auto-ap.views.components.grid :as grid]
[auto-ap.subs :as subs] [auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.events :as events] [auto-ap.views.pages.admin.yodlee2.table :as table]
[auto-ap.entities.clients :as entity] [auto-ap.views.utils :refer [dispatch-event]]
[auto-ap.views.components.layouts :refer [side-bar-layout]] [re-frame.core :as re-frame]
[auto-ap.views.components.admin.side-bar :refer [admin-side-bar]] [reagent.core :as reagent]))
[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]))
(re-frame/reg-sub (re-frame/reg-sub
::authentication ::authentication

View File

@@ -1,9 +1,9 @@
(ns auto-ap.views.pages.admin.plaid (ns auto-ap.views.pages.company.plaid
(:require (:require
[auto-ap.effects.forward :as forward] [auto-ap.effects.forward :as forward]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [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.grid :as grid]
[auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.views.components.layouts :refer [side-bar-layout]]
[auto-ap.views.pages.admin.plaid.table :as table] [auto-ap.views.pages.admin.plaid.table :as table]
@@ -85,7 +85,8 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::unmounted ::unmounted
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
{::forward/dispose {:id ::plaid-item-deleted}})) {::forward/dispose {:id ::plaid-item-deleted}
::track/dispose [{:id ::params}]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@@ -146,19 +147,17 @@
(defn plaid-link-token-button [] (defn plaid-link-token-button []
(let [status @(re-frame/subscribe [::status/single ::get-link-token]) (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) [:button.button.is-primary {:disabled (status/disabled-for status)
:class (status/class-for status) :class (status/class-for status)
:on-click (dispatch-event [::get-link-token client-code])} :on-click (dispatch-event [::get-link-token (:code client)])}
"Authenticate with Plaid (" client-code ")"])) "Authenticate with Plaid (" (:name client) ")"]))
(defn link-flow [] (defn link-flow []
[:div [:div
(let [link-token @(re-frame/subscribe [::link-token]) (let [link-token @(re-frame/subscribe [::link-token])
client-code (:code @(re-frame/subscribe [::subs/client]))] client-code (:code @(re-frame/subscribe [::subs/client]))]
(cond (cond
(and link-token client-code) (and link-token client-code)
[:div [:div
"Authentication successful!" "Authentication successful!"
@@ -185,12 +184,12 @@
])) ]))
(defn admin-plaid-page [] (defn plaid-page []
(reagent/create-class (reagent/create-class
{:component-will-unmount #(re-frame/dispatch [::unmounted]) {:component-will-unmount #(re-frame/dispatch [::unmounted])
:component-did-mount #(re-frame/dispatch [::mounted]) :component-did-mount #(re-frame/dispatch [::mounted])
:reagent-render (fn [] :reagent-render (fn []
[side-bar-layout {:side-bar [admin-side-bar {}] [side-bar-layout {:side-bar [company-side-bar {}]
:main [admin-plaid-item-content]}])})) :main [admin-plaid-item-content]}])}))

View File

@@ -14,4 +14,8 @@
[:a.item {:href (bidi/path-for routes/routes :reports) [:a.item {:href (bidi/path-for routes/routes :reports)
:class [(active-when ap = :reports)]} :class [(active-when ap = :reports)]}
[:span {:class "icon icon-receipt" :style {:font-size "25px"}}] [: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 (ns auto-ap.views.pages.invoices.advanced-print-checks
(:require [auto-ap.forms :as forms] (:require
[auto-ap.status :as status] [auto-ap.forms :as forms]
[auto-ap.subs :as subs] [auto-ap.forms.builder :as form-builder]
[auto-ap.utils :refer [by]] [auto-ap.schema :as schema]
[auto-ap.views.components.modal :as modal] [auto-ap.status :as status]
[auto-ap.views.pages.invoices.common :refer [invoice-read does-amount-exceed-outstanding?]] [auto-ap.subs :as subs]
[auto-ap.views.pages.invoices.form :as form] [auto-ap.views.components :as com]
[auto-ap.views.utils :refer [dispatch-event horizontal-field with-user]] [auto-ap.views.components.modal :as modal]
[re-frame.core :as re-frame])) [auto-ap.views.components.money-field :refer [money-field]]
[auto-ap.views.pages.invoices.common
(re-frame/reg-sub :refer [does-amount-exceed-outstanding? invoice-read]]
::can-submit [auto-ap.views.utils :refer [coerce-float dispatch-event with-user]]
:<- [::forms/form ::form] [malli.core :as m]
(fn [{ {:keys [invoices invoice-amounts]} :data}] [malli.error :as me]
(cond (seq (filter [re-frame.core :as re-frame]))
(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}))
(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 [] (defn form []
(let [real-bank-accounts @(re-frame/subscribe [::subs/real-bank-accounts]) (let [real-bank-accounts @(re-frame/subscribe [::subs/real-bank-accounts])
{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) {:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} advanced-print-checks-form]
(form-inline {} [form-builder/builder {:submit-event [::try-save]
[:<> :id ::form
[:div.field :schema advanced-print-schema}
[:label.label "Pay using"] [form-builder/field-v2 {:field :bank-account-id}
[:div.control "Pay using"
[:span.select [com/select-field {:options (for [{:keys [id name]} real-bank-accounts]
[raw-field [id name])
[:select {:type "select" :allow-nil? true}]]
:field :bank-account-id}
(for [{:keys [id number name]} real-bank-accounts]
^{:key id} [:option {:value id} name])]]]]]
[:table.table.is-fullwidth [:table.table.is-fullwidth
[:thead [:thead
[:tr [:tr
[:th "Vendor"] [:th "Vendor"]
[:th "Invoice ID"] [:th "Invoice ID"]
[:th {:style {"width" "10em"}} "Payment"]]] [:th {:style {"width" "10em"}} "Payment"]]]
[:tbody [:tbody
(doall (doall
(for [{:keys [vendor payment outstanding-balance invoice-number id] :as i} (:invoices data)] (for [{:keys [vendor invoice-number id]} (:invoices data)]
^{:key id} ^{:key id}
[:tr [:tr
[:td (:name vendor)] [:td (:name vendor)]
[:td invoice-number] [:td invoice-number]
[:td [:div.field.has-addons.is-extended [:td
[:p.control [:a.button.is-static "$"]] [form-builder/raw-field-v2 {:field [:invoice-amounts id :amount]}
[:p.control [money-field]]]]))]]]))
(raw-field
[:input.input.has-text-right {:type "number"
:field [:invoice-amounts id :amount]
:step "0.01"}])]]]]))]]])))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::show ::show
(fn [{:keys [db]} [_ invoices]] (fn [{:keys [db]} [_ invoices]]
{:dispatch [::modal/modal-requested {:title "Print Checks" {:dispatch [::modal/modal-requested {:title "Print Checks"
:body [form] :body [form]
:confirm {:value "Print checks" :confirm {:value "Print checks"
:status-from [::status/single ::form] :status-from [::status/single ::form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::save]) :on-click (dispatch-event [::try-save])
:can-submit [::can-submit]
:close-event [::status/completed ::form]}}] :close-event [::status/completed ::form]}}]
:db (-> db :db (-> db
(forms/start-form ::form (forms/start-form ::form
{:bank-account-id (:id (first @(re-frame/subscribe [::subs/real-bank-accounts]))) {:invoices invoices
:invoices invoices
:invoice-amounts (into {} :invoice-amounts (into {}
(map (fn [i] [(:id i) (map (fn [i] [(:id i)
{:amount (:outstanding-balance i)}]) {:amount (coerce-float (:outstanding-balance i))}])
invoices))}))})) 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 (re-frame/reg-event-fx
::save ::save
[with-user (forms/in-form ::form) ] [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]) (let [type (or (->> @(re-frame/subscribe [::subs/client])
:bank-accounts :bank-accounts
(filter #(= bank-account-id (:id %))) (filter #(= (:bank-account-id (:data db)) (:id %)))
first first
:type) :type)
:check) :check)
{:keys [date invoices invoice-amounts check-number bank-account-id client]} (:data db)] {:keys [invoices invoice-amounts bank-account-id]} (:data db)]
{:graphql {:graphql
{:token user {:token user
:owns-state {:single ::form} :owns-state {:single ::form}
@@ -123,6 +127,6 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::checks-printed ::checks-printed
(fn [{:keys [db]} [_ data]] (fn [{:keys [_]} [_ _]]
{:dispatch [::modal/modal-closed]})) {:dispatch [::modal/modal-closed]}))

View File

@@ -1,6 +1,5 @@
(ns auto-ap.views.pages.invoices.form (ns auto-ap.views.pages.invoices.form
(:require (:require
[auto-ap.entities.invoice :as invoice]
[auto-ap.events :as events] [auto-ap.events :as events]
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder] [auto-ap.forms.builder :as form-builder]
@@ -8,10 +7,11 @@
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.time-utils :refer [next-dom]] [auto-ap.time-utils :refer [next-dom]]
[auto-ap.utils :refer [dollars=]] [auto-ap.utils :refer [dollars=]]
[auto-ap.schema :as schema]
[auto-ap.views.components.expense-accounts-field [auto-ap.views.components.expense-accounts-field
:as eaf :as eaf
:refer [recalculate-amounts :refer [recalculate-amounts
expense-accounts-field]] expense-accounts-field-v2]]
[auto-ap.views.components.layouts :as layouts] [auto-ap.views.components.layouts :as layouts]
[auto-ap.views.components.level :refer [left-stack]] [auto-ap.views.components.level :refer [left-stack]]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
@@ -20,19 +20,33 @@
[auto-ap.views.components.switch-field :refer [switch-field]] [auto-ap.views.components.switch-field :refer [switch-field]]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components.typeahead :refer [typeahead-v3]]
[auto-ap.views.components.typeahead.vendor [auto-ap.views.components.typeahead.vendor
:refer [search-backed-typeahead]] :refer [search-backed-typeahead]]
[auto-ap.views.pages.invoices.common :refer [invoice-read]] [auto-ap.views.pages.invoices.common :refer [invoice-read]]
[auto-ap.views.utils [auto-ap.views.utils
:refer [date-picker-optional :refer [date-picker
dispatch-event dispatch-event
with-user]] with-user]]
[cljs-time.core :as c] [cljs-time.core :as c]
[clojure.spec.alpha :as s]
[clojure.string :as str] [clojure.string :as str]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[reagent.core :as r] [reagent.core :as r]
[malli.core :as m]
[vimsical.re-frame.cofx.inject :as inject] [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 ;; SUBS
(re-frame/reg-sub (re-frame/reg-sub
@@ -42,11 +56,11 @@
(let [min-total (if (= (:total (:original data)) (:outstanding-balance (:original data))) (let [min-total (if (= (:total (:original data)) (:outstanding-balance (:original data)))
nil nil
(- (:total (:original data)) (:outstanding-balance (:original data)))) (- (:total (:original data)) (:outstanding-balance (:original data))))
account-total (reduce + 0 (map (fn [ea] (js/parseFloat (:amount ea))) (:expense-accounts data)))] account-total (reduce + 0 (map :amount (:expense-accounts data)))]
(and (s/valid? ::invoice/invoice data) (and
(or (not min-total) (>= (:total data) min-total)) (or (not min-total) (>= (:total data) min-total))
(or (not (:id data)) (or (not (:id data))
(dollars= (Math/abs (js/parseFloat (:total data))) (Math/abs account-total))))))) (dollars= (Math/abs (:total data)) (Math/abs account-total)))))))
(re-frame/reg-sub (re-frame/reg-sub
::create-query ::create-query
@@ -143,8 +157,8 @@
:vendor (:vendor edit-invoice) :vendor (:vendor edit-invoice)
:client (:client edit-invoice) :client (:client edit-invoice)
:expense-accounts (eaf/from-graphql (:expense-accounts which) :expense-accounts (eaf/from-graphql (:expense-accounts which)
(:total which) (:total which)
locations-for-client)}))}))) locations-for-client)}))})))
@@ -321,7 +335,8 @@
[form-builder/builder {:can-submit [::can-submit] [form-builder/builder {:can-submit [::can-submit]
:change-event [::changed] :change-event [::changed]
:submit-event [::save-requested [::saving ]] :submit-event [::save-requested [::saving ]]
:id ::form} :id ::form
:schema schema}
[form-builder/section {:title [:div "New Invoice " [form-builder/section {:title [:div "New Invoice "
(cond (cond
@@ -344,71 +359,61 @@
nil)]} nil)]}
(when-not active-client (when-not active-client
[form-builder/field {:required? true} [form-builder/field-v2 {:required? true
:field [:client]}
"Client" "Client"
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) [typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients])
:entity->text :name :entity->text :name
:type "typeahead-v3" :style {:width "18em"}
:auto-focus (if active-client false true) :auto-focus (if active-client false true)
:field [:client]
:disabled exists?}]]) :disabled exists?}]])
[form-builder/field {:required? true} [form-builder/field-v2 {:required? true
:field [:vendor]}
"Vendor" "Vendor"
[search-backed-typeahead {:disabled exists? [search-backed-typeahead {:disabled exists?
:search-query (fn [i] :search-query (fn [i]
[:search_vendor [:search_vendor
{:query i} {:query i}
[:name :id]]) [:name :id]])
:type "typeahead-v3" :style {:width "18em"}
:auto-focus (if active-client true false) :auto-focus (if active-client true false)}]]
:field [:vendor]}]] [form-builder/field-v2 {:required? true
[form-builder/vertical-control {:required? true} :field :date}
"Date" "Date"
[:label [date-picker {:output :cljs-date}]]
[form-builder/raw-field
[date-picker-optional {:type "date2"
:field [:date]
:output :cljs-date}]]]]
[form-builder/field [form-builder/field-v2 {:field [:due]}
"Due (optional)" "Due (optional)"
[date-picker-optional {:type "date2" [date-picker {:output :cljs-date}]]
:field [:due]
:output :cljs-date}]]
[form-builder/vertical-control [form-builder/vertical-control
"Scheduled payment (optional)" "Scheduled payment (optional)"
[left-stack [left-stack
[:div.control [:div.control
[form-builder/raw-field [form-builder/raw-field-v2 {:field :scheduled-payment}
[date-picker-optional {:type "date2" [date-picker {:output :cljs-date}]]
:field [:scheduled-payment] [form-builder/raw-error-v2 {:field :scheduled-payment}]]
:output :cljs-date}]]]
[:div.control [:div.control
[form-builder/raw-field [form-builder/raw-field-v2 {:field :schedule-when-due}
[switch-field {:id "schedule-when-due" [com/switch-input {:id "schedule-when-due"
:field [:schedule-when-due] :label "Same as due date"}]]]]]
:label "Same as due date" [form-builder/field-v2 {:required? true
:type "checkbox"}]]]]] :field :invoice-number}
[form-builder/field {:required? true}
"Invoice #" "Invoice #"
[:input.input {:type "text" [:input.input {:style {:width "12em"}}]]
:field [:invoice-number]}]]
[form-builder/field {:required? true} [form-builder/field-v2 {:required? true
:field :total}
"Total" "Total"
[money-field {:type "money" [money-field {:disabled (if can-change-amount? "" "disabled")
:field [:total]
:disabled (if can-change-amount? "" "disabled")
:style {:max-width "8em"} :style {:max-width "8em"}
:min min-total :min min-total}]]]
:step "0.01"}]]] [form-builder/field-v2 {:field :expense-accounts}
[form-builder/raw-field "Expense Accounts"
[expense-accounts-field {:type "expense-accounts" [expense-accounts-field-v2 {:descriptor "expense account"
:descriptor "expense account" :locations (:locations (:client data))
:locations (:locations (:client data)) :max (:total data)
:max (:total data) :client (or (:client data) active-client)}]]
:client (or (:client data) active-client)
:field [:expense-accounts]}]]
[form-builder/error-notification] [form-builder/error-notification]
[:div {:style {:margin-bottom "1em"}}] [:div {:style {:margin-bottom "1em"}}]
[:div.columns [:div.columns
@@ -433,6 +438,7 @@
(list (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 "-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]))))]]]) ^{:key (str id "-debit")} [:a.dropdown-item {:on-click (dispatch-event [::save-requested [::add-and-print id :debit]])} "Debit from " name]))))]]])
[:div.column [:div.column
[form-builder/submit-button {:class ["is-fullwidth"]} [form-builder/submit-button {:class ["is-fullwidth"]}
"Save"]]]])]) "Save"]]]])])

View File

@@ -1,84 +1,74 @@
(ns auto-ap.views.pages.invoices.handwritten-checks (ns auto-ap.views.pages.invoices.handwritten-checks
(:require [auto-ap.entities.invoice :as invoice] (:require
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.status :as status] [auto-ap.forms.builder :as form-builder]
[auto-ap.subs :as subs] [auto-ap.status :as status]
[auto-ap.utils :refer [by]] [auto-ap.subs :as subs]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.pages.data-page :as data-page] [auto-ap.views.components.money-field :refer [money-field]]
[auto-ap.views.pages.invoices.common [auto-ap.views.pages.invoices.common
:refer :refer [does-amount-exceed-outstanding? invoice-read]]
[does-amount-exceed-outstanding? invoice-read]] [auto-ap.views.utils
[auto-ap.views.pages.invoices.form :as form] :refer [date-picker dispatch-event with-user]]
[auto-ap.views.utils [clojure.string :as str]
:refer [re-frame.core :as re-frame]
[date-picker dispatch-event horizontal-field with-user]] [auto-ap.views.components :as com]
[clojure.string :as str] [malli.core :as m]
[re-frame.core :as re-frame])) [auto-ap.schema :as schema]))
(def handwrite-checks-form (forms/vertical-form {:submit-event [::save] (def handwritten-check-schema
:change-event [::forms/change ::form] (m/schema
:can-submit [::can-submit] [:map
:id ::form})) [:bank-account-id schema/not-empty-string]
[:date schema/date]
[:check-number [:int {:min 1000 :max 99999}]]
]))
(defn form [] (defn form []
(let [real-bank-accounts @(re-frame/subscribe [::subs/real-bank-accounts]) (let [real-bank-accounts @(re-frame/subscribe [::subs/real-bank-accounts])
{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) {:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} handwrite-checks-form] [form-builder/builder {:submit-event [::save]
(form-inline {} :can-submit [::can-submit]
[:<> :id ::form
[:div.field :schema handwritten-check-schema}
[:label.label "Pay using"] [form-builder/field-v2 {:field :bank-account-id}
[:div.control "Pay using"
[:span.select [com/select-field {:options (for [{:keys [id name]} real-bank-accounts]
[raw-field [id name])
[:select {:type "select" :allow-nil? true}]]
: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
(raw-field [form-builder/field-v2 {:field :date}
[:input.input.has-text-right {:type "number" "Date"
:field [:invoice-amounts id :amount] [date-picker {:type "date"
#_#_:max outstanding-balance :output :cljs-date}]]
:step "0.01"}])]]]]))]]])))
[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 (re-frame/reg-sub
::can-submit ::can-submit
:<- [::forms/form ::form] :<- [::forms/form ::form]
(fn [{ {:keys [check-number date invoices invoice-amounts]} :data}] (fn [{ {:keys [check-number date invoices invoice-amounts]} :data}]
(boolean (cond (seq (filter (boolean (cond (seq (filter
(fn [{:keys [id outstanding-balance]}] (fn [{:keys [id outstanding-balance]}]
(does-amount-exceed-outstanding? (get-in invoice-amounts [id :amount]) outstanding-balance )) (does-amount-exceed-outstanding? (get-in invoice-amounts [id :amount]) outstanding-balance ))
invoices)) invoices))
false false
:else :else
@@ -98,7 +88,7 @@
:close-event [::status/completed ::form]}}] :close-event [::status/completed ::form]}}]
:db (-> db :db (-> db
(forms/start-form ::form (forms/start-form ::form
{:bank-account-id (:id (first @(re-frame/subscribe [::subs/real-bank-accounts]))) {:bank-account-id nil
:invoices invoices :invoices invoices
:invoice-amounts (into {} :invoice-amounts (into {}
(map (fn [i] [(:id i) (map (fn [i] [(:id i)
@@ -132,6 +122,6 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::succeeded ::succeeded
[(forms/triggers-stop ::form)] [(forms/triggers-stop ::form)]
(fn [{:keys [db]} [_ invoices]] (fn [_ _]
{:dispatch [::modal/modal-closed]})) {: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.side-bar :refer [ledger-side-bar]]
[auto-ap.views.pages.ledger.table :as ledger-table] [auto-ap.views.pages.ledger.table :as ledger-table]
[auto-ap.views.utils [auto-ap.views.utils
:refer [date->str :refer [date-picker
date-picker-friendly
dispatch-event dispatch-event
local-now local-now
standard
with-user]] with-user]]
[cljs-time.core :as t] [cljs-time.core :as t]
[clojure.set :as set] [clojure.set :as set]
@@ -25,7 +23,9 @@
[reagent.core :as reagent] [reagent.core :as reagent]
[vimsical.re-frame.fx.track :as track] [vimsical.re-frame.fx.track :as track]
[vimsical.re-frame.cofx.inject :as inject] [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] (defn data-params->query-params [params]
(when params (when params
@@ -38,11 +38,6 @@
:to-numeric-code (:to-numeric-code params) :to-numeric-code (:to-numeric-code params)
:date-range (:date-range params)})) :date-range (:date-range params)}))
(re-frame/reg-sub
::can-submit
(fn [_]
true))
(re-frame/reg-sub (re-frame/reg-sub
::ledger-list-active? ::ledger-list-active?
@@ -78,9 +73,7 @@
:graphql {:token user :graphql {:token user
:query-obj {:venia/queries [[:balance-sheet :query-obj {:venia/queries [[:balance-sheet
(-> (:data db) (-> (:data db)
(assoc :client-id (:id client)) (assoc :client-id (:id client)))
(update :date (fnil #(date->str % standard) nil))
(update :comparison-date (fnil #(date->str % standard) nil)))
[[:balance-sheet-accounts [:name :amount :account-type :id :numeric-code]] [[:balance-sheet-accounts [:name :amount :account-type :id :numeric-code]]
[:comparable-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 :graphql {:token user
:query-obj {:venia/queries [[:balance-sheet-pdf :query-obj {:venia/queries [[:balance-sheet-pdf
(-> (:data db) (-> (:data db)
(assoc :client-id (:id client)) (assoc :client-id (:id client)))
(update :date (fnil #(date->str % standard) nil))
(update :comparison-date (fnil #(date->str % standard) nil)))
[:url :name]]]} [:url :name]]]}
:owns-state {:single ::page} :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 :from-numeric-code from-numeric-code
:to-numeric-code to-numeric-code :to-numeric-code to-numeric-code
:date-range {:start "2000-01-01" :date-range {:start "2000-01-01"
:end (date->str date-range standard)}}]})) :end date-range}}]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::ledger-params-change ::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])}})) :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 [] (defn report-form []
(let [{:keys [form-inline raw-field]} balance-sheet-form (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
{:keys [data]} @(re-frame/subscribe [::forms/form ::form])] [form-builder/builder {:change-event [::change]
(form-inline {} :submit-event [::report-requested]
[:div :id ::form}
[:div.report-controls [:div
[:div.level [:div.report-controls
[:div.level-left [:div.level
[:div.level-item [:div.level-left
[:div.control [:div.level-item
[:p.help "Date"] [:div.control
(raw-field [form-builder/field-v2 {:field :date}
[date-picker-friendly {:cljs-date? true "Date"
:type "date" [date-picker {:output :cljs-date}]]]]
:field [:date]}])]] [:div.level-item
[:div.level-item [form-builder/field-v2 {:field :include-comparison}
[:div.control [:div.mt-5]
[:div.mt-3] [com/switch-input {:id "include-comparison"
[switch-field {:id "include-comparison" :label "Include compariison"}]]]
:checked (:include-comparison data) [:div.level-item
:on-change (fn [e]
(re-frame/dispatch [::change [:include-comparison] (.-checked (.-target e))]))
:label "Include comparison"
:type "checkbox"}]]]
[:div.level-item
(when (boolean (:include-comparison data)) (when (boolean (:include-comparison data))
[:div.control [form-builder/field-v2 {:field :comparison-date}
[:p.help "Comparison Date"] "Comparison Date"
(raw-field [date-picker {:output :cljs-date}]])]]
[date-picker-friendly {:cljs-date? true [:div.level-right
:type "date" [:div.buttons
:field [:comparison-date]}])])]]
[:div.level-right
[:div.buttons
(when @(re-frame/subscribe [::subs/is-admin?]) (when @(re-frame/subscribe [::subs/is-admin?])
[:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"]) [:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
[:button.button.is-primary "Run"]]]]]]))) [:button.button.is-primary "Run"]]]]]]]))
(defn balance-sheet-report [{:keys [args report-data]}] (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] (map (fn [b]
(assoc b (assoc b
:period (:date args) :period (:date args)

View File

@@ -1,41 +1,29 @@
(ns auto-ap.views.pages.ledger.external-import (ns auto-ap.views.pages.ledger.external-import
(:require [auto-ap.subs :as subs] (:require
[auto-ap.views.components.layouts :refer [side-bar-layout]] [auto-ap.events :as events]
[goog.string :as gstring] [auto-ap.forms :as forms]
[auto-ap.forms :as forms] [auto-ap.forms.builder :as form-builder]
[auto-ap.utils :refer [by]] [auto-ap.status :as status]
[auto-ap.events :as events] [auto-ap.views.components :as com]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] [auto-ap.views.components.dropdown
[auto-ap.views.utils :refer [date->str date-picker bind-field local-now standard ->$ str->date dispatch-event]] :refer [drop-down drop-down-contents]]
[auto-ap.views.components.dropdown :refer [drop-down drop-down-contents]] [auto-ap.views.components.layouts :refer [side-bar-layout]]
[cljs-time.core :as t] [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[re-frame.core :as re-frame] [auto-ap.views.utils :refer [dispatch-event]]
[reagent.core :as r] [clojure.string :as str]
[clojure.string :as str] [re-frame.core :as re-frame]
[auto-ap.status :as status])) [reagent.core :as r]))
(defn line->id [{:keys [source id client-code]}]
(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}]
(str client-code "-" source "-" id)) (str client-code "-" source "-" id))
(re-frame/reg-sub (re-frame/reg-sub
::request ::request
:<- [::forms/form ::form] :<- [::forms/form ::form]
(fn [{{lines :line-items :as d} :data :as g}] (fn [{{lines :line-items} :data}]
(into [] (into []
(for [[external-id lines] (group-by line->id lines) (for [[_ lines] (group-by line->id lines)
:let [{:keys [source id client-code date vendor-name note cleared-against] :as line} (first lines)]] :let [{:keys [source client-code date vendor-name note cleared-against] :as line} (first lines)]]
{:source source {:source source
:external-id (line->id line) :external-id (line->id line)
:client-code client-code :client-code client-code
@@ -142,6 +130,8 @@
[:form.form [:form.form
(if value (if value
[:div [:div
[:a.button {:on-click #(on-change nil)}
"reset"]
[:table.table {:style {:width "100%"}} [:table.table {:style {:width "100%"}}
[:thead [:thead
[:tr [:tr
@@ -193,68 +183,72 @@
(def balance-sheet-content (def balance-sheet-content
(with-meta (with-meta
(fn [] (fn []
(let [current-client @(re-frame/subscribe [::subs/client]) (let [status @(re-frame/subscribe [::status/single ::import])
user @(re-frame/subscribe [::subs/user]) {:keys [data result]} @(re-frame/subscribe [::forms/form ::form]) ]
status @(re-frame/subscribe [::status/single ::import]) [form-builder/builder {:id ::form
{:keys [data result active? error id]} @(re-frame/subscribe [::forms/form ::form]) ] :submit-event [::importing]}
[:div [:div
[:div.level [:div.level
[:div.level-left [:div.level-left
[:h1.title "Eternal Import"]] [:h1.title "Eternal Import"]]
[:div.level-right [:div.level-right
[:button.button.is-primary.is-pulled-right.is-large {:disabled (or (not data) [form-builder/submit-button "Import"]]]
(= :loading (:state status ))) [status/status-notification {:statuses [[::status/single ::import]]} ]
:on-click (dispatch-event [::importing])} "Import"]]] (when result
[status/status-notification {:statuses [[::status/single ::import]]} ] [:div.notification
(when result "Imported with "
[:div.notification (count (:errors result)) " errors, "
"Imported with " (count (:ignored result)) " ignored, "
(count (:errors result)) " errors, " (count (:success result)) " successful."])
(count (:ignored result)) " ignored, " (if (= :loading (:state status ))
(count (:success result)) " successful."]) [status/big-loader status]
(if (= :loading (:state status )) [:div
[status/big-loader status] [:div.is-clearfix
[:div [:div.is-pulled-right
[:div.is-clearfix [form-builder/raw-field-v2 {:field :only-show-errors?}
[:div.is-pulled-right [com/checkbox {:label "Only show errors"}]]]]
[:label.checkbox [:div
[bind-field [form-builder/raw-field-v2 {:field :line-items}
[:input {:type "checkbox" [textarea->table {:headings [["Id" :id]
:event [::forms/change ::form] ["Client" :client-code]
:subscription data ["Source" :source]
:field [:only-show-errors?]}]] ["Vendor" :vendor-name]
"Only show errors"]]] ["Date" :date]
[:div ["Account" :account-identifier]
[bind-field ["Location" :location]
[textarea->table {:type "textarea->table" ["Debit" :debit]
:field [:line-items] ["Credit" :credit]
:headings [["Id" :id] ["Note" :note]
["Client" :client-code] ["Cleared against" :cleared-against]]
["Source" :source] :read-only-headings
["Vendor" :vendor-name] [["status" :status]]
["Date" :date]
["Account" :account-identifier]
["Location" :location]
["Debit" :debit]
["Credit" :credit]
["Note" :note]
["Cleared against" :cleared-against]]
:read-only-headings
[["status" :status]]
:row-filter :row-filter
(fn [{:keys [status-category]}] (fn [{:keys [status-category]}]
(if (:only-show-errors? data) (if (:only-show-errors? data)
(= :error status-category) (= :error status-category)
true)) true))}]]]])]]))
:event [::forms/change ::form]
:subscription data}
]]]])]))
{})) {}))
(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-layout
{:side-bar [ledger-side-bar] {:side-bar [ledger-side-bar]
:main [balance-sheet-content]}]) :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]] :refer [appearing-side-bar side-bar-layout]]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.components.switch-field :refer [switch-field]] [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.data-page :as data-page]
[auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]] [auto-ap.views.pages.ledger.side-bar :refer [ledger-side-bar]]
[auto-ap.views.pages.ledger.table :as ledger-table] [auto-ap.views.pages.ledger.table :as ledger-table]
[auto-ap.views.utils [auto-ap.views.utils
:refer [date->str :refer [date->str
date-picker-friendly date-picker
dispatch-event dispatch-event
local-today local-today
multi-field
query-params query-params
standard standard
str->date str->date
@@ -31,7 +29,9 @@
[react-dom :as react-dom] [react-dom :as react-dom]
[reagent.core :as reagent] [reagent.core :as reagent]
[vimsical.re-frame.cofx.inject :as inject] [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 (cond-> {:graphql {:token user
:owns-state {:single ::page} :owns-state {:single ::page}
:query-obj {:venia/queries [[:profit-and-loss :query-obj {:venia/queries [[:profit-and-loss
{:client-ids (map :id (:clients (:data db))) {:client-ids (map (comp :id :client) (:clients (:data db)))
:periods (mapv encode-period (:periods (:data db))) :periods (mapv #(select-keys % #{:start :end} ) (:periods (:data db)))
:include-deltas (:include-deltas (:data db)) :include-deltas (:include-deltas (:data db))
:column-per-location (:column-per-location (:data db))} :column-per-location (:column-per-location (:data db))}
[[:periods [[:accounts [:name :amount :client_id :account-type :id :count :numeric-code :location]]]]]]]} [[:periods [[:accounts [:name :amount :client_id :account-type :id :count :numeric-code :location]]]]]]]}
@@ -81,7 +81,7 @@
:set-uri-params {:periods (mapv :set-uri-params {:periods (mapv
encode-period encode-period
(:periods (:data db))) (:periods (:data db)))
:clients (mapv #(select-keys % [:name :id]) (:clients (:data db))) } :clients (mapv #(select-keys (:client %) [:name :id]) (:clients (:data db))) }
:db (-> db :db (-> db
(dissoc :report) (dissoc :report)
(update-in [:data :clients] #(into [] (filter seq %))))}))) (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 (cond-> {:graphql {:token user
:owns-state {:single ::page} :owns-state {:single ::page}
:query-obj {:venia/queries [[:profit-and-loss-pdf :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)) :include-deltas (:include-deltas (:data db))
:column-per-location (:column-per-location (: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]]]} [:url :name]]]}
:on-success [::received-pdf]} :on-success [::received-pdf]}
:set-uri-params {:periods (mapv encode-period (:periods (:data db))) :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)}))) :db (dissoc db :report)})))
@@ -223,10 +223,6 @@ NOTE: Please review the transactions we may have question for you here: https://
(fn [_] (fn [_]
true)) 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] (defn report-control-detail [{:keys [active box which]} children]
(when (and @box (when (and @box
@@ -243,191 +239,177 @@ NOTE: Please review the transactions we may have question for you here: https://
[:div.control [:div.control
[:a.button [:a.button
{:class (when (= selected-preset title) "is-active") {:class (when (= selected-preset title) "is-active")
:on-click (dispatch-event :on-click (fn []
[::change (re-frame/dispatch-sync [::change
[:periods] [:periods]
periods periods
[:selected-preset] title])} [:selected-preset] title])
(re-frame/dispatch-sync [::change
[:show-advanced?]
false]))}
title]])) title]]))
(defn report-controls [_] (defn report-controls []
(let [!box (reagent/atom nil) (let [!box (atom nil)
active (reagent/atom nil)] active (reagent/atom nil)]
(fn [pnl-form] (fn []
(let [{:keys [raw-field]} pnl-form (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
{:keys [data]} @(re-frame/subscribe [::forms/form ::form])
{:keys [periods selected-preset include-deltas column-per-location]} data] {:keys [periods selected-preset include-deltas column-per-location]} data]
[:div.report-controls [form-builder/builder {:can-submit [::can-submit]
[:div.level.mb-2 :change-event [::change]
[:div.level-left :submit-event [::report-requested]
[:div.level-item :id ::form}
[buttons/dropdown {:on-click (fn [] (reset! active :clients))} [:div.report-controls
[:span (str "Companies" [:div.level.mb-2
(when-let [clients (:clients data)] [:div.level-left
(str " (" (str/join ", " (map :name clients)) ")")))]] [:div.level-item
[report-control-detail {:active active :box !box :which :clients} [buttons/dropdown {:on-click (fn [] (reset! active :clients))}
[:div {:style {:width "20em"}} [:span (str "Companies"
[:h4.subtitle "Companies"] (when-let [clients (:clients data)]
[raw-field (str " (" (str/join ", " (map (comp :name :client) clients)) ")")))]]
[multi-field {:type "multi-field" [report-control-detail {:active active :box !box :which :clients}
:field [:clients] [:div {:style {:width "20em"}}
:template [[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) [:h4.subtitle "Companies"]
:entity->text :name [form-builder/raw-field-v2 {:field :clients}
:type "typeahead-v3"}]]}]] [com/multi-field-v2 {:new-text "Add another company"
]]] :template [[form-builder/raw-field-v2 {:field :client}
[:div.level-item [com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
[buttons/dropdown {:on-click (fn [] (reset! active :range))} :style {:width "18em"}
[:span (str "Range" :entity->text :name}]]]
(when selected-preset :key-fn :id}]]]]]
(str " (" selected-preset ")")))]] [:div.level-item
[report-control-detail {:active active :box !box :which :range} [buttons/dropdown {:on-click (fn [] (reset! active :range))}
[:div [:span (str "Range"
[:h4.subtitle "Range"] (when selected-preset
[:div.field.is-grouped (str " (" selected-preset ")")))]]
[:div.control [report-control-detail {:active active :box !box :which :range}
[:div.field.has-addons [:div
[:div.control [:h4.subtitle "Range"]
(raw-field [:div.field.is-grouped
[date-picker-friendly {:placeholder "End date" [:div.control
:type "date" [:div.field.has-addons
:cljs-date? true [:div.control
:field [:thirteen-periods-end]}])] [form-builder/raw-field-v2 {:field :thirteen-periods-end}
[period-preset-button {:title "13 periods" [date-picker {:placeholder "End date"
:periods (let [today (or (some-> (:thirteen-periods-end data)) :output :cljs-date}]]]
(local-today))] [period-preset-button {:title "13 periods"
(into :periods (let [today (or (some-> (:thirteen-periods-end data))
[{:start (t/plus (t/minus today (t/weeks (* 13 4))) (local-today))]
(t/days 1)) (into
:end today [{:start (t/plus (t/minus today (t/weeks (* 13 4)))
:title "Total"}]
(for [i (range 13)]
{:start (t/plus (t/minus today (t/weeks (* (inc i) 4)))
(t/days 1)) (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.control
[:div.field.has-addons [:div.field.has-addons
[:div.control [:div.control
(raw-field [form-builder/raw-field-v2 {:field :twelve-periods-end}
[date-picker-friendly {:placeholder "End date" [date-picker {:placeholder "End date"
:cljs-date? true :output :cljs-date}]]]
:type "date" [period-preset-button {:title "12 months"
:field [:twelve-periods-end]}])] :periods (let [end-date (or (some-> (:twelve-periods-end data))
[period-preset-button {:title "12 months" (local-today))
:periods (let [end-date (or (some-> (:twelve-periods-end data)) this-month (t/local-date (t/year end-date)
(local-today)) (t/month end-date)
this-month (t/local-date (t/year end-date) 1)]
(t/month end-date) (into
1)] [{:start (t/minus this-month (t/months 11))
(into :end (t/minus (t/plus this-month (t/months 1))
[{:start (t/minus this-month (t/months 11)) (t/days 1))
:end (t/minus (t/plus this-month (t/months 1)) :title "Total"}]
(t/days 1)) (for [i (range 12)]
:title "Total"}] {:start (t/minus this-month (t/months (- 11 i)))
(for [i (range 12)] :end (t/minus (t/minus this-month (t/months (- 10 i)))
{:start (t/minus this-month (t/months (- 11 i))) (t/days 1))})))}]]]
: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)] [period-preset-button {:periods (let [last-sunday (loop [current (local-today)]
(if (= 7 (t/day-of-week current)) (if (= 7 (t/day-of-week current))
current current
(recur (t/minus current (t/period :days 1)))))] (recur (t/minus current (t/period :days 1)))))]
(and-last-year {:start (t/minus last-sunday (t/period :days 6)) (and-last-year {:start (t/minus last-sunday (t/period :days 6))
:end last-sunday})) :end last-sunday}))
:title "Last week"}] :title "Last week"}]
[period-preset-button {:periods (and-last-year {:start (loop [current (local-today)] [period-preset-button {:periods (and-last-year {:start (loop [current (local-today)]
(if (= 1 (t/day-of-week current)) (if (= 1 (t/day-of-week current))
current current
(recur (t/minus current (t/period :days 1))))) (recur (t/minus current (t/period :days 1)))))
:end (local-today)}) :end (local-today)})
:title "Week to date"}] :title "Week to date"}]
[period-preset-button {:periods (and-last-year {:start (t/minus (t/local-date (t/year (local-today)) [period-preset-button {:periods (and-last-year {:start (t/minus (t/local-date (t/year (local-today))
(t/month (local-today)) (t/month (local-today))
1) 1)
(t/period :months 1)) (t/period :months 1))
:end (t/minus (t/local-date (t/year (local-today)) :end (t/minus (t/local-date (t/year (local-today))
(t/month (local-today)) (t/month (local-today))
1) 1)
(t/period :days 1))}) (t/period :days 1))})
:title "Last month"}] :title "Last month"}]
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today)) [period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today))
(t/month (local-today)) (t/month (local-today))
1) 1)
:end (local-today)}) :end (local-today)})
:title "Month to date"}] :title "Month to date"}]
[period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today)) 1 1) [period-preset-button {:periods (and-last-year {:start (t/local-date (t/year (local-today)) 1 1)
:end :end
(local-today)}) (local-today)})
:title "Year to date"}] :title "Year to date"}]
[period-preset-button {:periods [{:start (t/local-date (dec (t/year (local-today))) 1 1) [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)}] :end (t/local-date (dec (t/year (local-today))) 12 31)}]
:title "Last calendar year"}] :title "Last calendar year"}]
[period-preset-button {:periods (and-last-year {:start (t/plus (t/minus (local-today) (t/period :years 1)) [period-preset-button {:periods (and-last-year {:start (t/plus (t/minus (local-today) (t/period :years 1))
(t/period :days 1)) (t/period :days 1))
:end (local-today)}) :end (local-today)})
:title "Full year"}]] :title "Full year"}]]
[:div [:div
[:div.field [form-builder/raw-field-v2 {:field :show-advanced?}
[:label.checkbox [com/checkbox {:label "Show Advanced"}]]]
(raw-field (when (:show-advanced? data)
[:input {:type "checkbox" [form-builder/raw-field-v2 {:field :periods}
:field [:show-advanced?]}]) [com/multi-field-v2 {:template [[form-builder/raw-field-v2 {:field :start}
" Show Advanced"]]] [date-picker {:output :cljs-date}]]
(when (:show-advanced? data) [form-builder/raw-field-v2 {:field :end}
(doall [date-picker {:output :cljs-date}]]]}]])]]]
(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]}])]
[:div.control [:div.level-item
[:p.help "To"] [:div
(raw-field [switch-field {:id "include-deltas"
[date-picker-friendly {:type "date" :checked (boolean include-deltas)
:cljs-date? true :on-change (fn [e]
:field [:periods i :end]}])]])))]]] (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 (when @(re-frame/subscribe [::subs/is-admin?])
[:div [:button.button.is-secondary {:on-click (dispatch-event [::export-pdf])} "Export"])
[switch-field {:id "include-deltas" [:button.button.is-primary "Run"]]
: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"]) [:div.report-control-detail {:ref (fn [el]
[:button.button.is-primary "Run"]] (when (not= @!box el)
(reset! !box el)))}]]]))))
]]
[:div.report-control-detail {:ref (fn [el]
(when-not @!box
(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) report (l-reports/summarize-pnl pnl-data)
table (rtable/concat-tables (concat (:summaries report) (:details report)))] table (rtable/concat-tables (concat (:summaries report) (:details report)))]
[:div [: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) (when (:warning report)
[:div.notification.is-warning.is-light [:div.notification.is-warning.is-light
(:warning report)]) (:warning report)])
@@ -466,13 +448,11 @@ NOTE: Please review the transactions we may have question for you here: https://
(defn profit-and-loss-content [] (defn profit-and-loss-content []
(let [status @(re-frame/subscribe [::status/single ::page]) (let [status @(re-frame/subscribe [::status/single ::page])
{:keys [data report]} @(re-frame/subscribe [::forms/form ::form]) {:keys [data report]} @(re-frame/subscribe [::forms/form ::form])]
{:keys [form-inline]} pnl-form]
[:div [:div
(form-inline {} [:div
[:div [status/status-notification {:statuses [[::status/single ::page]]}]
[status/status-notification {:statuses [[::status/single ::page]]}] [report-controls]]
[report-controls pnl-form]])
[status/big-loader status] [status/big-loader status]
(when (and (not= :loading (:state status)) (when (and (not= :loading (:state status))
report) report)
@@ -493,9 +473,11 @@ NOTE: Please review the transactions we may have question for you here: https://
(mapv (fn [period] (mapv (fn [period]
{:start (str->date (:start period) standard) {:start (str->date (:start period) standard)
:end (str->date (:end period) standard)}))) :end (str->date (:end period) standard)})))
:clients (or (:clients qp) :clients (mapv (fn [c] {:client c :id (random-uuid)})
[(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]))]) (or (:clients qp)
:include-deltas false}) [(some-> @(re-frame/subscribe [::subs/client]) (select-keys [:name :id]) )]))
:include-deltas false
:show-advanced? false})
::track/register {:id ::ledger-params ::track/register {:id ::ledger-params
:subscription [::data-page/params ::ledger] :subscription [::data-page/params ::ledger]
:event-fn (fn [params] [::ledger-params-change params])}}))) :event-fn (fn [params] [::ledger-params-change params])}})))

View File

@@ -30,7 +30,7 @@
[(keyword (str "border-" (name b))) "1px solid black"]) [(keyword (str "border-" (name b))) "1px solid black"])
) )
(into s)))) (into s))))
(:colspan c) (assoc :colspan (:colspan c)) (:colspan c) (assoc :col-span (:colspan c))
(:align c) (assoc :align (:align c)) (:align c) (assoc :align (:align c))
(= :dollar (:format c)) (assoc :align :right) (= :dollar (:format c)) (assoc :align :right)
(= :percent (:format c)) (assoc :align :right) (= :percent (:format c)) (assoc :align :right)

View File

@@ -17,19 +17,22 @@
[vimsical.re-frame.fx.track :as track])) [vimsical.re-frame.fx.track :as track]))
(defn data-params->query-params [params] (defn data-params->query-params [params]
{:start (:start params 0) (if (:exact-match-id params)
:per-page (:per-page params) {:client-id (:id @(re-frame/subscribe [::subs/client]))
:sort (:sort params) :exact-match-id (some-> (:exact-match-id params) str)}
:client-id (:id @(re-frame/subscribe [::subs/client])) {:start (:start params 0)
:vendor-id (:id (:vendor params)) :per-page (:per-page params)
:payment-type (:payment-type params) :sort (:sort params)
:status (:status params) :client-id (:id @(re-frame/subscribe [::subs/client]))
:exact-match-id (some-> (:exact-match-id params) str) :vendor-id (:id (:vendor params))
:date-range (:date-range params) :payment-type (:payment-type params)
:amount-gte (:amount-gte (:amount-range params)) :status (:status params)
:amount-lte (:amount-lte (:amount-range params))
:check-number-like (str (:check-number-like params)) :date-range (:date-range params)
:invoice-number (:invoice-number 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 (re-frame/reg-event-fx
::params-change ::params-change

View File

@@ -15,18 +15,21 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::params-change ::params-change
[with-user] [with-user]
(fn [{:keys [user db ]}[_ params]] (fn [{:keys [user]}[_ params]]
{:graphql {:token user {:graphql {:token user
:owns-state {:single [::data-page/page ::page]} :owns-state {:single [::data-page/page ::page]}
:query-obj {:venia/queries [[:expected_deposit_page :query-obj {:venia/queries [[:expected_deposit_page
{:start (:start params 0) (if (:exact-match-id params)
:sort (:sort params) {:exact-match-id (some-> (:exact-match-id params) str)
:per-page (:per-page params) :client-id (:id @(re-frame/subscribe [::subs/client]))}
:exact-match-id (some-> (:exact-match-id params) str) {:start (:start params 0)
:total-gte (:amount-gte (:total-range params)) :sort (:sort params)
:total-lte (:amount-lte (:total-range params)) :per-page (:per-page params)
:date-range (:date-range params) :exact-match-id (some-> (:exact-match-id params) str)
:client-id (:id @(re-frame/subscribe [::subs/client]))} :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 [[:expected-deposits [:id :total :fee :location :date :status
[:totals [:date :count :amount]] [:totals [:date :count :amount]]
[:transaction [:id :date]] [:transaction [:id :date]]
@@ -42,13 +45,13 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::unmounted ::unmounted
(fn [{:keys [db]} _] (fn [_ _]
{:dispatch [::data-page/dispose ::page] {:dispatch [::data-page/dispose ::page]
::track/dispose {:id ::params}})) ::track/dispose {:id ::params}}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::mounted ::mounted
(fn [{:keys [db]} _] (fn [_ _]
{::track/register {:id ::params {::track/register {:id ::params
:subscription [::data-page/params ::page] :subscription [::data-page/params ::page]
:event-fn (fn [params] :event-fn (fn [params]

View File

@@ -1,25 +1,15 @@
(ns auto-ap.views.pages.pos.form (ns auto-ap.views.pages.pos.form
(:require (:require
[auto-ap.events :as events] [auto-ap.forms :as forms]
[auto-ap.forms :as forms] [auto-ap.subs :as subs]
[auto-ap.subs :as subs] [auto-ap.views.components.layouts :as layouts]
[auto-ap.utils :refer [dollars=]] [auto-ap.views.components.money-field :refer [money-field]]
[auto-ap.views.components.dropdown :refer [drop-down]] [auto-ap.views.utils
[auto-ap.views.components.expense-accounts-field :as expense-accounts-field :refer [expense-accounts-field recalculate-amounts]] :refer [date->str date-picker dispatch-event standard]]
[auto-ap.views.components.layouts :as layouts] [re-frame.core :as re-frame]
[auto-ap.views.components.money-field :refer [money-field]] [auto-ap.forms.builder :as form-builder]
[auto-ap.views.components.typeahead :refer [typeahead-v3]] [auto-ap.views.components :as com]))
[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))
(re-frame/reg-event-db (re-frame/reg-event-db
::editing ::editing
@@ -27,70 +17,56 @@
(let [which (update which :date #(date->str % standard))] (let [which (update which :date #(date->str % standard))]
(forms/start-form db ::form which)))) (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 ])} [layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form ])}
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
{:keys [form-inline field raw-field error-notification submit-button ]} sales-order-form] [form-builder/builder {:submit-event [::saving ]
(with-meta :id ::form}
(form-inline (assoc params :title "Sales order") [form-builder/section {:title "Sales Order"}
[:<> (when-not @(re-frame/subscribe [::subs/client])
(when-not @(re-frame/subscribe [::subs/client]) [form-builder/field-v2 {:field :client}
(field [:span "Client" "Client"
[:span.has-text-danger " *"]] [com/entity-typeahead {:entities @(re-frame/subscribe [::subs/clients])
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/clients]) :entity->text :name
:entity->text :name :disabled true}]])
:type "typeahead-v3"
:field [:client]
:disabled true}]))
(field "Date" [form-builder/field-v2 {:field :date}
[date-picker {:class-name "input" "Date"
:class "input" [date-picker {:output :cljs-date
:type "date" :disabled true}]]
:disabled true [form-builder/field-v2 {:field :total}
:field [:date]}]) "Total"
(field "Total" [money-field {:disabled true}]]
[money-field {:type "money" [form-builder/field-v2 {:field :tax}
:field [:total] "Tax"
:disabled true}]) [money-field {:disabled true}]
(field "Tax" [form-builder/field-v2 {:field :discount}
[money-field {:type "money" "Discount"
:field [:tax] [money-field {:disabled true}]]]
:disabled true}])
(field "Discount"
[money-field {:type "money"
:field [:discount]
:disabled true}])
(field "Returns" [form-builder/field-v2 {:field :returns}
[money-field {:type "money" "Returns"
:field [:returns] [money-field {:disabled true}]]
:disabled true}])
(field "Service Charge" [form-builder/field-v2 {:field :service-charge}
[money-field {:type "money" "Service Charge"
:field [:service-charge] [money-field {:disabled true}]]
:disabled true}])
(field "Tip" [form-builder/field-v2 {:field :tip}
[money-field {:type "money" "Tip"
:field [:tip] [money-field {:disabled true}]]
:disabled true}])
[:h1.subtitle.is-4 "Charges"] [form-builder/section {:title "Charges"}
[:ul [:ul
(for [charge (:charges data)] (for [charge (:charges data)]
[:li (:type-name charge) ": " (:total charge)])] ^{:key (:id charge)}
[:li (:type-name charge) ": " (:total charge)])]]
[:h1.subtitle.is-4 "Line Items"] [form-builder/section {:title "Line Items"}
[:ul [:ul
(for [line-item (:line-items data)] (for [line-item (:line-items data)]
[:li (:item-name line-item) ": " (:total line-item) [:span.tag (:category line-item)]])]]) ^{:key (:item-name line-item)}
{:key (:id data)}))]) [:li (:item-name line-item) ": " (:total line-item) [:span.tag (:category line-item)]])]]]])])

View File

@@ -112,7 +112,6 @@
{:id ::manual-import {:id ::manual-import
:events #{::manual/import-completed} :events #{::manual/import-completed}
:event-fn (fn [[_ result]] :event-fn (fn [[_ result]]
(println result)
[::status/info ::manual-import [::status/info ::manual-import
(str "Successfully " (str "Successfully "
(str/join ", " (str/join ", "

View File

@@ -3,13 +3,10 @@
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components.button-radio :refer [button-radio]]
[auto-ap.views.components.expense-accounts-field [auto-ap.views.components.expense-accounts-field
:as 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.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.data-page :as data-page]
[auto-ap.views.pages.transactions.common [auto-ap.views.pages.transactions.common
:refer [data-params->query-params]] :refer [data-params->query-params]]
@@ -19,7 +16,11 @@
[reagent.core :as r] [reagent.core :as r]
[vimsical.re-frame.fx.track :as track] [vimsical.re-frame.fx.track :as track]
[auto-ap.events :as events] [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 (re-frame/reg-sub
::can-submit ::can-submit
@@ -105,45 +106,41 @@
(fn [] (fn []
{::track/dispose {:id ::vendor-change}})) {::track/dispose {:id ::vendor-change}}))
(def code-form (forms/vertical-form {:submit-event [::code-selected]
:change-event [::changed] (def bulk-update-schema
:can-submit [::can-submit] (m/schema
:id ::form})) [:map
[:vendor schema/reference]]))
(defn form-content [_] (defn form-content [_]
(let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form]) (let [{:keys [data]} @(re-frame/subscribe [::forms/form ::form])]
{:keys [form-inline field]} code-form] [form-builder/builder {:submit-event [::code-selected]
:change-event [::changed]
:can-submit [::can-submit]
:id ::form}
(form-inline {}
[:<> [form-builder/field-v2 {:field :vendor}
(field "Vendor" "Vendor"
[search-backed-typeahead {:search-query (fn [i] [com/search-backed-typeahead {:search-query (fn [i]
[:search_vendor [:search_vendor
{:query i} {:query i}
[:name :id]]) [:name :id]])
:type "typeahead-v3" :auto-focus true}]]
:auto-focus true
:field [:vendor]}])
(field "Approval Status" [form-builder/field-v2 {:field [:transaction-approval-status]}
[button-radio "Approval Status"
{:type "button-radio" [com/button-radio-input
:field [:transaction-approval-status] {:options [[:unapproved "Unapproved"]
:options [[:unapproved "Unapproved"] [:requires-feedback "Client Review"]
[:requires-feedback "Client Review"] [:approved "Approved"]
[:approved "Approved"] [:excluded "Excluded from Ledger"]]}]]
[:excluded "Excluded from Ledger"]]}])
(with-meta [form-builder/raw-field-v2 {:field :accounts}
(field nil [expense-accounts-field-v2 {:descriptor "account asssignment"
[expense-accounts-field {:type "expense-accounts" :percentage-only? true
:descriptor "account asssignment" :client (:client data)
:percentage-only? true :locations (into ["Shared"] @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]))
:client (:client data) :max 100}]]]))
:locations (into ["Shared"] @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]))
:max 100
:field [:accounts]}])
{:key (some-> data :vendor :id str)})
])))
(defn form [_] (defn form [_]
(r/create-class (r/create-class
{:display-name "transaction-bulk-update-form" {:display-name "transaction-bulk-update-form"

View File

@@ -23,25 +23,28 @@
[:bank-account [:name :yodlee-account-id :current-balance]]]) [:bank-account [:name :yodlee-account-id :current-balance]]])
(defn data-params->query-params [params] (defn data-params->query-params [params]
{:start (:start params 0) (if (:exact-match-id params)
:per-page (:per-page params) {:client-id (:id @(re-frame/subscribe [::subs/client]))
:sort (:sort params) :exact-match-id (some-> (:exact-match-id params) str)}
:client-id (:id @(re-frame/subscribe [::subs/client])) {:start (:start params 0)
:vendor-id (:id (:vendor params)) :per-page (:per-page params)
:date-range (:date-range params) :sort (:sort params)
:account-id (:id (:account params)) :client-id (:id @(re-frame/subscribe [::subs/client]))
:bank-account-id (:id (:bank-account params)) :vendor-id (:id (:vendor params))
:amount-gte (:amount-gte (:amount-range params)) :date-range (:date-range params)
:exact-match-id (some-> (:exact-match-id params) str) :account-id (:id (:account params))
:unresolved (:unresolved params) :bank-account-id (:id (:bank-account params))
:potential-duplicates (:potential-duplicates params) :amount-gte (:amount-gte (:amount-range params))
:location (:location params) :exact-match-id (some-> (:exact-match-id params) str)
:import-batch-id (some-> (:import-batch-id params) str) :unresolved (:unresolved params)
:amount-lte (:amount-lte (:amount-range params)) :potential-duplicates (:potential-duplicates params)
:description (:description params) :location (:location params)
:approval-status (condp = @(re-frame/subscribe [::subs/active-page]) :import-batch-id (some-> (:import-batch-id params) str)
:transactions nil :amount-lte (:amount-lte (:amount-range params))
:unapproved-transactions :unapproved :description (:description params)
:requires-feedback-transactions :requires-feedback :approval-status (condp = @(re-frame/subscribe [::subs/active-page])
:excluded-transactions :excluded :transactions nil
:approved-transactions :approved)}) :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 (ns auto-ap.views.pages.transactions.form
(:require (:require
[auto-ap.events :as events]
[auto-ap.forms :as forms] [auto-ap.forms :as forms]
[auto-ap.forms.builder :as form-builder]
[auto-ap.schema :as schema]
[auto-ap.status :as status] [auto-ap.status :as status]
[auto-ap.subs :as subs] [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 [auto-ap.views.components.expense-accounts-field
:as 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.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.pages.transactions.common :refer [transaction-read]]
[auto-ap.views.utils [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] [clojure.string :as str]
[malli.core :as m]
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[react :as react] [react :as react]
[reagent.core :as r] [reagent.core :as r]
[vimsical.re-frame.fx.track :as track] [vimsical.re-frame.fx.track :as track]))
[auto-ap.events :as events]))
(def schema
(m/schema [:map
[:vendor schema/reference]
[:accounts expense-accounts-field/schema]
[:approval-status schema/approval-status]]))
;; SUBS ;; SUBS
(re-frame/reg-sub (re-frame/reg-sub
@@ -44,13 +50,6 @@
accounts)}} accounts)}}
transaction-read]}]})) transaction-read]}]}))
(re-frame/reg-sub
::can-submit
:<- [::forms/form ::form]
(fn [{:keys [status]} _]
(not= :loading status)))
;; EVENTS ;; EVENTS
(re-frame/reg-event-db (re-frame/reg-event-db
@@ -211,10 +210,6 @@
;; VIEWS ;; 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]}] (defn potential-transaction-rule-matches-box [{:keys [potential-transaction-rule-matches]}]
(let [states @(re-frame/subscribe [::status/multi ::matching])] (let [states @(re-frame/subscribe [::status/multi ::matching])]
@@ -303,7 +298,6 @@
(defonce ^js/React.Context current-tab-context ( react/createContext "default")) (defonce ^js/React.Context current-tab-context ( react/createContext "default"))
(def ^js/React.Provider CurrentTabProvider (. current-tab-context -Provider)) (def ^js/React.Provider CurrentTabProvider (. current-tab-context -Provider))
#_(println "Provider is" Provider)
(def ^js/React.Consumer CurrentTabConsumer (. current-tab-context -Consumer)) (def ^js/React.Consumer CurrentTabConsumer (. current-tab-context -Consumer))
(defn tabs [props & _] (defn tabs [props & _]
@@ -340,122 +334,106 @@
[layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form])} [layouts/side-bar {:on-close (dispatch-event [::forms/form-closing ::form])}
(let [{:keys [data] } @(re-frame/subscribe [::forms/form ::form]) (let [{:keys [data] } @(re-frame/subscribe [::forms/form ::form])
locations @(re-frame/subscribe [::subs/locations-for-client (:id (:client data))]) 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-admin? @(re-frame/subscribe [::subs/is-admin?])
is-power-user? @(re-frame/subscribe [::subs/is-power-user?]) is-power-user? @(re-frame/subscribe [::subs/is-power-user?])
should-disable-for-client? (and (not (or is-admin? is-power-user?)) should-disable-for-client? (and (not (or is-admin? is-power-user?))
(not= :requires-feedback (:original-status data))) (not= :requires-feedback (:original-status data)))
is-already-matched? (:payment data)] is-already-matched? (:payment data)]
(with-meta [form-builder/builder {:change-event [::changed]
(form-inline {:title "Transaction"} :submit-event [::saving ]
[:<> :id ::form
:schema schema}
[form-builder/section {:title "Transaction"}
[:<>
(when (and @(re-frame/subscribe [::subs/is-admin?]) (when 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? [form-builder/field-v2 {:field [:matched-rule :note] }
(field "Matched Rule" "Matched Rule"
[:input.input {:type "text" [:input.input {:type "text" :disabled "disabled"}]])
:field [:matched-rule :note] [form-builder/field-v2 {:field :amount}
:disabled "disabled"}])) "Amount"
(field "Amount" [:input.input {:type "text"
[:input.input {:type "text" :disabled "disabled"}]]
:field [:amount] [form-builder/field-v2 {:field [:description-original]}
:disabled "disabled"}]) "Description"
(field "Description" [:input.input {:type "text"
[:input.input {:type "text" :disabled "disabled"}]]
:field [:description-original]
:disabled "disabled"}])
(field "Date" [form-builder/field-v2 {:field [:date]}
[:input.input {:type "text" "Date"
:field [:date] [date-picker {
:disabled "disabled"}]) :disabled "disabled"}]]
(when (and (:payment data) (when (and (:payment data)
(or is-admin? is-power-user?)) (or is-admin? is-power-user?))
[:p.notification.is-info.is-light>div.level>div.level-left [:p.notification.is-info.is-light>div.level>div.level-left
[:div.level-item "This transaction is linked to a payment "] [:div.level-item "This transaction is linked to a payment "]
[:div.level-item [:button.button.is-warning {:on-click (dispatch-event [::unlink])} "Unlink"]]]) [:div.level-item [:button.button.is-warning {:on-click (dispatch-event [::unlink])} "Unlink"]]])
[tabs {:default-tab :details} [tabs {:default-tab :details}
(when (when
(and (seq (:potential-transaction-rule-matches data)) (and (seq (:potential-transaction-rule-matches data))
(not (:matched-rule data)) (not (:matched-rule data))
is-admin?) is-admin?)
[tab {:title "Transaction Rule" :key :transaction-rule} [tab {:title "Transaction Rule" :key :transaction-rule}
[potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]]) [potential-transaction-rule-matches-box {:potential-transaction-rule-matches (:potential-transaction-rule-matches data)}]])
(when (when
(and (seq (:potential-autopay-invoices-matches data)) (and (seq (:potential-autopay-invoices-matches data))
(not is-already-matched?) (not is-already-matched?)
(or is-admin? is-power-user?)) (or is-admin? is-power-user?))
[tab {:title "Autopay Invoices" :key :autopay-invoices} [tab {:title "Autopay Invoices" :key :autopay-invoices}
[potential-autopay-invoices-matches-box {:potential-autopay-invoices-matches (:potential-autopay-invoices-matches data)}]]) [potential-autopay-invoices-matches-box {:potential-autopay-invoices-matches (:potential-autopay-invoices-matches data)}]])
(when (when
(and (seq (:potential-unpaid-invoices-matches data)) (and (seq (:potential-unpaid-invoices-matches data))
(not is-already-matched?) (not is-already-matched?)
(or is-admin? is-power-user?)) (or is-admin? is-power-user?))
[tab {:title "Unpaid Invoices" :key :unpaid-invoices} [tab {:title "Unpaid Invoices" :key :unpaid-invoices}
[potential-unpaid-invoices-matches-box {:potential-unpaid-invoices-matches (:potential-unpaid-invoices-matches data)}]]) [potential-unpaid-invoices-matches-box {:potential-unpaid-invoices-matches (:potential-unpaid-invoices-matches data)}]])
(when (when
(and (seq (:potential-payment-matches data)) (and (seq (:potential-payment-matches data))
(not is-already-matched?) (not is-already-matched?)
(or is-admin? is-power-user?)) (or is-admin? is-power-user?))
[tab {:title "Payment" :key :payment} [tab {:title "Payment" :key :payment}
[potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]]) [potential-payment-matches-box {:potential-payment-matches (:potential-payment-matches data)}]])
[tab {:title "Details" :key :details} [tab {:title "Details" :key :details}
[:div [:div
(field "Vendor" [form-builder/field-v2 {:field :vendor}
[search-backed-typeahead {:search-query (fn [i] "Vendor"
[:search_vendor [com/search-backed-typeahead {:search-query (fn [i]
{:query i} [:search_vendor
[:name :id]]) {:query i}
:type "typeahead-v3" [:name :id]])
:auto-focus true :auto-focus true
:field [:vendor] :disabled (or (boolean (:payment data))
:disabled (or (boolean (:payment data)) should-disable-for-client?)}]]
should-disable-for-client?)}]) [form-builder/raw-field-v2 {:field :accounts}
(with-meta [expense-accounts-field-v2
(field nil {:max (Math/abs (js/parseFloat (:amount data)))
[expense-accounts-field :descriptor "credit account"
{:type "expense-accounts" :client (:client data)
:field [:accounts] :disabled (or (boolean (:payment data))
:max (Math/abs (js/parseFloat (:amount data))) should-disable-for-client?)
:descriptor "credit account" :locations locations}]]
:client (:client data) [form-builder/field-v2 {:field :approval-status}
:disabled (or (boolean (:payment data)) "Approval Status"
should-disable-for-client?) [com/button-radio-input
:locations locations}]) {:options [[:unapproved "Unapproved"]
{:key (str (:id (:vendor data)))}) [:requires-feedback "Client Review"]
(field "Approval Status" [:approved "Approved"]
[button-radio [:excluded "Excluded from Ledger"]]
{:type "button-radio" :disabled should-disable-for-client?}]]
:field [:approval-status]
:options [[:unapproved "Unapproved"]
[:requires-feedback "Client Review"]
[:approved "Approved"]
[:excluded "Excluded from Ledger"]]
:disabled should-disable-for-client?}])
(field "Forecasted-transaction" [form-builder/field-v2 {:field [:forecast-match]}
[typeahead-v3 {:entities @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))]) "Forecasted-transaction"
:entity->text :identifier [com/entity-typeahead {:entities @(re-frame/subscribe [::subs/forecasted-transactions-for-client (:id (:client data))])
:type "typeahead-v3" :entity->text :identifier}]]
:field [:forecast-match]}]) [form-builder/error-notification]
(error-notification) (when-not should-disable-for-client?
(when-not should-disable-for-client? [form-builder/submit-button "Save"])]]]]]])])
(submit-button "Save"))]]]])
{:key (:id data)}))])
(defn form [_] (defn form [_]
(r/create-class (r/create-class

View File

@@ -5,7 +5,10 @@
[auto-ap.subs :as subs] [auto-ap.subs :as subs]
[auto-ap.views.components.modal :as modal] [auto-ap.views.components.modal :as modal]
[auto-ap.views.utils :refer [dispatch-event with-user]] [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 (re-frame/reg-sub
::can-submit ::can-submit
@@ -13,22 +16,19 @@
(fn [{ {:keys [data]} :data}] (fn [{ {:keys [data]} :data}]
(not-empty data))) (not-empty data)))
(def import-form (forms/vertical-form {:submit-event [::save] (def schema
:change-event [::forms/change ::form] (m/schema [:map [:data schema/not-empty-string]]))
:can-submit [::can-submit]
:id ::form}))
(defn form [{import-completed-event :import-completed}] (defn form []
(let [{:keys [data active? error id]} @(re-frame/subscribe [::forms/form ::form]) [form-builder/builder {:submit-event [::try-save]
{:keys [form-inline horizontal-field field raw-field error-notification submit-button]} import-form] :id ::form
:schema schema}
(form-inline {} [form-builder/field-v2 {:required? true
[:div.field :field :data}
[:label.label "Yodlee manual import table"
"Yodlee manual import table"] [:textarea.textarea ]]
[:div.control [form-builder/hidden-submit-button]])
[raw-field
[:textarea.textarea {:field [:data]}]]]])))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::opening ::opening
@@ -38,19 +38,16 @@
:confirm {:value "Import" :confirm {:value "Import"
:status-from [::status/single ::form] :status-from [::status/single ::form]
:class "is-primary" :class "is-primary"
:on-click (dispatch-event [::save]) :on-click (dispatch-event [::try-save])
:can-submit [::can-submit]
:close-event [::status/completed ::form]}}] :close-event [::status/completed ::form]}}]
:db (-> db :db (-> db
(forms/start-form ::form (forms/start-form ::form
{:client-id (:id @(re-frame/subscribe [::subs/client])) {:client-id (:id @(re-frame/subscribe [::subs/client]))
:data ""}))})) :data ""}))}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::import-completed ::import-completed
(fn [{:keys [db]} [_ {:keys [imported errors] :as result}]] (fn [_ _]
{:dispatch [::modal/modal-closed ]})) {:dispatch [::modal/modal-closed ]}))
(re-frame/reg-event-fx (re-frame/reg-event-fx
@@ -66,6 +63,15 @@
:uri (str "/api/transactions/batch-upload") :uri (str "/api/transactions/batch-upload")
:on-success [::import-completed]}}))) :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 (ns auto-ap.views.pages.transactions.table
(:require [auto-ap.events :as events] (:require
[auto-ap.subs :as subs] [auto-ap.events :as events]
[auto-ap.views.components.dropdown [auto-ap.routes :as routes]
:refer [auto-ap.status :as status]
[drop-down drop-down-contents]] [auto-ap.subs :as subs]
[auto-ap.views.components.grid :as grid] [auto-ap.views.components.buttons :as buttons]
[auto-ap.views.pages.transactions.form :as edit] [auto-ap.views.components.dropdown
[auto-ap.views.utils :refer [drop-down drop-down-contents]]
:refer [auto-ap.views.components.grid :as grid]
[action-cell-width date->str dispatch-event dispatch-event-with-propagation nf pretty with-role]] [auto-ap.views.pages.data-page :as data-page]
[goog.string :as gstring] [auto-ap.views.pages.transactions.form :as edit]
[re-frame.core :as re-frame] [auto-ap.views.utils
[auto-ap.views.components.buttons :as buttons] :refer [action-cell-width
[auto-ap.status :as status] date->str
[auto-ap.views.pages.data-page :as data-page] dispatch-event-with-propagation
[bidi.bidi :as bidi] nf
[cemerick.url :as url] pretty
[auto-ap.routes :as routes])) with-role]]
[bidi.bidi :as bidi]
[cemerick.url :as url]
[re-frame.core :as re-frame]))
(re-frame/reg-event-fx (re-frame/reg-event-fx
::editing-matches-found ::editing-matches-found
(fn [{:keys [db]} [_ which matches]] (fn [_ [_ which matches]]
{:dispatch {:dispatch
[::edit/editing which (:potential-payment-matches matches) (:potential-autopay-invoices-matches matches) (:potential-unpaid-invoices-matches matches) (:potential-transaction-rule-matches matches)]})) [::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 (re-frame/reg-event-fx
::editing-matches-failed ::editing-matches-failed
(fn [{:keys [db]} [_ which payment-matches]] (fn [_ [_ which payment-matches]]
{:dispatch {:dispatch
[::edit/editing which payment-matches]})) [::edit/editing which payment-matches]}))
@@ -71,12 +74,10 @@
(fn [{table-params :db} [_ params :as z]] (fn [{table-params :db} [_ params :as z]]
{:db (merge table-params params)})) {: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]) (let [selected-client @(re-frame/subscribe [::subs/client])
{:keys [data status params]} @(re-frame/subscribe [::data-page/page data-page]) {:keys [data params]} @(re-frame/subscribe [::data-page/page data-page])
states @(re-frame/subscribe [::status/multi ::edits]) 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?])]
[grid/grid {:data-page data-page [grid/grid {:data-page data-page
:column-count (if selected-client 6 7) :column-count (if selected-client 6 7)
:check-boxes? check-boxes?} :check-boxes? check-boxes?}
@@ -94,7 +95,7 @@
[grid/sortable-header-cell {:sort-key "status" :sort-name "Status" :style {:width "7em"}} "Status"] [grid/sortable-header-cell {:sort-key "status" :sort-name "Status" :style {:width "7em"}} "Status"]
[grid/header-cell {:style {:width (action-cell-width 3)}}]]] [grid/header-cell {:style {:width (action-cell-width 3)}}]]]
[grid/body [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} ^{:key id}
[grid/row {:class (:class i) :id id :entity i} [grid/row {:class (:class i) :id id :entity i}
(when-not selected-client (when-not selected-client

View File

@@ -51,7 +51,8 @@
(re-frame/reg-event-fx (re-frame/reg-event-fx
::unmounted ::unmounted
(fn [{:keys [db]} _] (fn [{:keys [db]} _]
{:dispatch [::data-page/dispose :invoices] {:dispatch-n [[::data-page/dispose :invoices]
[::forms/form-closing ::form/form]]
::forward/dispose [{:id ::updated} ::forward/dispose [{:id ::updated}
{:id ::checks-printed}] {:id ::checks-printed}]
::track/dispose [{:id ::params}]})) ::track/dispose [{:id ::params}]}))

View File

@@ -11,7 +11,6 @@
[re-frame.core :as re-frame] [re-frame.core :as re-frame]
[react-transition-group :as react-transition-group] [react-transition-group :as react-transition-group]
#_{:clj-kondo/ignore [:unused-namespace]} #_{:clj-kondo/ignore [:unused-namespace]}
[react-datepicker :as react-datepicker]
[reagent.core :as reagent] [reagent.core :as reagent]
[reagent.core :as r] [reagent.core :as r]
[react :as react] [react :as react]
@@ -64,7 +63,7 @@
(def login-url (def login-url
(let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com" (let [client-id "264081895820-0nndcfo3pbtqf30sro82vgq5r27h8736.apps.googleusercontent.com"
redirect-uri (js/encodeURI (str (.-origin (.-location js/window)) "/api/oauth"))] 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] (defn dispatch-value-change [event]
(fn [e] (fn [e]
@@ -107,26 +106,6 @@
(when d (when d
(format/parse f 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] (defn with-keys [children]
(map-indexed (fn [i c] ^{:key i} c) children)) (map-indexed (fn [i c] ^{:key i} c) children))
@@ -151,358 +130,33 @@
(first children) (first children)
[:span])]))) [: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]} ] (= "entering" state)
(let [value-repr (reagent/atom (mapv 0.0
(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"
(:new? override) (= "exiting" state)
"has-background-info-light" 0.0
:else
"")}
(let [template (if (fn? template)
(template override)
template)]
[:<> (for [[idx template] (map vector (range ) template)]
^{:key idx}
[:div.level-item (= "exited" state)
(update template 1 assoc 0.0)}}
:value (let [value (get-in override (get-in template [1 :field])) ;; TODO this is really ugly to support maps or strings child])))])])))
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")])
(defn coerce-date [d] (defn coerce-date [d]
(cond (and (string? d) (cond (and (string? d)
@@ -524,7 +178,7 @@
:else :else
nil )) 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))) (let [[text set-text ] (react/useState (some-> params :value coerce-date (date->str standard)))
[value set-value ] (react/useState (some-> params :value coerce-date)) [value set-value ] (react/useState (some-> params :value coerce-date))
@@ -561,12 +215,14 @@
(swap-external-value (some-> (.. e -target -value) coerce-date)))) (swap-external-value (some-> (.. e -target -value) coerce-date))))
:on-blur (fn [] :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")] :type "date" :placeholder "12/1/2021")]
]])) ]]))
(defn date-picker-optional [] (defn date-picker []
[:f> date-picker-optional-internal [:f> date-picker-internal
(r/props (r/current-component))]) (r/props (r/current-component))])
(defn local-now [] (defn local-now []
@@ -654,3 +310,25 @@
:else :else
x)) 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))