calculate prices

This commit is contained in:
Daniel O'Connell 2021-03-27 17:32:47 +01:00
parent 9bee3a66f9
commit 06059b830d
12 changed files with 214 additions and 73 deletions

View File

@ -24,6 +24,9 @@
(defn save-product-group [user-id customer-id body]
(customers/save-product-group user-id (Integer/parseInt customer-id) body)
(get-customers user-id))
(defn save-customer-prices [user-id customer-id body]
(customers/save-prices user-id (Integer/parseInt customer-id) body)
(get-customers user-id))
(defn delete-customer [user-id id]
(->> id edn/read-string (customers/delete! user-id))
@ -52,6 +55,8 @@
(DELETE "/customers/:id" [id :as {user-id :basic-authentication}] (delete-customer user-id id))
(POST "/customers/:id/product-group" [id :as {user-id :basic-authentication body :body}]
(save-product-group user-id id body))
(POST "/customers/:id/prices" [id :as {user-id :basic-authentication body :body}]
(save-customer-prices user-id id body))
(GET "/products" request (get-products request))
(POST "/products" request (save-products request))

View File

@ -23,18 +23,40 @@
(reduce-kv insert-products {})
(assoc client :product-groups))))
(def users-select-query
"SELECT * FROM customers c
(def user-product-groups-query
"SELECT c.id, c.name, cg.name, cg.id, cgp.amount, cgp.price, p.name FROM customers c
LEFT OUTER JOIN customer_groups cg on c.id = cg.customer_id
LEFT OUTER JOIN customer_group_products cgp on cg.id = cgp.customer_group_id
LEFT OUTER JOIN products p ON p.id = cgp.product_id
WHERE c.deleted IS NULL aND c.user_id = ?")
WHERE c.deleted IS NULL AND c.user_id = ?")
(defn get-all [user-id]
(->> (sql/query db/db-uri [users-select-query user-id])
(def user-prices-query
"SELECT c.id, c.name, cp.price, p.name FROM customers c
LEFT OUTER JOIN customer_products cp on c.id = cp.customer_id
LEFT OUTER JOIN products p ON p.id = cp.product_id
WHERE c.deleted IS NULL AND c.user_id = ?")
(defn get-product-groups [user-id]
(->> (sql/query db/db-uri [user-product-groups-query user-id])
(group-by (fn [{:customers/keys [id name]}] {:id id :name name}))
(map (partial apply extract-product-groups))))
(defn get-prices [user-id]
(->> (sql/query db/db-uri [user-prices-query user-id])
(filter :customer_products/price)
(group-by :customers/id)
(reduce-kv (fn [coll user vals]
(->> vals
(reduce #(assoc-in %1 [(-> %2 :products/name keyword) :price] (:customer_products/price %2)) {})
(assoc coll user))) {})))
(defn get-all [user-id]
(let [prices (get-prices user-id)]
(map #(assoc % :prices (-> % :id prices))
(get-product-groups user-id))))
(defn create! [user-id name]
(jdbc/execute! db/db-uri
["INSERT INTO customers (name, user_id) VALUES(?, ?) ON CONFLICT (name, user_id) DO UPDATE SET deleted = NULL"
@ -54,7 +76,6 @@
(do (create! user-id name)
(get-by-name tx user-id name))))
(defn upsert-customer-group! [tx user-id customer-id {:keys [id name]}]
(if (jdbc/execute-one! tx ["SELECT * FROM customer_groups WHERE user_id = ? AND customer_id = ? AND id =?"
user-id customer-id id])
@ -69,3 +90,8 @@
tx user-id :customer_group
(upsert-customer-group! tx user-id customer-id group)
(:products group))))
(defn save-prices [user-id customer-id group]
(jdbc/with-transaction [tx db/db-uri]
(when (db/get-by-id tx user-id :customers customer-id)
(products/update-products-mapping! tx user-id :customer customer-id group))))

View File

@ -71,27 +71,29 @@
(deftest test-get-all
(testing "query is correct"
(with-redefs [sql/query (fn [_ query]
(is (= query [sut/users-select-query "1"]))
[])]
(sut/get-all "1")))
(with-redefs [sql/query (fn [_ [query id]]
(is (#{sut/user-product-groups-query
sut/user-prices-query} query))
(is (= id 1))
{})]
(sut/get-all 1)))
(testing "results are mapped correctly"
(testing "product results are mapped correctly"
(with-redefs [sql/query (constantly [{:customers/id 1 :customers/name "mr blobby" :bla 123}])]
(is (= (sut/get-all 2)
[{:id 1 :name "mr blobby" :product-groups {}}]))))
[{:id 1 :name "mr blobby" :product-groups {} :prices nil}]))))
(testing "customer groups are mapped correctly"
(with-redefs [sql/query (constantly sample-customers)]
(is (= (sut/get-all "1")
[{:id 1, :name "klient 1",
:product-groups {"group1" {:id 1, :products {:eggs {:amount 2, :price 43},
[{:id 1, :name "klient 1" :prices nil
:product-groups {"group1" {:id 1 :products {:eggs {:amount 2, :price 43},
:milk {:amount 32, :price nil}}},
"group 2" {:id 2, :products {:milk {:amount 1, :price 91},
"group 2" {:id 2 :products {:milk {:amount 1, :price 91},
:eggs {:amount 6, :price 23},
:carrots {:amount 89, :price nil}}}}}
{:id 2, :name "klient 2",
:product-groups {"group 3" {:id 3, :products {:milk {:amount 41, :price 12},
{:id 2, :name "klient 2" :prices nil
:product-groups {"group 3" {:id 3 :products {:milk {:amount 41, :price 12},
:eggs {:amount 6, :price nil}}}}}])))))
(deftest test-create!
@ -99,7 +101,7 @@
(with-redefs [jdbc/execute! (constantly [])
sql/query (constantly [{:customers/id 1 :customers/name "mr blobby" :bla 123}])]
(is (= (sut/create! "1" "mr blobby")
{:customers [{:id 1 :name "mr blobby" :product-groups {}}]})))))
{:customers [{:id 1 :name "mr blobby" :prices nil :product-groups {}}]})))))
(deftest save-product-group-test

View File

@ -247,6 +247,12 @@ html body .calendar .day .product .product-name {
html body .calendar .day .product .product-amount {
width: 40px;
max-height: 5px;
margin-right: 10px;
}
html body .calendar .day .product .product-price {
width: 40px;
max-height: 5px;
}
html body .calendar .day .summary {

View File

@ -4,6 +4,7 @@
[reagent.core :as reagent]
[chicken-master.subs :as subs]
[chicken-master.html :as html]
[chicken-master.config :as config]
[chicken-master.products :as prod]
[chicken-master.events :as event]
[chicken-master.time :as time]))
@ -26,6 +27,13 @@
first :product-groups
(reduce-kv #(assoc %1 %2 (:products %3)) {})))
(defn calc-order-prices [{:keys [who products] :as order}]
(->> products
(reduce-kv (fn [coll prod {:keys [amount price]}]
(assoc-in coll [prod :final-price]
(prod/calc-price (:id who) prod price amount))) products)
(assoc order :products)))
(defn order-form
([order] (order-form order #{:who :day :notes :products :group-products}))
([order fields]
@ -59,7 +67,9 @@
(html/input :notes "notka"
{:default (:notes @state)}))
(when (:products fields)
[prod/products-edit (:products @state) :available-prods available-prods])]))))
[prod/products-edit (:products @state)
:available-prods available-prods
:fields (if (config/settings :prices) #{:amount :price} #{:amount})])]))))
(defn edit-order []
(html/modal
@ -68,7 +78,7 @@
;; On success
:on-submit (fn [form] (re-frame/dispatch [::event/save-order (format-raw-order form)]))))
(defn format-order [settings {:keys [id who day hour notes products state]}]
(defn format-order [settings {:keys [id who day hour notes state products]}]
[:div {:class [:order state] :key (gensym)
:draggable true
:on-drag-start #(-> % .-dataTransfer (.setData "text" id))}
@ -93,6 +103,7 @@
(into [:div {:class :products}]))])
(defn day [settings [date orders]]
(let [orders (map calc-order-prices orders)]
[:div {:class [:day (when (-> date time/parse-date time/today?) :today)]
:on-drag-over #(.preventDefault %)
:on-drop #(let [id (-> % .-dataTransfer (.getData "text") prod/num-or-nil)]
@ -101,9 +112,11 @@
[:div {:class :day-header} (-> date time/parse-date time/format-date)]
[:div
[:div {:class :orders}
(if (settings :hide-fulfilled-orders)
(->> orders (remove (comp #{:fulfilled} :state)) (map (partial format-order settings)))
(map (partial format-order settings) orders))
(->> (if (settings :hide-fulfilled-orders)
(remove (comp #{:fulfilled} :state) orders)
orders)
(map (partial format-order settings))
doall)
(when (settings :show-day-add-order)
[:button {:type :button
:on-click #(re-frame/dispatch [::event/edit-order date])} "+"])
@ -113,10 +126,10 @@
[:div {:class :header} "w sumie:"]
(->> orders
(map :products)
(apply merge-with +)
(apply merge-with (partial merge-with +))
(sort-by first)
(map (partial prod/format-product settings))
(into [:div {:class :products-sum}]))])]]])
(into [:div {:class :products-sum}]))])]]]))
(defn calendar [days settings]
(->> days

View File

@ -32,7 +32,7 @@
:editable-number-inputs (get-setting :editable-number-inputs false) ; only allow number modifications in the edit modal
:hide-fulfilled-orders (get-setting :hide-fulfilled-orders false)
:prices (get-setting :prices false)
:prices (get-setting :prices true)
:backend-url (get-setting :backend-url
(if (= (.. js/window -location -href) "http://localhost:8280/")

View File

@ -135,6 +135,9 @@
:overflow :hidden
:margin-right "10px"}]
[:.product-amount {:width "40px"
:max-height "5px"
:display :inline-block}]
[:.product-price {:width "40px"
:max-height "5px"}]
]]

View File

@ -5,6 +5,7 @@
[chicken-master.products :as prod]
[chicken-master.subs :as subs]
[chicken-master.html :as html]
[chicken-master.config :as config]
[chicken-master.events :as event]))
(defn order-adder [order who]
@ -47,6 +48,16 @@
(when (and (:name @state) (:products @state))
(re-frame/dispatch [::event/save-product-group (:id who) (assoc @state :products %)])))]])])))
(defn price-adder [who]
(let [state (reagent/atom (:prices who))]
(fn []
[:details {:class :customer-prices}
[:summary "Ceny"]
[prod/products-edit state
:fields #{:price}
:getter-fn #(when (seq @state)
(re-frame/dispatch [::event/save-customer-prices (:id who) @state]))]])))
(defn show-customers []
(html/modal
@ -57,12 +68,16 @@
(let [client-orders (->> @(re-frame/subscribe [::subs/orders])
vals
(group-by #(get-in % [:who :id])))]
(doall
(for [{:keys [name id] :as who} @(re-frame/subscribe [::subs/available-customers])]
[:details {:class "client" :key (gensym)}
[:details {:class :client :key (gensym)}
[:summary [:span name [:button {:on-click #(re-frame/dispatch
[::event/confirm-action
"na pewno usunąć?"
::event/remove-customer id])} "-"]]]
(when (config/settings :prices)
[price-adder who])
[:details {:class :customer}
[:summary "Stałe zamówienia"]
(for [group (:product-groups who)]
@ -74,5 +89,5 @@
[:summary "Zamówienia"]
[order-adder who]
(for [order (reverse (sort-by :day (client-orders id)))]
[order-adder (assoc order :key (gensym)) who])]]))]
[order-adder (assoc order :key (gensym)) who])]])))]
:class :wide-popup))

