From 4ddfa94318bc25a379ecb85d5dab2a3c9ecfc8e8 Mon Sep 17 00:00:00 2001 From: Daniel O'Connell Date: Thu, 11 Mar 2021 23:25:28 +0100 Subject: [PATCH] frontend product groups --- backend/src/chicken_master/customers.clj | 4 +- frontend/resources/public/css/screen.css | 21 +++++- frontend/src/chicken_master/calendar.cljs | 71 ++++++++++++++----- frontend/src/chicken_master/css.clj | 18 +++-- frontend/src/chicken_master/customers.cljs | 45 +++++++++--- frontend/src/chicken_master/events.cljs | 9 +++ frontend/src/chicken_master/html.cljs | 7 +- frontend/src/chicken_master/products.cljs | 9 ++- .../test/chicken_master/calendar_test.cljs | 47 +++++++++--- frontend/test/chicken_master/events_test.cljs | 12 +++- frontend/test/chicken_master/time_test.cljs | 8 +-- 11 files changed, 193 insertions(+), 58 deletions(-) diff --git a/backend/src/chicken_master/customers.clj b/backend/src/chicken_master/customers.clj index 8047798..f523c8a 100644 --- a/backend/src/chicken_master/customers.clj +++ b/backend/src/chicken_master/customers.clj @@ -5,7 +5,9 @@ (defn get-all [user-id] (->> (sql/query db/db-uri ["select * from customers where deleted is null AND user_id = ?" user-id]) - (map (fn [{:customers/keys [id name]}] {:id id :name name})))) + (map (fn [{:customers/keys [id name]}] {:id id :name name + :product-groups [{:name "bla" :products {:eggs 2 :carrots 13}} + {:name "ble" :products {:eggs 12 :milk 3}}]})))) (defn get-by-name [tx user-id name] (:customers/id (db/get-by-id tx user-id :customers (:name name) :name))) diff --git a/frontend/resources/public/css/screen.css b/frontend/resources/public/css/screen.css index f23bbee..7b58379 100644 --- a/frontend/resources/public/css/screen.css +++ b/frontend/resources/public/css/screen.css @@ -88,6 +88,10 @@ html body .popup .popup-content ..popup-form-buttons * { margin: 20px; } +html body .wide-popup .popup-content { + width: 45%; +} + html body .scroll-button { display: none; } @@ -144,6 +148,12 @@ html body .scroll-button { width: 15%; } + + + html body .wide-popup .popup-content { + width: 45%; + } + html body .calendar { display: grid; grid-template-columns: 25% 25% 25% 25%; @@ -266,21 +276,26 @@ html body .customers-modal details { padding: 0.5em; } -html body .customers-modal details.customer-order { +html body .customers-modal .customer-block { margin-left: 1em; padding: 0.5em; } -html body .customers-modal details.customer-order .order-date-picker { +html body .customers-modal .customer-block .order-date-picker { display: inline-block; width: 75%; cursor: pointer; } -html body .customers-modal details.customer-order .product-item-edit { +html body .customers-modal .customer-block .product-item-edit { margin-left: 1em; } +html body .customer-product-group-edit { + margin-left: 1.2em; + padding: 0.5em; +} + input::-webkit-outer-spin-button { -webkit-appearance: none; margin: 0; diff --git a/frontend/src/chicken_master/calendar.cljs b/frontend/src/chicken_master/calendar.cljs index 9e232d3..9f05630 100644 --- a/frontend/src/chicken_master/calendar.cljs +++ b/frontend/src/chicken_master/calendar.cljs @@ -8,7 +8,7 @@ [chicken-master.events :as event] [chicken-master.time :as time])) -(defn format-raw-order [{:strs [who who-id notes] :as raw-values}] +(defn format-raw-order [{:strs [day who who-id notes] :as raw-values}] {:who {:name who :id (if (prod/num-or-nil who-id) (prod/num-or-nil who-id) @@ -17,24 +17,63 @@ (some->> @(re-frame/subscribe [::subs/available-customers]) (filter (comp #{who} :name)) first :id))} + :day day :notes notes :products (prod/collect-products (remove (comp #{"who" "notes"} first) raw-values))}) -(defn order-form [order] - (let [state (reagent/atom (or order{})) - customers @(re-frame/subscribe [::subs/available-customers]) - available-prods @(re-frame/subscribe [::subs/available-products])] - (fn [] - [:div - (let [who (:who @state)] - [:div - (html/input :who "kto" {:required true :default (:name who) :list :customers}) - (into [:datalist {:id :customers}] - (for [cust customers] [:option {:value (:name cust) :id (:id cust)}])) - [:input {:id :who-id :name :who-id :type :hidden :value (or (:id who) "")}]]) - (html/input :notes "notka" - {:default (:notes @state)}) - [prod/products-edit (:products @state) :available-prods available-prods]]))) +(defn get-group-products [customers who] + (some->> customers + (filter (comp #{who} :name)) + first + :product-groups + (reduce #(assoc %1 (:name %2) (:products %2)) {}))) + +(defn group-products [state] + [:div {:class :input-item} + [:label {:for :order-group-products} "stałe"] + [:select {:class :order-group-products :id :order-group-products + :value "-" :on-change #(some->> % .-target .-value + (get (:group-products @state)) + (reset! (:products @state)))} + [:option "-"] + (for [[group _] (:group-products @state)] + [:option {:key (gensym)} group])]]) + +(defn order-form + ([order] (order-form order #{:who :day :notes :products :group-products})) + ([order fields] + (let [customers @(re-frame/subscribe [::subs/available-customers]) + available-prods @(re-frame/subscribe [::subs/available-products]) + state (-> (or order {}) + (update :products reagent/atom) + (assoc :group-products + (get-group-products customers (-> order :who :name))) + reagent/atom)] + (fn [] + [:div + (when (:who fields) + (let [who (:who @state)] + [:div + (html/input :who "kto" {:required true + :default (:name who) + :list :customers + :on-blur #(->> % .-target .-value + (get-group-products customers) + (swap! state assoc :group-products))}) + (into [:datalist {:id :customers}] + (for [cust customers] [:option {:value (:name cust) :id (:id cust)}])) + [:input {:id :who-id :name :who-id :type :hidden :value (or (:id who) "")}]])) + + (when (:day fields) + (html/input :day "dzień" {:type :date :required true :default (:day order)})) + (when (and (:group-products fields) (-> @state :group-products seq)) + [group-products state]) + (when (:notes fields) + (html/input :notes "notka" + {:default (:notes @state)})) + (when (:products fields) + (prn "Asd") + [prod/products-edit (:products @state) :available-prods available-prods])])))) (defn edit-order [] (html/modal diff --git a/frontend/src/chicken_master/css.clj b/frontend/src/chicken_master/css.clj index fb9d878..5faa2e9 100644 --- a/frontend/src/chicken_master/css.clj +++ b/frontend/src/chicken_master/css.clj @@ -36,12 +36,13 @@ :right "0" :bottom "0"}]] - [:.popup {:position :fixed - :height "100%" - :width "100%" - :overflow :auto - :z-index 1 - :background-color "rgba(0,0,0,0.4)"} + [:.popup + {:position :fixed + :height "100%" + :width "100%" + :overflow :auto + :z-index 1 + :background-color "rgba(0,0,0,0.4)"} [:.popup-content { :background-color "#fefefe" :margin "15% auto" @@ -54,6 +55,7 @@ :display :inline-block}]] [:..popup-form-buttons {:margin "10px"} [:* {:margin "20px"}]]]] + [:.wide-popup [:.popup-content {:width "45%"}]] [:.scroll-button {:display :none}] (at-media @@ -85,6 +87,7 @@ :border "1px solid #888" :width "15%" }]] + [:.wide-popup [:.popup-content {:width "45%"}]] [:.calendar {:display :grid :grid-template-columns "25% 25% 25% 25%" :grid-template-rows "50% 50%"}]) @@ -147,9 +150,10 @@ [:.customers-modal [:details {:padding "0.5em"}] - [:details.customer-order {:margin-left "1em" :padding "0.5em"} + [:.customer-block {:margin-left "1em" :padding "0.5em"} [:.order-date-picker {:display :inline-block :width "75%" :cursor :pointer}] [:.product-item-edit {:margin-left "1em"}]]] + [:.customer-product-group-edit {:margin-left "1.2em" :padding "0.5em"}] ]] diff --git a/frontend/src/chicken_master/customers.cljs b/frontend/src/chicken_master/customers.cljs index 29fbcad..08c35a9 100644 --- a/frontend/src/chicken_master/customers.cljs +++ b/frontend/src/chicken_master/customers.cljs @@ -10,7 +10,7 @@ (defn order-adder [order] (let [state (reagent/atom order)] (fn [] - [:details {:class (or (:class order) :customer-order) :key (gensym) :open (:open @state)} + [:details {:class (or (:class order) :customer-block) :key (gensym) :open (:open @state)} [:summary {:on-click #(swap! state update :open not)} [prod/item-adder :type :date @@ -18,9 +18,30 @@ :class :order-date-picker :callback (fn [day] (swap! state #(assoc % :day day :open true)))]] (if (:day @state) - [prod/products-edit (:products @state) + [prod/products-edit (reagent/atom (or (:products @state) {})) :getter-fn #(re-frame/dispatch [::event/save-order (assoc @state :products %)])])]))) +(defn product-group-adder [who product-group] + (let [state (reagent/atom product-group)] + (fn [] + [:div {:class :customer-block} + (if-not (:edit @state) + [:div + [:span {:class :customer-product-group-name} (:name @state)] + [:button {:type :button :on-click #(swap! state assoc :edit true)} + (if (:name @state) "e" "+")]] + + [:div {:class :customer-product-group-edit} + (html/input :customer-product-group-name "nazwa" + {:default (:name @state) + :on-blur #(swap! state assoc :name (-> % .-target .-value))}) + [prod/products-edit (reagent/atom (or (:products @state) {})) + :getter-fn #(do + (swap! state dissoc :edit) + (when (and (:name @state) (:products @state)) + (re-frame/dispatch [::event/save-product-group (:id who) (assoc @state :products %)])))]])]))) + + (defn show-customers [] (html/modal :clients @@ -31,13 +52,21 @@ vals (group-by #(get-in % [:who :id])))] (for [{:keys [name id] :as who} @(re-frame/subscribe [::subs/available-customers])] - [:details {:class "client" :key (gensym)} + [:details {:open true :class "client" :key (gensym)} [:summary [:span name [:button {:on-click #(re-frame/dispatch [::event/confirm-action "na pewno usunąć?" ::event/remove-customer id])} "-"]]] - [order-adder {:who who}] - (for [order (reverse (sort-by :day (client-orders id)))] - [order-adder (assoc order :key (gensym))]) - ]))] - )) + [:details {:class :customer} + [:summary "Stałe zamówienia"] + (for [group (:product-groups who)] + [:div {:key (gensym)} + [product-group-adder who group]]) + [product-group-adder {}]] + + [:details {:class :client-orders} + [:summary "Zamówienia"] + [order-adder {:who who}] + (for [order (reverse (sort-by :day (client-orders id)))] + [order-adder (assoc order :key (gensym))])]]))] + :class :wide-popup)) diff --git a/frontend/src/chicken_master/events.cljs b/frontend/src/chicken_master/events.cljs index 072bd9e..f0601f7 100644 --- a/frontend/src/chicken_master/events.cljs +++ b/frontend/src/chicken_master/events.cljs @@ -170,6 +170,15 @@ :http-xhrio (http-request :delete (str "customers/" id) :on-success ::process-stock)})) +(re-frame/reg-event-fx + ::save-product-group + (fn [_ [_ id group]] + {:dispatch [::start-loading] + :http-xhrio (http-request :post (str "customers/" id "/product-group") + :body group + :on-success ::process-stock)})) + + ;;; Storage events (re-frame/reg-event-fx diff --git a/frontend/src/chicken_master/html.cljs b/frontend/src/chicken_master/html.cljs index 9d6876c..8e0147e 100644 --- a/frontend/src/chicken_master/html.cljs +++ b/frontend/src/chicken_master/html.cljs @@ -39,10 +39,11 @@ content [:div {:class :form-buttons} [:button {:type :button :on-click #(re-frame/dispatch [::event/hide-modal modal-id])} "ok"]]]]) - ([modal-id content & {:keys [on-submit submit-text show-cancel] + ([modal-id content & {:keys [on-submit submit-text show-cancel class] :or {submit-text "ok" - show-cancel true}}] - [:div {:class :popup :on-click #(re-frame/dispatch [::event/hide-modal modal-id])} + show-cancel true + class :popup}}] + [:div {:class [:popup class] :on-click #(re-frame/dispatch [::event/hide-modal modal-id])} [:form {:action "#" :class :popup-content :on-click #(.stopPropagation %) diff --git a/frontend/src/chicken_master/products.cljs b/frontend/src/chicken_master/products.cljs index 220ee2e..12f9d89 100644 --- a/frontend/src/chicken_master/products.cljs +++ b/frontend/src/chicken_master/products.cljs @@ -50,10 +50,9 @@ (number-input (str "amount-" id) nil (@state what) #(swap! state assoc what (-> % .-target .-value num-or-nil)))])) -(defn products-edit [selected-prods & {:keys [available-prods getter-fn] - :or {available-prods @(re-frame/subscribe [::subs/available-products])}}] - (let [state (reagent/atom (or selected-prods {})) - all-product-names (-> available-prods keys set)] +(defn products-edit [state & {:keys [available-prods getter-fn] + :or {available-prods @(re-frame/subscribe [::subs/available-products])}}] + (let [all-product-names (-> available-prods keys set)] (fn [] (let [available (remove (partial get @state) all-product-names) product-names (if (seq available) @@ -80,7 +79,7 @@ (let [state (reagent/atom value)] (fn [] [:div {:class class :on-click #(.stopPropagation %)} - [:input {:type type :name :user-name :default value :value @state + [:input {:type type :name :user-name :default-value value :on-change #(let [val (-> % .-target .-value)] (reset! state val) (if-not button (callback val)))}] diff --git a/frontend/test/chicken_master/calendar_test.cljs b/frontend/test/chicken_master/calendar_test.cljs index 2b72952..f505034 100644 --- a/frontend/test/chicken_master/calendar_test.cljs +++ b/frontend/test/chicken_master/calendar_test.cljs @@ -5,18 +5,20 @@ (deftest format-raw-order-test (testing "no products" - (is (= (sut/format-raw-order {}) {:who {:name nil :id nil} :notes nil :products {}})) + (is (= (sut/format-raw-order {}) {:who {:name nil :id nil} :day nil :notes nil :products {}})) (is (= (sut/format-raw-order {"who" "bla" "notes" "ble"}) - {:who {:name "bla" :id nil} :notes "ble" :products {}})) - (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble"}) - {:who {:name "bla" :id 123} :notes "ble" :products {}}))) + {:who {:name "bla" :id nil} :day nil :notes "ble" :products {}})) + (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" "day" "2020-10-10"}) + {:who {:name "bla" :id 123} :day "2020-10-10" :notes "ble" :products {}}))) (testing "decent products" (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" + "day" "2020-10-10" "product-eggs" "eggs" "amount-eggs" "12" "product-cows" "cows" "amount-cows" "22" "product-milk" "milk" "amount-milk" "3.2"}) - {:who {:name "bla" :id 123} :notes "ble" :products {:eggs 12 :cows 22 :milk 3.2}}))) + {:who {:name "bla" :id 123} :day "2020-10-10" :notes "ble" + :products {:eggs 12 :cows 22 :milk 3.2}}))) (testing "duplicate products" (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" @@ -25,21 +27,46 @@ "product-cows1" "cows" "amount-cows1" "1" "product-cows2" "cows" "amount-cows2" "2" "product-milk" "milk" "amount-milk" "3.2"}) - {:who {:name "bla" :id 123} :notes "ble" :products {:eggs 24 :cows 3 :milk 3.2}}))) + {:who {:name "bla" :id 123} :day nil :notes "ble" :products {:eggs 24 :cows 3 :milk 3.2}}))) (testing "unselected are ignored" - (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" + (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" "day" "2020-10-10" "product-eggs" "eggs" "amount-eggs" "12" "product-bad1" "" "amount-bad1" "12" "product-bad2" "" "amount-bad2" "1" "product-milk" "milk" "amount-milk" "3.2" "product-bad3" "" "amount-bad3" "2"}) - {:who {:name "bla" :id 123} :notes "ble" :products {:eggs 12 :milk 3.2}}))) + {:who {:name "bla" :id 123} :day "2020-10-10" :notes "ble" :products {:eggs 12 :milk 3.2}}))) (testing "items with 0 are removed" - (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" + (is (= (sut/format-raw-order {"who" "bla" "who-id" "123" "notes" "ble" "day" "2020-10-10" "product-eggs" "eggs" "amount-eggs" "12" "product-eggs1" "eggs" "amount-eggs1" "0" "product-cow" "cow" "amount-cow" "0" "product-milk" "milk" "amount-milk" "3.2"}) - {:who {:name "bla" :id 123} :notes "ble" :products {:eggs 12 :milk 3.2}})))) + {:who {:name "bla" :id 123} :day "2020-10-10" :notes "ble" :products {:eggs 12 :milk 3.2}})))) + +(def customers + [{:id 1 :name "mr blobby" :product-groups [{:name "group 1" :products {:eggs 1 :carrots 2}} + {:name "group 2" :products {:eggs 11 :carrots 2}} + {:name "group 3" :products {:milk 2 :eggs 12}}]} + {:id 2 :name "johnny D" :product-groups [{:name "group 4" :products {:eggs 2}} + {:name "group 5" :products {:milk 2}}]} + {:id 3 :name "joe" :product-groups []} + {:id 4 :name "mark"}]) + +(deftest get-group-products-test + (testing "products get returned if the customer has them" + (is (= (sut/get-group-products customers "mr blobby") + {"group 1" {:eggs 1, :carrots 2} + "group 2" {:eggs 11, :carrots 2} + "group 3" {:milk 2, :eggs 12}}))) + + (testing "no products are returned if the customer has none" + (is (= (sut/get-group-products customers "joe") {}))) + + (testing "missing products are handled" + (is (nil? (sut/get-group-products customers "mark")))) + + (testing "missing customers are handled" + (is (nil? (sut/get-group-products customers "bla bla bla"))))) diff --git a/frontend/test/chicken_master/events_test.cljs b/frontend/test/chicken_master/events_test.cljs index f2a945e..2a24d3e 100644 --- a/frontend/test/chicken_master/events_test.cljs +++ b/frontend/test/chicken_master/events_test.cljs @@ -366,7 +366,17 @@ (is (= method :delete)) (is (= uri "customers/1")))) - (rf/dispatch [::sut/remove-customer 1])))) + (rf/dispatch [::sut/remove-customer 1]))) + + ;; FIXME: the request handler is not being overloaded + (testing "product groups can be saved" + (rf-test/run-test-sync + (param-validator :http-xhrio (fn [[{:keys [method uri body]}]] + (is (= method :post)) + (is (= uri "customers/1/product-group")) + (is (= body {:name "bla" :products {:eggs 1 :milk 3}})))) + + (rf/dispatch [::sut/save-product-group 1 {:name "bla" :products {:eggs 1 :milk 3}}])))) (deftest stock-tests diff --git a/frontend/test/chicken_master/time_test.cljs b/frontend/test/chicken_master/time_test.cljs index 7776b36..d12a16d 100644 --- a/frontend/test/chicken_master/time_test.cljs +++ b/frontend/test/chicken_master/time_test.cljs @@ -60,14 +60,14 @@ (deftest test-format-date (testing "don't show date" - (with-redefs [sut/settings (atom {:show-date nil})] - (is (nil? (sut/format-date (new Date 2020 01 01)))))) + (with-redefs [sut/settings (atom {:date-format ""})] + (is (= (sut/format-date (new Date 2020 01 01)) "")))) (testing "without name" - (with-redefs [sut/settings (atom {:show-date true :show-day-name-with-date nil})] + (with-redefs [sut/settings (atom {:date-format "%m/%d"})] (is (= (sut/format-date (new Date 2020 01 01)) "2/1")))) (testing "with name" - (with-redefs [sut/settings (atom {:show-date true :show-day-name-with-date true + (with-redefs [sut/settings (atom {:date-format "%D %m/%d" :day-names ["Niedz" "Pon" "Wt" "Śr" "Czw" "Pt" "Sob"]})] (is (= (sut/format-date (new Date 2020 01 01)) "Sob 2/1")))))