diff --git a/src/clj/auto_ap/datomic/clients.clj b/src/clj/auto_ap/datomic/clients.clj index 8ae770b8..bbfeb757 100644 --- a/src/clj/auto_ap/datomic/clients.clj +++ b/src/clj/auto_ap/datomic/clients.clj @@ -14,7 +14,6 @@ (map (fn [ba] (update ba :bank-account/type :db/ident )) bas))))))) - (defn get-by-id [id] (->> (d/query (-> {:query {:find ['(pull ?e [*])] diff --git a/src/clj/auto_ap/datomic/vendors.clj b/src/clj/auto_ap/datomic/vendors.clj index df17baba..f4540209 100644 --- a/src/clj/auto_ap/datomic/vendors.clj +++ b/src/clj/auto_ap/datomic/vendors.clj @@ -21,7 +21,9 @@ :in $ ?e :where [?e]] (d/db (d/connect uri)) - (Long/parseLong id)) + (if (string? id) + (Long/parseLong id) + id)) (map first) (first) #_(map (fn [c] diff --git a/src/clj/auto_ap/graphql.clj b/src/clj/auto_ap/graphql.clj index b3aac920..b682489b 100644 --- a/src/clj/auto_ap/graphql.clj +++ b/src/clj/auto_ap/graphql.clj @@ -14,6 +14,7 @@ [auto-ap.datomic.invoices :as d-invoices] [auto-ap.datomic.vendors :as d-vendors] [auto-ap.graphql.users :as gq-users] + [auto-ap.graphql.vendors :as gq-vendors] [auto-ap.graphql.checks :as gq-checks] [auto-ap.graphql.expense-accounts :as expense-accounts] [auto-ap.graphql.invoices :as gq-invoices] @@ -257,6 +258,31 @@ :role {:type 'String} :clients {:type '(list String)}}} + :add_contact + {:fields {:id {:type 'String} + :name {:type 'String} + :email {:type 'String} + :phone {:type 'String}}} + :add_address + {:fields {:street1 {:type 'String} + :street2 {:type 'String} + :city {:type 'String} + :state {:type 'String} + :zip {:type 'String}}} + + :add_vendor + {:fields {:id {:type 'String} + :name {:type 'String} + :code {:type 'String} + + :print_as {:type 'String} + :primary_contact {:type :add_contact} + :secondary_contact {:type :add_contact} + :address {:type :add_address} + + :default_expense_account {:type 'Int} + :invoice_reminder_schedule {:type 'String}}} + :edit_expense_account {:fields {:id {:type 'String} :expense_account_id {:type 'Int} @@ -303,6 +329,9 @@ :args {:edit_user {:type :edit_user}} :resolve :mutation/edit-user} + :upsert_vendor {:type :vendor + :args {:vendor {:type :add_vendor}} + :resolve :mutation/upsert-vendor} :add_invoice {:type :invoice :args {:invoice {:type :add_invoice}} :resolve :mutation/add-invoice} @@ -443,6 +472,7 @@ :mutation/edit-user gq-users/edit-user :mutation/add-invoice gq-invoices/add-invoice :mutation/edit-invoice gq-invoices/edit-invoice + :mutation/upsert-vendor gq-vendors/upsert-vendor :mutation/void-invoice gq-invoices/void-invoice :mutation/void-payment gq-checks/void-check :mutation/edit-expense-accounts gq-invoices/edit-expense-accounts diff --git a/src/clj/auto_ap/graphql/vendors.clj b/src/clj/auto_ap/graphql/vendors.clj new file mode 100644 index 00000000..6dd79fc6 --- /dev/null +++ b/src/clj/auto_ap/graphql/vendors.clj @@ -0,0 +1,53 @@ +(ns auto-ap.graphql.vendors + (:require [auto-ap.graphql.utils :refer [->graphql assert-can-see-company]] + [auto-ap.datomic.vendors :as d-vendors] + [auto-ap.time :refer [parse iso-date]] + [datomic.api :as d] + [auto-ap.datomic :refer [uri remove-nils]] + [clj-time.coerce :as coerce] + [clojure.set :as set])) + + +(defn upsert-vendor [context {{:keys [id name code print_as primary_contact secondary_contact address default_expense_account invoice_reminder_schedule] :as in} :vendor} value] + (let [transaction [(remove-nils #:vendor {:db/id (if id + (Long/parseLong id) + "vendor") + :name name + :code code + :print-as print_as + :default-expense-account default_expense_account + :invoice-reminder-schedule (keyword invoice_reminder_schedule) + :address (when address + (remove-nils #:address {:db/id (if (:id address) + (Long/parseLong (:id address)) + "address") + :street1 (:street1 address) + :street2 (:street2 address) + :city (:city address) + :state (:state address) + :zip (:zip address)})) + :primary-contact (when primary_contact + + (remove-nils #:contact {:db/id (if (:id primary_contact) + (Long/parseLong (:id primary_contact)) + "primary") + :name (:name primary_contact) + :phone (:phone primary_contact) + :email (:email primary_contact)})) + + :secondary-contact (when secondary_contact + + (remove-nils #:contact {:db/id (if (:id secondary_contact) + (Long/parseLong (:id secondary_contact)) + "secondary") + :name (:name secondary_contact) + :phone (:phone secondary_contact) + :email (:email secondary_contact)}) + )})] + transaction-result @(d/transact (d/connect uri) transaction)] + + (-> (d-vendors/get-by-id (or (-> transaction-result :tempids (get "vendor")) + id)) + (->graphql)))) + + diff --git a/src/clj/auto_ap/routes/invoices.clj b/src/clj/auto_ap/routes/invoices.clj index 7f7ca397..a5600f49 100644 --- a/src/clj/auto_ap/routes/invoices.clj +++ b/src/clj/auto_ap/routes/invoices.clj @@ -144,19 +144,14 @@ {{:keys [excel-rows]} :edn-params user :identity} (assert-admin user) (let [columns [:raw-date :vendor-name :check :location :invoice-number :amount :company :bill-entered :bill-rejected :added-on :exported-on :default-expense-account] - all-vendors (by :name (vendors/get-all)) - all-companies (companies/get-all) all-companies (merge (by :code all-companies) (by :name all-companies)) - - rows (->> (str/split excel-rows #"\n" ) (map #(str/split % #"\t")) (map #(into {} (map (fn [c k] [k c] ) % columns))) (map reset-id) (map assoc-company-code) - (map (parse-or-error :company-id #(parse-company % all-companies))) (map (parse-or-error :vendor-id #(parse-vendor % all-vendors))) (map (parse-or-error :default-expense-account parse-default-expense-account)) diff --git a/src/clj/auto_ap/yodlee/import.clj b/src/clj/auto_ap/yodlee/import.clj index 59ad6a9f..8f5cca7c 100644 --- a/src/clj/auto_ap/yodlee/import.clj +++ b/src/clj/auto_ap/yodlee/import.clj @@ -1,7 +1,5 @@ (ns auto-ap.yodlee.import (:require [auto-ap.yodlee.core :as client] - [auto-ap.db.transactions :as transactions] - [auto-ap.db.vendors :as vendors] [auto-ap.utils :refer [by]] [datomic.api :as d] [auto-ap.datomic :refer [uri remove-nils]] @@ -21,7 +19,6 @@ :amount (- amount) :status :payment-status/pending}) first - (doto println) :db/id) (and client-id bank-account-id amount) diff --git a/src/cljc/auto_ap/entities/contact.cljc b/src/cljc/auto_ap/entities/contact.cljc new file mode 100644 index 00000000..0c94f10a --- /dev/null +++ b/src/cljc/auto_ap/entities/contact.cljc @@ -0,0 +1,14 @@ +(ns auto-ap.entities.contact + (:require [clojure.spec.alpha :as s] + [clojure.string :as str] + [auto-ap.entities.address :as address])) + +(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") + +(s/def ::id (s/nilable string?)) +(s/def ::name (s/nilable string?)) +(s/def ::email (s/nilable (s/and string? (s/or :is-email #(re-matches email-regex %) + :is-empty #(= % ""))))) +(s/def ::phone (s/nilable string?)) + +(s/def ::contact (s/keys :opt-un [::name ::email ::phone ::id])) diff --git a/src/cljc/auto_ap/entities/vendors.cljc b/src/cljc/auto_ap/entities/vendors.cljc index bacdc4ab..1c6a0828 100644 --- a/src/cljc/auto_ap/entities/vendors.cljc +++ b/src/cljc/auto_ap/entities/vendors.cljc @@ -1,28 +1,20 @@ (ns auto-ap.entities.vendors (:require [clojure.spec.alpha :as s] [clojure.string :as str] + [auto-ap.entities.contact :as contact] [auto-ap.entities.address :as address])) -(def email-regex #"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,63}$") -(s/def ::id int) +(s/def ::id string?) (s/def ::identifier (s/nilable string?)) (s/def ::required-identifier (s/and string? #(not (str/blank? %)))) (s/def ::name ::required-identifier) (s/def ::print-as (s/nilable string?)) -(s/def ::email (s/nilable (s/and string? (s/or :is-email #(re-matches email-regex %) - :is-empty #(= % ""))))) -(s/def ::phone (s/nilable string?)) - (s/def ::invoice-reminder-schedule (s/nilable #{"Weekly" "Never" nil})) -(s/def ::primary-contact (s/nilable ::identifier)) -(s/def ::primary-email (s/nilable ::email)) -(s/def ::primary-phone (s/nilable ::phone)) -(s/def ::secondary-contact (s/nilable ::identifier)) -(s/def ::secondary-email (s/nilable ::email)) -(s/def ::secondary-phone (s/nilable ::phone)) +(s/def ::primary-contact (s/nilable ::contact/contact)) +(s/def ::secondary-contact (s/nilable ::contact/contact)) (s/def ::address (s/nilable ::address/address)) (s/def ::default-expense-account int?) @@ -35,14 +27,8 @@ ::print-as ::invoice-reminder-schedule ::primary-contact - ::primary-email - ::primary-phone ::secondary-contact - ::secondary-email - ::secondary-phone - ::address - - ])) + ::address])) (def vendor-spec (apply hash-map (drop 1 (s/form ::vendor)))) diff --git a/src/cljs/auto_ap/events.cljs b/src/cljs/auto_ap/events.cljs index 1f689c60..f95d67da 100644 --- a/src/cljs/auto_ap/events.cljs +++ b/src/cljs/auto_ap/events.cljs @@ -111,17 +111,6 @@ (fn [db [_ new-invoices]] (assoc-in db [:invoices :pending] new-invoices))) -(re-frame/reg-event-fx - ::approve-invoices - (fn [cofx [_]] - {:http {:method :post - :token (-> cofx :db :user) - :uri (str "/api/invoices/approve" - (when-let [company-id (:id @(re-frame/subscribe [::subs/company]))] - (str "?company=" company-id))) - :on-success [::received-invoices :pending] - }})) - (re-frame/reg-event-fx ::view-pending-invoices (fn [cofx []] @@ -143,16 +132,6 @@ [:id :total :invoice-number :date [:vendor [:name :id]] [:company [:name :id]]]]]} :on-success [::received-invoices :unpaid]}})) -(re-frame/reg-event-fx - ::reject-invoices - (fn [cofx [_]] - {:http {:method :post - :token (-> cofx :db :user) - :uri (str "/api/invoices/reject" - (when-let [company-id (:id @(re-frame/subscribe [::subs/company]))] - (str "?company=" company-id))) - :on-success [::received-invoices :pending] - }})) (re-frame/reg-event-db ::submitted-new-invoice (fn [db [_ invoice]] diff --git a/src/cljs/auto_ap/events/admin/vendors.cljs b/src/cljs/auto_ap/events/admin/vendors.cljs index d463741b..c01fc2bc 100644 --- a/src/cljs/auto_ap/events/admin/vendors.cljs +++ b/src/cljs/auto_ap/events/admin/vendors.cljs @@ -6,9 +6,17 @@ [auto-ap.effects :as effects] [auto-ap.entities.vendors :as entity] [auto-ap.events :as events] + [auto-ap.utils :refer [by]] [bidi.bidi :as bidi])) +(def vendor-query + [:id :name :default-expense-account + [:primary-contact [:name :phone :email :id]] + [:secondary-contact [:id :name :phone :email]] + :print-as :invoice-reminder-schedule :code + [:address [:street1 :street2 :city :state :zip]]]) + (re-frame/reg-event-fx ::edit (fn [{:keys [db]} [_ vendor-id]] @@ -16,11 +24,12 @@ (get (:vendors db) vendor-id)) :dispatch [::events/modal-status :auto-ap.views.pages.admin.vendors/admin-vendor {:visible? true}]})) -(re-frame/reg-event-db +(re-frame/reg-event-fx ::new - (fn [db _] - (assoc-in db [:admin :vendor] - {}))) + (fn [{:keys [db]} _] + {:db (assoc-in db [:admin :vendor] + {}) + :dispatch [::events/modal-status :auto-ap.views.pages.admin.vendors/admin-vendor {:visible? true}]})) (re-frame/reg-event-fx ::save @@ -28,21 +37,16 @@ (let [edited-vendor (get-in db [:admin :vendor]) fx {:db (assoc-in db [:admin :vendor :saving?] true)}] (when (s/valid? ::entity/vendor edited-vendor) - (if (:id edited-vendor) - (assoc fx :http {:method :put - :token (:user db) - :body (pr-str edited-vendor) - :headers {"Content-Type" "application/edn"} - :uri (str "/api/vendors/" (:id edited-vendor)) - :on-success [::save-complete] - :on-error [::save-error]}) - (assoc fx :http {:method :post - :token (:user db) - :body (pr-str edited-vendor) - :headers {"Content-Type" "application/edn"} - :uri (str "/api/vendors") - :on-success [::save-complete] - :on-error [::save-error]})))))) + (assoc fx :graphql + {:token (-> db :user) + :query-obj {:venia/operation {:operation/type :mutation + :operation/name "UpsertVendor"} + + :venia/queries [{:query/data [:upsert-vendor + {:vendor edited-vendor} + vendor-query]}]} + :on-success [::save-complete] + :on-error [::save-error]}))))) (re-frame/reg-event-fx ::remind @@ -69,7 +73,7 @@ (re-frame/reg-event-fx ::save-complete - (fn [{:keys [db]} [_ vendor]] + (fn [{:keys [db]} [_ {vendor :upsert-vendor} ]] {:dispatch [::events/modal-completed :auto-ap.views.pages.admin.vendors/admin-vendor ] :db (-> db @@ -107,15 +111,11 @@ (re-frame/reg-event-fx ::mounted (fn [{:keys [db]} _] - {:http {:method :get - :token (:user db) - :uri "/api/vendors" - :on-success [::received-vendors]}})) + {:graphql {:token (:user db) + :query-obj {:venia/queries [[:vendor vendor-query]]} + :on-success [::received-vendors]}})) (re-frame/reg-event-db ::received-vendors (fn [db [_ vendors]] - (assoc db :vendors (reduce (fn [vendors vendor] - (assoc vendors (:id vendor) vendor)) - {} - vendors)))) + (assoc db :vendors (by :id (:vendor vendors ))))) diff --git a/src/cljs/auto_ap/views/components/vendor_dialog.cljs b/src/cljs/auto_ap/views/components/vendor_dialog.cljs index b77303f5..15a181ec 100644 --- a/src/cljs/auto_ap/views/components/vendor_dialog.cljs +++ b/src/cljs/auto_ap/views/components/vendor_dialog.cljs @@ -8,10 +8,11 @@ [auto-ap.expense-accounts :refer [chooseable-expense-accounts]] [clojure.spec.alpha :as s] [auto-ap.entities.vendors :as entity] + [auto-ap.entities.contact :as contact] [auto-ap.subs :as subs])) (defn vendor-dialog [{:keys [vendor save-event change-event id] {:keys [name]} :vendor}] - + (println (s/explain ::entity/vendor vendor) ) (let [companies-by-id @(re-frame/subscribe [::subs/companies-by-id])] [action-modal {:id id :title [:span (if (:id vendor) @@ -82,7 +83,7 @@ [bind-field [:input.input.is-expanded {:type "text" :field [:primary-contact :name] - :spec ::entity/primary-contact + :spec ::contact/name :event change-event :subscription vendor}]] [:span.icon.is-small.is-left @@ -94,7 +95,7 @@ [bind-field [:input.input {:type "email" :field [:primary-contact :email] - :spec ::entity/primary-email + :spec ::contact/email :event change-event :subscription vendor}]]] @@ -102,7 +103,7 @@ [bind-field [:input.input {:type "phone" :field [:primary-contact :phone] - :spec ::entity/primary-phone + :spec ::contact/phone :event change-event :subscription vendor}]] [:span.icon.is-small.is-left @@ -114,7 +115,7 @@ [bind-field [:input.input.is-expanded {:type "text" :field [:secondary-contact :name] - :spec ::entity/secondary-contact + :spec ::contact/name :event change-event :subscription vendor}]] [:span.icon.is-small.is-left @@ -125,14 +126,14 @@ [bind-field [:input.input {:type "email" :field [:secondary-contact :email] - :spec ::entity/secondary-email + :spec ::contact/email :event change-event :subscription vendor}]]] [:div.control.has-icons-left [bind-field [:input.input {:type "phone" :field [:secondary-contact :phone] - :spec ::entity/secondary-phone + :spec ::contact/phone :event change-event :subscription vendor}]] [:span.icon.is-small.is-left diff --git a/src/cljs/auto_ap/views/pages/admin/vendors.cljs b/src/cljs/auto_ap/views/pages/admin/vendors.cljs index 7e9b4e90..8f2179bb 100644 --- a/src/cljs/auto_ap/views/pages/admin/vendors.cljs +++ b/src/cljs/auto_ap/views/pages/admin/vendors.cljs @@ -55,197 +55,6 @@ (vec (concat [dom keys] rest)))) -#_(defn edit-dialog [] - (let [editing-vendor (:vendor @(re-frame/subscribe [::subs/admin])) - companies-by-id @(re-frame/subscribe [::subs/companies-by-id])] - - [modal {:title [:span (if (:id editing-vendor) - (str "Edit " (or (:name editing-vendor) "")) - (str "Add " (or (:name editing-vendor) ""))) - (when (:error editing-vendor) - [:span.icon.has-text-danger - [:i.fa.fa-exclamation-triangle]])] - :foot [:button.button.is-primary {:on-click (fn [] (re-frame/dispatch [::events/save])) - :disabled (when (not (s/valid? ::entity/vendor editing-vendor )) - "disabled")} - [:span "Save"] - (when (:saving? editing-vendor) - [:span.icon - [:i.fa.fa-spin.fa-spinner]])] - :hide-event [::events/edit nil]} - - [horizontal-field - [:label.label "Name"] - [:div.control - [bind-field - [:input.input {:type "text" - :field :name - :spec ::entity/name - :event ::events/change - :subscription editing-vendor}]]]] - - [horizontal-field - [:label.label "Code"] - [:div.control - - [bind-field - [:input.input.is-expanded {:type "text" - :field :code - :spec ::entity/code - :event ::events/change - :subscription editing-vendor}]] - [:p.help "The vendor code is used for invoice parsing. Only one vendor at a time can use a code"]]] - - [:h2.subtitle "Address"] - [address-field {:field [:address] - :event ::events/change - :subscription editing-vendor}] - - - - [:h2.subtitle "Contact"] - [horizontal-field - [:label.label "Primary"] - [:div.control.has-icons-left - [bind-field - [:input.input.is-expanded {:type "text" - :field :primary-contact - :spec ::entity/primary-contact - :event ::events/change - :subscription editing-vendor}]] - [:span.icon.is-small.is-left - [:i.fa.fa-user]]] - - [:div.control.has-icons-left - [:span.icon.is-small.is-left - [:i.fa.fa-envelope]] - [bind-field - [:input.input {:type "email" - :field :primary-email - :spec ::entity/primary-email - :event ::events/change - :subscription editing-vendor}]]] - - [:div.control.has-icons-left - [bind-field - [:input.input {:type "phone" - :field :primary-phone - :spec ::entity/primary-phone - :event ::events/change - :subscription editing-vendor}]] - [:span.icon.is-small.is-left - [:i.fa.fa-phone]]]] - - [horizontal-field - [:label.label "Secondary"] - [:div.control.has-icons-left - [bind-field - [:input.input.is-expanded {:type "text" - :field :secondary-contact - :spec ::entity/secondary-contact - :event ::events/change - :subscription editing-vendor}]] - [:span.icon.is-small.is-left - [:i.fa.fa-user]]] - [:div.control.has-icons-left - [:span.icon.is-small.is-left - [:i.fa.fa-envelope]] - [bind-field - [:input.input {:type "email" - :field :secondary-email - :spec ::entity/secondary-email - :event ::events/change - :subscription editing-vendor}]]] - [:div.control.has-icons-left - [bind-field - [:input.input {:type "phone" - :field :secondary-phone - :spec ::entity/secondary-phone - :event ::events/change - :subscription editing-vendor}]] - [:span.icon.is-small.is-left - [:i.fa.fa-phone]]]] - - [horizontal-field - [:label.label "Invoice Reminders"] - [:div.control - [:label.radio - [bind-field - [:input {:type "radio" - :name "schedule" - :value "Weekly" - :field :invoice-reminder-schedule - :spec ::entity/invoice-reminder-schedule - :event ::events/change - :subscription editing-vendor}]] - " Send weekly"] - - [:label.radio - [bind-field - [:input {:type "radio" - :name "schedule" - :value "Never" - :field :invoice-reminder-schedule - :spec ::entity/invoice-reminder-schedule - :event ::events/change - :subscription editing-vendor}]] - " Never"]]] - - [:h2.subtitle "Expense Accounts"] - [horizontal-field - [:label.label "Default"] - [bind-field - [typeahead {:matches (map (fn [[k v]] [k (:name v)]) expense-accounts) - :type "typeahead" - :field [:default-expense-account] - :spec ::entity/default-expense-account - :event ::events/change - :subscription editing-vendor}]]] - - - - - #_[:h2.subtitle "Clients"] - - #_[horizontal-field - nil - [:div.control@(re-frame/subscribe [::subs/exp]) - [:div.select.is-expanded - [bind-field - [:select {:type "select" - :field :new-relationship-company - :event ::events/change - :subscription editing-vendor} - (for [company @(re-frame/subscribe [::subs/companies])] - [:option {:value (:id company)} (:name company)])]]]] - [:div.control - [bind-field - [:input.input {:type "text" - :field :new-relationship-account-number - :subscription editing-vendor - :event ::events/change - :placeholder "Account number"}]]] - [:div.control - [:button.button.is-primary - {:on-click (dispatch-event [::events/add-relationship])} - [:span.icon - [:i.fa.fa-plus]]]]] - #_[horizontal-field - nil - [:ul - (for [[i r] (map vector (range) (:relationships editing-vendor))] - ^{:key i} - [:li - (:name (companies-by-id (js/parseInt (:company-id r)))) ": " - (:account-number r) - [:a - {:on-click (dispatch-event [::events/remove-relationship i])} - [:span.icon - [:i.fa.fa-times]]]])]] - - - (when (:saving? editing-vendor) [:div.is-overlay {:style {"backgroundColor" "rgba(150,150,150, 0.5)"}}])])) - (defn admin-vendors-page [] [(with-meta (fn []