<commit_before>;;; Author: <NAME> (<EMAIL>)
;;; Copyright (c) 2017, <NAME>

(ns receipts-client.views
  (:require
   [cljs.tools.reader :as reader]
   [cljs-time.coerce :as time-coerce]
   [cljs-time.core :as time]
   [cljs-time.format :as time-format]
   [clojure.string :as str]
   [re-com.core :as re-com]
   [re-frame.core :as re-frame]
   [reagent.core :as reagent]
   [receipts-client.routes :as routes]
   [receipts-client.utils :as utils]
   [struct.core :as struct]))


;; home

(def tight-gap "0.2rem")
(def std-gap "0.5rem")
(def title-gap "0.5rem")
(def title-width "7em")
(def tiny-field-width "5rem")
(def tight-field-width "11rem")
(def field-width "14rem")

(defn app-title []
  (let [name (re-frame/subscribe [:name])
        server (re-frame/subscribe [:server])]
    (fn []
      [re-com/title
       :label (str @name (when (= @server :development) " (local test server)"))
       :level :level1])))

(defn panel-title [label]
  [re-com/title :label label :level :level1])

(defn panel-subtitle [label]
  [re-com/title :label label :level :level2])

(defn section-title [label]
  [re-com/title :label label :level :level3])

(defn subsection-title [label]
  [re-com/title :label label :level :level4])

(defn button [dispatch label tooltip]
  [re-com/button
   :label label
   :tooltip tooltip
   :on-click #(if (vector? dispatch)
                (re-frame/dispatch dispatch)
                (dispatch))])

(defn labelled [label error component]
  (fn [label error component]
    [re-com/h-box
     :width "100%"
     :gap std-gap
     :children [[re-com/label
                 :class (if error "errmsg" "")
                 :width title-width
                 :label (str label (if (str/blank? label) "" ": "))]
                [re-com/v-box :children [component
                                         (when error
                                           [re-com/label :class "errmsg" :label error])]]]]))

;;; Per re-com, new-date-time is a goog.date.Date, while new-time is an integer encoding
;;; (+ minutes (* 100 hours)).
;;; old-date-time, and the retval, on the other hand, are instances of goog.date.DateTime
;;; [TOOD] This would be a great first place to learn spec pre-condition syntax
(defn update-date-time [old-date-time new-date-time new-time]
  (let [old-date-time (time-coerce/to-date-time old-date-time)]
    (time-coerce/to-date
     (time/date-time (time/year  (or new-date-time old-date-time))
                     (time/month (or new-date-time old-date-time))
                     (time/day   (or new-date-time old-date-time))
                     (if new-time (quot new-time 100) (time/hour old-date-time))
                     (if new-time (mod new-time 100) (time/minute old-date-time))
                     (time/second old-date-time)))))

(defn date-time-picker [& {:keys [model on-change show-multi-year? show-time?]}]
  (let [date (time-coerce/to-date-time model)
        time (if date (+ (* 100 (time/hour date)) (time/minute date)) 0)
        date-atom (reagent/atom date)
        time-atom (reagent/atom time)]
    [re-com/h-box
     :gap tight-gap
     :align :center
     :children [(when show-multi-year?
                  [re-com/button
                   :label "<-5 yrs"
                   :class "btn-primary btn-sm"
                   :on-click #(do (swap! date-atom time/minus (time/years 5))
                                  (on-change (update-date-time model @date-atom nil)))])
                [re-com/datepicker-dropdown
                 :show-today? true
                 :model date-atom
                 :on-change #(do (reset! date-atom %)
                                 (on-change (update-date-time model % nil)))]
                (when show-multi-year?
                  [re-com/button
                   :label "5 yrs->"
                   :class "btn-primary btn-sm"
                   :on-click #(do (swap! date-atom time/plus (time/years 5))
                                  (on-change (update-date-time model @date-atom nil)))])
                (when show-time?
                  [re-com/input-time
                   :model time-atom
                   :show-icon? true
                   :on-change #(do (reset! time-atom %)
                                   (on-change (update-date-time model nil %)))])]]))