View File

@ -178,6 +178,13 @@
:body group
:on-success ::process-stock)}))
(re-frame/reg-event-fx
::save-customer-prices
(fn [_ [_ id products]]
{:dispatch [::start-loading]
:http-xhrio (http-request :post (str "customers/" id "/prices")
:body products
:on-success ::process-stock)}))
;;; Storage events

View File

@ -4,7 +4,6 @@
[re-frame.core :as re-frame]
[reagent.core :as reagent]
[chicken-master.html :as html]
[chicken-master.config :as config]
[chicken-master.subs :as subs]))
(defn num-or-nil [val]
@ -46,30 +45,31 @@
(apply merge-with +))]))
(into {})))
(defn product-item [available state what]
(defn product-item [available state fields what]
(let [id (gensym)]
[:div {:class :product-item-edit :key (gensym)}
[:div {:class :input-item}
;; [:label {:for :product} "co"]
[:select {:name (str "product-" id) :id :product :defaultValue (or (some-> what name) "-")
:on-change #(let [prod (-> % .-target .-value keyword)]
(if-not (= prod :-)
(swap! state assoc prod (+ (@state prod) (@state what))))
(if-not (= prod :-) (swap! state assoc prod {}))
(swap! state dissoc what))}
(for [product (->> available (concat [what]) (remove nil?) sort vec)]
[:option {:key (gensym) :value product} (name product)])
[:option {:key (gensym) :value nil} "-"]]]
(when (:amount fields)
(number-input (str "amount-" id) nil (get-in @state [what :amount])
#(swap! state assoc-in [what :amount] (-> % .-target .-value num-or-nil)))
(when (config/settings :prices)
#(swap! state assoc-in [what :amount] (-> % .-target .-value num-or-nil))))
(when (:price fields)
[:div {:class :stock-product-price}
(number-input (str "price-" id) "cena" (format-price (get-in @state [what :price]))
#(swap! state assoc-in
[what :price]
(some-> % .-target .-value num-or-nil normalise-price)))])]))
(defn products-edit [state & {:keys [available-prods getter-fn]
:or {available-prods @(re-frame/subscribe [::subs/available-products])}}]
(defn products-edit [state & {:keys [available-prods getter-fn fields]
:or {available-prods @(re-frame/subscribe [::subs/available-products])
fields #{:amount :price}}}]
(let [all-product-names (-> available-prods keys set)]
(swap! state #(or % {}))
(fn []
@ -78,7 +78,7 @@
(conj (->> @state (map first) vec) nil)
(map first @state))
products (->> product-names
(map (partial product-item available state))
(map (partial product-item available state fields))
(into [:div {:class :product-items-edit}]))]
(if getter-fn
(conj products
@ -86,12 +86,21 @@
:on-click #(getter-fn (dissoc @state nil))} "ok"])
products)))))
(defn format-product [settings [product {:keys [amount price]}]]
(defn calc-price [who what price amount]
(when-let [price (or price
(get-in @(re-frame/subscribe [::subs/customer-prices]) [who what])
(get-in @(re-frame/subscribe [::subs/available-products]) [what :price]))]
(* amount price)))
(defn format-product [settings [product {:keys [amount final-price]}]]
[:div {:key (gensym) :class :product}
[:span {:class :product-name} product]
(if (settings :editable-number-inputs)
(number-input (str "amount-" product) "" amount nil)
[:span {:class :product-amount} amount])])
[:span {:class :product-amount} amount])
(when (settings :prices)
[:span {:class :product-price}
(format-price final-price)])])
(defn item-adder [& {:keys [type value callback button class]
:or {type :text value "" button nil}}]

View File

@ -8,6 +8,12 @@
(re-frame/reg-sub ::available-products (fn [db] (:products db)))
(re-frame/reg-sub ::available-customers (fn [db] (:customers db)))
(re-frame/reg-sub
::customer-prices
(fn [db]
(->> db :customers
(reduce (fn [col {:keys [id prices]}]
(assoc col id (reduce-kv #(assoc %1 %2 (:price %3)) {} prices))) {}))))
(re-frame/reg-sub ::orders (fn [db] (:orders db)))
(defn- show-modal? [modal db] (and (-> modal db :show) (-> db :loading? zero?)))

View File

@ -1,8 +1,57 @@
(ns chicken-master.calendar-test
(:require
[chicken-master.calendar :as sut]
[day8.re-frame.test :as rf-test]
[re-frame.core :as rf]
[cljs.test :refer-macros [deftest is testing]]))
(defn set-db [updates]
(rf/reg-event-db
::merge-db
(fn [db [_ incoming]] (merge db incoming)))
(rf/dispatch [::merge-db updates]))
(deftest calc-order-prices-test
(let [order {:who {:name "bla" :id 123} :day "2020-10-10" :notes "ble"}]
(testing "no products"
(is (= (sut/calc-order-prices order) (assoc order :products nil))))
(testing "prices set in order"
(is (= (sut/calc-order-prices (assoc order :products {:eggs {:amount 12 :price 2}}))
(assoc order :products {:eggs {:amount 12 :price 2 :final-price 24}}))))
(testing "prices set per customer"
(rf-test/run-test-sync
(set-db {:customers [{:id 123 :prices {:eggs {:price 3}}}]})
(is (= (sut/calc-order-prices (assoc order :products {:eggs {:amount 12}}))
(assoc order :products {:eggs {:amount 12 :final-price 36}})))))
(testing "prices set globally"
(rf-test/run-test-sync
(set-db {:products {:eggs {:price 4}}})
(is (= (sut/calc-order-prices (assoc order :products {:eggs {:amount 12}}))
(assoc order :products {:eggs {:amount 12 :final-price 48}})))))
(testing "no price set"
(rf-test/run-test-sync
(set-db {:products {}})
(is (= (sut/calc-order-prices (assoc order :products {:eggs {:amount 12}}))
(assoc order :products {:eggs {:amount 12 :final-price nil}})))))
(testing "all together"
(rf-test/run-test-sync
(set-db {:products {:eggs {:price 4}}
:customers [{:id 123 :prices {:cows {:price 3}}}]})
(is (= (sut/calc-order-prices
(assoc order :products {:eggs {:amount 12}
:cows {:amount 2}
:milk {:amount 3 :price 5}
:carrots {:amount 6}}))
(assoc order :products {:eggs {:amount 12 :final-price 48}
:cows {:amount 2 :final-price 6}
:milk {:amount 3 :price 5 :final-price 15}
:carrots {:amount 6 :final-price nil}})))))))
(deftest format-raw-order-test
(testing "no products"
(is (= (sut/format-raw-order {}) {:who {:name nil :id nil} :day nil :notes nil :products {}}))