From 06059b830de5d2d3c8102b62b494017488aba776 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Sat, 27 Mar 2021 17:32:47 +0100 Subject: [PATCH] calculate prices --- backend/src/chicken_master/api.clj | 5 ++ backend/src/chicken_master/customers.clj | 38 +++++++++-- .../test/chicken_master/customers_test.clj | 34 +++++----- frontend/resources/public/css/screen.css | 6 ++ frontend/src/chicken_master/calendar.cljs | 65 +++++++++++-------- frontend/src/chicken_master/config.cljs | 2 +- frontend/src/chicken_master/css.clj | 5 +- frontend/src/chicken_master/customers.cljs | 37 +++++++---- frontend/src/chicken_master/events.cljs | 7 ++ frontend/src/chicken_master/products.cljs | 33 ++++++---- frontend/src/chicken_master/subs.cljs | 6 ++ .../test/chicken_master/calendar_test.cljs | 49 ++++++++++++++ 12 files changed, 214 insertions(+), 73 deletions(-) diff --git a/backend/src/chicken_master/api.clj b/backend/src/chicken_master/api.clj index f15abbc..6e618ac 100644 --- a/backend/src/chicken_master/api.clj +++ b/backend/src/chicken_master/api.clj @@ -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)) diff --git a/backend/src/chicken_master/customers.clj b/backend/src/chicken_master/customers.clj index f65828e..d932a5c 100644 --- a/backend/src/chicken_master/customers.clj +++ b/backend/src/chicken_master/customers.clj @@ -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)))) diff --git a/backend/test/chicken_master/customers_test.clj b/backend/test/chicken_master/customers_test.clj index d5f624c..5825629 100644 --- a/backend/test/chicken_master/customers_test.clj +++ b/backend/test/chicken_master/customers_test.clj @@ -71,35 +71,37 @@ (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}, - :milk {:amount 32, :price nil}}}, - "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}, - :eggs {:amount 6, :price nil}}}}}]))))) + [{: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}, + :eggs {:amount 6, :price 23}, + :carrots {:amount 89, :price nil}}}}} + {: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! (testing "correct format is returned" (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 diff --git a/frontend/resources/public/css/screen.css b/frontend/resources/public/css/screen.css index 4d2bf5a..8503bfd 100644 --- a/frontend/resources/public/css/screen.css +++ b/frontend/resources/public/css/screen.css @@ -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 { diff --git a/frontend/src/chicken_master/calendar.cljs b/frontend/src/chicken_master/calendar.cljs index 8b0c037..2349ad8 100644 --- a/frontend/src/chicken_master/calendar.cljs +++ b/frontend/src/chicken_master/calendar.cljs @@ -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,30 +103,33 @@ (into [:div {:class :products}]))]) (defn day [settings [date 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)] - (.preventDefault %) - (re-frame/dispatch [::event/move-order id date]))} - [: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)) - (when (settings :show-day-add-order) - [:button {:type :button - :on-click #(re-frame/dispatch [::event/edit-order date])} "+"]) - (when (seq (map :products orders)) - [:div {:class :summary} - [:hr {:class :day-seperator}] - [:div {:class :header} "w sumie:"] - (->> orders - (map :products) - (apply merge-with +) - (sort-by first) - (map (partial prod/format-product settings)) - (into [:div {:class :products-sum}]))])]]]) + (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)] + (.preventDefault %) + (re-frame/dispatch [::event/move-order id date]))} + [:div {:class :day-header} (-> date time/parse-date time/format-date)] + [:div + [:div {:class :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])} "+"]) + (when (seq (map :products orders)) + [:div {:class :summary} + [:hr {:class :day-seperator}] + [:div {:class :header} "w sumie:"] + (->> orders + (map :products) + (apply merge-with (partial merge-with +)) + (sort-by first) + (map (partial prod/format-product settings)) + (into [:div {:class :products-sum}]))])]]])) (defn calendar [days settings] (->> days diff --git a/frontend/src/chicken_master/config.cljs b/frontend/src/chicken_master/config.cljs index 95df88d..6086945 100644 --- a/frontend/src/chicken_master/config.cljs +++ b/frontend/src/chicken_master/config.cljs @@ -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/") diff --git a/frontend/src/chicken_master/css.clj b/frontend/src/chicken_master/css.clj index 5faa0d3..dc255c5 100644 --- a/frontend/src/chicken_master/css.clj +++ b/frontend/src/chicken_master/css.clj @@ -135,7 +135,10 @@ :overflow :hidden :margin-right "10px"}] [:.product-amount {:width "40px" - :max-height "5px"}] + :max-height "5px" + :display :inline-block}] + [:.product-price {:width "40px" + :max-height "5px"}] ]] [:.summary {:margin-top "10px"}]]] diff --git a/frontend/src/chicken_master/customers.cljs b/frontend/src/chicken_master/customers.cljs index 197db12..6c0c3e2 100644 --- a/frontend/src/chicken_master/customers.cljs +++ b/frontend/src/chicken_master/customers.cljs @@ -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])))] - (for [{:keys [name id] :as who} @(re-frame/subscribe [::subs/available-customers])] - [:details {:class "client" :key (gensym)} - [:summary [:span name [:button {:on-click #(re-frame/dispatch - [::event/confirm-action - "na pewno usunąć?" - ::event/remove-customer id])} "-"]]] + (doall + (for [{:keys [name id] :as who} @(re-frame/subscribe [::subs/available-customers])] + [: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)] @@ -70,9 +85,9 @@ [product-group-adder who group]]) [product-group-adder who []]] - [:details {:class :client-orders} - [:summary "Zamówienia"] - [order-adder who] - (for [order (reverse (sort-by :day (client-orders id)))] - [order-adder (assoc order :key (gensym)) who])]]))] + [:details {:class :client-orders} + [:summary "Zamówienia"] + [order-adder who] + (for [order (reverse (sort-by :day (client-orders id)))] + [order-adder (assoc order :key (gensym)) who])]])))] :class :wide-popup)) diff --git a/frontend/src/chicken_master/events.cljs b/frontend/src/chicken_master/events.cljs index 817a4e5..9c449d3 100644 --- a/frontend/src/chicken_master/events.cljs +++ b/frontend/src/chicken_master/events.cljs @@ -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 diff --git a/frontend/src/chicken_master/products.cljs b/frontend/src/chicken_master/products.cljs index 497ba19..db06e87 100644 --- a/frontend/src/chicken_master/products.cljs +++ b/frontend/src/chicken_master/products.cljs @@ -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} "-"]]] - (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) + (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 (: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}}] diff --git a/frontend/src/chicken_master/subs.cljs b/frontend/src/chicken_master/subs.cljs index 769b8a2..721c101 100644 --- a/frontend/src/chicken_master/subs.cljs +++ b/frontend/src/chicken_master/subs.cljs @@ -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?))) diff --git a/frontend/test/chicken_master/calendar_test.cljs b/frontend/test/chicken_master/calendar_test.cljs index ccf217e..fd09cfd 100644 --- a/frontend/test/chicken_master/calendar_test.cljs +++ b/frontend/test/chicken_master/calendar_test.cljs @@ -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 {}}))