(defn dropdown
  "Dropdown component

  multiple? - Allow multiple selection
  "
  [& {:keys [multiple? field-key subs-key filter-fn schema-key schema-id-key schema-label-key]
      :or {model-key-fn identity
           schema-id-key schema-key
           schema-label-key schema-key}}]
  (let [current-receipt (re-frame/subscribe [:current-receipt])
        schema (re-frame/subscribe [subs-key])]
    [(if multiple? re-com/selection-list re-com/single-dropdown)
     :width field-width
     :model ((if multiple? (partial into #{}) identity)
             (field-key @current-receipt))
     :choices (mapv (fn [{id schema-id-key
                          label schema-label-key}]
                      {:id id :label label})
                    (if filter-fn
                      (filter filter-fn @schema)
                      @schema))
     :on-change #(re-frame/dispatch [:edit-current-receipt field-key %])]))


(defn login-panel []
  (let [email (reagent/atom "")
        password (reagent/atom "")]
    (fn []
      [re-com/v-box
       :gap title-gap
       :children [(panel-title "Login")
                  [labelled "Email" nil
                   [re-com/input-text
                    :width field-width
                    :model email
                    :on-change #(reset! email %)
                    :attr {:type "Email"}]]
                  [labelled "Password" nil
                   [re-com/input-text
                    :width field-width
                    :model password
                    :on-change #(reset! password %)
                    :attr {:type "Password"}]]
                  [button [:login @email @password] "Login" "Login to server"]]])))

(defn unavailable []
  [:div [:em "(Unavailable)"]])

;;; See https://funcool.github.io/struct/latest/
;;; [TODO] Replace with spec, asap.
(def complete-receipt
  {:purchase/paymentMethod [[struct/required :message "'Paid by' missing"] struct/string]
   :purchase/date [[struct/required :message "Please specify date"] struct/positive]
   :purchase/category [[struct/required :message  "Category missing"] struct/string]
   :purchase/vendor [[struct/required :message  "Choose vendor in category"] struct/string]
   :purchase/forWhom [[struct/required :message  "Specify user(s) of this purchase"] struct/set]})

(defn validate-receipt [receipt]
  (struct/validate receipt complete-receipt))

(defn valid-receipt? [receipt]
  (struct/valid? receipt complete-receipt))

(defn receipt-page []
  (let [current-receipt (re-frame/subscribe [:current-receipt])]
    (fn []
      (let [validation-errors (first (validate-receipt @current-receipt))]
        [re-com/v-box
         :gap tight-gap
         :children
         [[labelled "Paid by"
           (:purchase/paymentMethod validation-errors)
           [dropdown :multiple? false
            :field-key :purchase/paymentMethod
            :subs-key :payment-methods
            :schema-key :paymentMethod/abbrev]]
          [labelled "Date"
           (:purchase/date validation-errors)
           [date-time-picker
            :model (or (:purchase/date @current-receipt) (time/now))
            :on-change #(re-frame/dispatch [:edit-current-receipt :purchase/date %])]]
          [labelled "Price"
           (:purchase/price validation-errors)
           [re-com/input-text
            :width field-width
            :model (or (:purchase/price @current-receipt) "0.00")
            :on-change #(re-frame/dispatch [:edit-current-receipt :purchase/price %])
            :attr {:type "number"
                   :step "0.01"}]]
          [labelled "Category"
           (:purchase/category validation-errors)
           [dropdown :multiple? false
            :field-key :purchase/category
            :subs-key :categories
            :schema-key :category/name]]
          [labelled "Vendor"
           (:purchase/vendor validation-errors)
           [dropdown :multiple? false
            :field-key :purchase/vendor
            :subs-key :vendors
            :schema-key :vendor/name
            :filter-fn #(some #{(:purchase/category @current-receipt)} (:vendor/category %))]]
          [labelled "Comment"
           (:purchase/comment validation-errors)
           [re-com/input-text
            :width field-width
            :model (or (:purchase/comment @current-receipt) "")
            :on-change #(re-frame/dispatch [:edit-current-receipt :purchase/comment %])
            :attr {:type "text"}]]
          [labelled "For Whom"
           (:purchase/forWhom validation-errors)
           [dropdown :multiple? true
            :field-key :purchase/forWhom
            :subs-key :users
            :filter-fn :user/isConsumer
            :schema-label-key :user/name
            :schema-id-key :user/abbrev]]
          [re-com/gap :size std-gap]<commit_msg>More form validation<commit_after>;;; Author: <NAME> (<EMAIL>)
;;; Copyright (c) 2017, <NAME>

(ns receipts-client.views
  (:require
   [cljs.tools.reader :as reader]
   [cljs-time.coerce :as time-coerce]
   [cljs-time.core :as time]
   [cljs-time.format :as time-format]
   [clojure.string :as str]
   [re-com.core :as re-com]
   [re-frame.core :as re-frame]
   [reagent.core :as reagent]
   [receipts-client.routes :as routes]
   [receipts-client.utils :as utils]
   [struct.core :as struct]))


;; home

(def tight-gap "0.2rem")
(def std-gap "0.5rem")
(def title-gap "0.5rem")
(def title-width "7em")
(def tiny-field-width "5rem")
(def tight-field-width "11rem")
(def field-width "14rem")

(defn app-title []
  (let [name (re-frame/subscribe [:name])
        server (re-frame/subscribe [:server])]
    (fn []
      [re-com/title
       :label (str @name (when (= @server :development) " (local test server)"))
       :level :level1])))

(defn panel-title [label]
  [re-com/title :label label :level :level1])

(defn panel-subtitle [label]
  [re-com/title :label label :level :level2])

(defn section-title [label]
  [re-com/title :label label :level :level3])

(defn subsection-title [label]
  [re-com/title :label label :level :level4])

(defn button [dispatch label tooltip]
  [re-com/button
   :label label
   :tooltip tooltip
   :on-click #(if (vector? dispatch)
                (re-frame/dispatch dispatch)
                (dispatch))])

(defn labelled [label error component]
  (fn [label error component]
    [re-com/h-box
     :width "100%"
     :gap std-gap
     :children [[re-com/label
                 :class (if error "errmsg" "")
                 :width title-width
                 :label (str label (if (str/blank? label) "" ": "))]
                [re-com/v-box :children [component
                                         (when error
                                           [re-com/label :class "errmsg" :label error])]]]]))

;;; Per re-com, new-date-time is a goog.date.Date, while new-time is an integer encoding
;;; (+ minutes (* 100 hours)).
;;; old-date-time, and the retval, on the other hand, are instances of goog.date.DateTime
;;; [TOOD] This would be a great first place to learn spec pre-condition syntax
(defn update-date-time [old-date-time new-date-time new-time]
  (let [old-date-time (time-coerce/to-date-time old-date-time)]
    (time-coerce/to-date
     (time/date-time (time/year  (or new-date-time old-date-time))
                     (time/month (or new-date-time old-date-time))
                     (time/day   (or new-date-time old-date-time))
                     (if new-time (quot new-time 100) (time/hour old-date-time))
                     (if new-time (mod new-time 100) (time/minute old-date-time))
                     (time/second old-date-time)))))

(defn date-time-picker [& {:keys [model on-change show-multi-year? show-time?]}]
  (let [date (time-coerce/to-date-time model)
        time (if date (+ (* 100 (time/hour date)) (time/minute date)) 0)
        date-atom (reagent/atom date)
        time-atom (reagent/atom time)]
    [re-com/h-box
     :gap tight-gap
     :align :center
     :children [(when show-multi-year?
                  [re-com/button
                   :label "<-5 yrs"
                   :class "btn-primary btn-sm"
                   :on-click #(do (swap! date-atom time/minus (time/years 5))
                                  (on-change (update-date-time model @date-atom nil)))])
                [re-com/datepicker-dropdown
                 :show-today? true
                 :model date-atom
                 :on-change #(do (reset! date-atom %)
                                 (on-change (update-date-time model % nil)))]
                (when show-multi-year?
                  [re-com/button
                   :label "5 yrs->"
                   :class "btn-primary btn-sm"
                   :on-click #(do (swap! date-atom time/plus (time/years 5))
                                  (on-change (update-date-time model @date-atom nil)))])
                (when show-time?
                  [re-com/input-time
                   :model time-atom
                   :show-icon? true
                   :on-change #(do (reset! time-atom %)
                                   (on-change (update-date-time model nil %)))])]]))

(defn dropdown
  "Dropdown component

  multiple? - Allow multiple selection
  "
  [& {:keys [multiple? field-key subs-key filter-fn schema-key schema-id-key schema-label-key]
      :or {model-key-fn identity
           schema-id-key schema-key
           schema-label-key schema-key}}]
  (let [current-receipt (re-frame/subscribe [:current-receipt])
        schema (re-frame/subscribe [subs-key])]
    [(if multiple? re-com/selection-list re-com/single-dropdown)
     :width field-width
     :model ((if multiple? (partial into #{}) identity)
             (field-key @current-receipt))
     :choices (mapv (fn [{id schema-id-key
                          label schema-label-key}]
                      {:id id :label label})
                    (if filter-fn
                      (filter filter-fn @schema)
                      @schema))
     :on-change #(re-frame/dispatch [:edit-current-receipt field-key %])]))


(defn login-panel []
  (let [email (reagent/atom "")
        password (reagent/atom "")]
    (fn []
      [re-com/v-box
       :gap title-gap
       :children [(panel-title "Login")
                  [labelled "Email" nil
                   [re-com/input-text
                    :width field-width
                    :model email
                    :on-change #(reset! email %)
                    :attr {:type "Email"}]]
                  [labelled "Password" nil
                   [re-com/input-text
                    :width field-width
                    :model password
                    :on-change #(reset! password %)
                    :attr {:type "Password"}]]
                  [button [:login @email @password] "Login" "Login to server"]]])))

(defn unavailable []
  [:div [:em "(Unavailable)"]])

;;; See https://funcool.github.io/struct/latest/
;;; [TODO] Replace with spec, asap.
(def complete-receipt
  {:purchase/paymentMethod [[struct/required :message "'Paid by' missing"] struct/string]
   :purchase/date [[struct/required :message "Please specify date"] struct/positive]
   :purchase/price [[struct/required :message "Please specify amount"] struct/positive]
   :purchase/category [[struct/required :message  "Category missing"] struct/string]
   :purchase/vendor [[struct/required :message  "Choose vendor in category"] struct/string]
   :purchase/forWhom [[struct/required :message  "Specify user(s) of this purchase"] struct/set]})

(defn validate-receipt [receipt]
  (struct/validate receipt complete-receipt))

(defn valid-receipt? [receipt]
  (struct/valid? receipt complete-receipt))

(defn receipt-page []
  (let [current-receipt (re-frame/subscribe [:current-receipt])]
    (fn []
      (let [validation-errors (first (validate-receipt @current-receipt))]
        [re-com/v-box
         :gap tight-gap
         :children
         [[labelled "Paid by"
           (:purchase/paymentMethod validation-errors)
           [dropdown :multiple? false
            :field-key :purchase/paymentMethod
            :subs-key :payment-methods
            :schema-id-key :paymentMethod/abbrev
            :schema-label-key :paymentMethod/name]]
          [labelled "Date"
           (:purchase/date validation-errors)
           [date-time-picker
            :model (or (:purchase/date @current-receipt) (time/now))
            :on-change #(re-frame/dispatch [:edit-current-receipt :purchase/date %])]]
          [labelled "Price"
           (:purchase/price validation-errors)
           [re-com/input-text
            :width field-width
            :model (or (:purchase/price @current-receipt) "0.00")
            :on-change #(re-frame/dispatch [:edit-current-receipt :purchase/price %])
            :change-on-blur? false
            :attr {:type "number"
                   :step "0.01"}]]
          [labelled "Category"
           (:purchase/category validation-errors)
           [dropdown :multiple? false
            :field-key :purchase/category
            :subs-key :categories
            :schema-key :category/name]]
          [labelled "Vendor"
           (:purchase/vendor validation-errors)
           [dropdown :multiple? false
            :field-key :purchase/vendor
            :subs-key :vendors
            :schema-key :vendor/name
            :filter-fn #(some #{(:purchase/category @current-receipt)} (:vendor/category %))]]
          [labelled "Comment"
           (:purchase/comment validation-errors)
           [re-com/input-text
            :width field-width
            :model (or (:purchase/comment @current-receipt) "")
            :on-change #(re-frame/dispatch [:edit-current-receipt :purchase/comment %])
            :attr {:type "text"}]]
          [labelled "For Whom"
           (:purchase/forWhom validation-errors)
           [dropdown :multiple? true
            :field-key :purchase/forWhom
            :subs-key :users
            :filter-fn :user/isConsumer
            :schema-label-key :user/name
            :schema-id-key :user/abbrev]]
          [re-com/gap :size std-gap]
{'fraction_non_alphanumeric': 0.14148941957243105, 'fraction_numerical': 0.005113420007615732, 'mean_word_length': 1.9051833122629582, 'pattern_counts': {'":': 2, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 2, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '3508575', 'n_tokens_mistral': 5829, 'n_tokens_neox': 5494, 'n_words': 1350}