Makes outgoing invoice look a little better
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
(defn content-card- [params & children]
|
(defn content-card- [params & children]
|
||||||
[:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))})
|
[:section (merge params {:class (hh/add-class " py-3 sm:py-5" (:class params))})
|
||||||
[:div {:class "max-w-screen-2xl"}
|
[:div {:class (:max-w params "max-w-screen-2xl")}
|
||||||
(into
|
(into
|
||||||
[:div {:class "relative overflow-hidden shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]
|
[:div {:class "relative overflow-hidden shadow-md dark:bg-gray-800 sm:rounded-lg border-2 border-gray-200 dark:border-gray-900 bg-white"}]
|
||||||
children)]])
|
children)]])
|
||||||
|
|||||||
@@ -6,10 +6,13 @@
|
|||||||
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated
|
[auto-ap.routes.utils :refer [wrap-client-redirect-unauthenticated
|
||||||
wrap-secure]]
|
wrap-secure]]
|
||||||
[auto-ap.ssr-routes :as ssr-routes]
|
[auto-ap.ssr-routes :as ssr-routes]
|
||||||
|
[auto-ap.ssr.common-handlers :refer [add-new-entity-handler]]
|
||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
[auto-ap.ssr.form-cursor :as fc]
|
[auto-ap.ssr.form-cursor :as fc]
|
||||||
[auto-ap.ssr.grid-page-helper :refer [wrap-trim-client-ids]]
|
[auto-ap.ssr.grid-page-helper :refer [wrap-trim-client-ids]]
|
||||||
|
[auto-ap.ssr.hx :as hx]
|
||||||
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
||||||
|
[auto-ap.ssr.svg :as svg]
|
||||||
[auto-ap.ssr.ui :refer [base-page]]
|
[auto-ap.ssr.ui :refer [base-page]]
|
||||||
[auto-ap.ssr.utils :refer [clj-date-schema modal-response money
|
[auto-ap.ssr.utils :refer [clj-date-schema modal-response money
|
||||||
percentage strip wrap-schema-decode]]
|
percentage strip wrap-schema-decode]]
|
||||||
@@ -27,7 +30,7 @@
|
|||||||
[:outgoing-invoice/tax percentage]
|
[:outgoing-invoice/tax percentage]
|
||||||
[:outgoing-invoice/to-address [:map
|
[:outgoing-invoice/to-address [:map
|
||||||
[:street1 :string]
|
[:street1 :string]
|
||||||
[:street2 {:optional true} [:maybe [ :string { :decode/string strip}]]]
|
[:street2 {:optional true} [:maybe [:string {:decode/string strip}]]]
|
||||||
[:city :string]
|
[:city :string]
|
||||||
[:state :string]
|
[:state :string]
|
||||||
[:zip :string]]]
|
[:zip :string]]]
|
||||||
@@ -38,136 +41,168 @@
|
|||||||
[:outgoing-invoice-line-item/unit-price money]
|
[:outgoing-invoice-line-item/unit-price money]
|
||||||
[:outgoing-invoice-line-item/quantity money]]]]]))
|
[:outgoing-invoice-line-item/quantity money]]]]]))
|
||||||
|
|
||||||
|
(defn line-item [z]
|
||||||
|
(com/data-grid-row
|
||||||
|
(hx/alpine-mount-then-appear {:x-data (hx/json {:show false})
|
||||||
|
:data-key "show"
|
||||||
|
:x-ref "p"})
|
||||||
|
(fc/with-field :db/id
|
||||||
|
(com/hidden {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)}))
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(fc/with-field :outgoing-invoice-line-item/description
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)
|
||||||
|
:class "w-full"
|
||||||
|
:placeholder "Catered sandwiches"})))
|
||||||
|
(com/data-grid-cell
|
||||||
|
{:class "w-8"}
|
||||||
|
(fc/with-field :outgoing-invoice-line-item/quantity
|
||||||
|
(com/money-input {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)
|
||||||
|
:class "w-24"
|
||||||
|
:placeholder "20"})))
|
||||||
|
(com/data-grid-cell
|
||||||
|
{}
|
||||||
|
(fc/with-field :outgoing-invoice-line-item/unit-price
|
||||||
|
(com/money-input {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)
|
||||||
|
:class "w-24"
|
||||||
|
:placeholder "23.50"})))
|
||||||
|
(com/data-grid-cell {:class "align-top"}
|
||||||
|
(com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x))))
|
||||||
|
|
||||||
(defn form* [{:keys [form-params form-errors]}]
|
(defn form* [{:keys [form-params form-errors]}]
|
||||||
|
|
||||||
(fc/start-form
|
(fc/start-form
|
||||||
form-params
|
form-params
|
||||||
form-errors
|
form-errors
|
||||||
[:form {:hx-post (bidi.bidi/path-for ssr-routes/only-routes
|
[:form.flex.gap-4 {:hx-post (bidi.bidi/path-for ssr-routes/only-routes
|
||||||
::route/new-submit)}
|
::route/new-submit)}
|
||||||
(fc/with-field :outgoing-invoice/invoice-number
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
|
||||||
:label "Invoice #"}
|
|
||||||
[:div.w-96
|
|
||||||
(com/text-input {:name (fc/field-name)
|
|
||||||
:autofocus true
|
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "10000"
|
|
||||||
:value (fc/field-value)})]))
|
|
||||||
(fc/with-field :outgoing-invoice/date
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
|
||||||
:label "Date"}
|
|
||||||
[:div.w-96
|
|
||||||
(com/date-input {:name (fc/field-name)
|
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "10000"
|
|
||||||
:value (-> (fc/field-value)
|
|
||||||
(atime/unparse-local atime/normal-date))})]))
|
|
||||||
[:div.flex.gap-4
|
|
||||||
(fc/with-field :outgoing-invoice/client
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
|
||||||
:label "From (client)"}
|
|
||||||
[:div.w-96
|
|
||||||
(com/typeahead {:name (fc/field-name)
|
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "Search..."
|
|
||||||
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
|
|
||||||
[:div.flex.flex-col.gap-2
|
|
||||||
(fc/with-field :outgoing-invoice/to
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
|
||||||
:label "To"}
|
|
||||||
[:div.w-96
|
|
||||||
(com/text-input {:name (fc/field-name)
|
|
||||||
:error? (fc/error?)
|
|
||||||
:class "w-96"
|
|
||||||
:placeholder "Hello Company"
|
|
||||||
:value (fc/field-value)})]))
|
|
||||||
|
|
||||||
(fc/with-field-default :outgoing-invoice/to-address {}
|
(com/content-card {:max-w "max-w-screen-lg"}
|
||||||
(list
|
[:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
|
||||||
(fc/with-field :address/street1
|
[:div
|
||||||
(com/validated-field {:label "Address"
|
[:h1.text-2xl.mb-3.font-bold "New outgoing invoice"]]
|
||||||
:errors (fc/field-errors)}
|
[:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}]]
|
||||||
(com/text-input {:name (fc/field-name)
|
[:div.p-4
|
||||||
:error? (fc/error?)
|
(fc/with-field :outgoing-invoice/client
|
||||||
:class "w-full"
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
:placeholder "1200 Pennsylvania Avenue"
|
:label "From (client)"}
|
||||||
:value (fc/field-value)})))
|
[:div.w-96
|
||||||
(fc/with-field :address/street2
|
(com/typeahead {:name (fc/field-name)
|
||||||
(com/validated-field {:errors (fc/field-errors)}
|
:error? (fc/error?)
|
||||||
(com/text-input {:name (fc/field-name)
|
:autofocus true
|
||||||
:error? (fc/error?)
|
:class "w-96"
|
||||||
:class "w-full"
|
:placeholder "Search..."
|
||||||
:placeholder "Suite 300"
|
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||||
:value (fc/field-value)})))
|
:value (fc/field-value)
|
||||||
[:div.flex.w-full.space-x-4
|
:content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})]))
|
||||||
(fc/with-field :address/city
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
(fc/with-field :outgoing-invoice/invoice-number
|
||||||
:class "w-full grow shrink"}
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
(com/text-input {:name (fc/field-name)
|
:label "Invoice #"}
|
||||||
:error? (fc/error?)
|
[:div.w-96
|
||||||
:class "w-full"
|
(com/text-input {:name (fc/field-name)
|
||||||
:placeholder "Cupertino"
|
:error? (fc/error?)
|
||||||
:value (fc/field-value)})))
|
:class "w-96"
|
||||||
(fc/with-field :address/state
|
:placeholder "10000"
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
:value (fc/field-value)})]))
|
||||||
:class "w-16 shrink-0"}
|
(fc/with-field :outgoing-invoice/date
|
||||||
(com/text-input {:name (fc/field-name)
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
:error? (fc/error?)
|
:label "Date"}
|
||||||
:class "w-full"
|
[:div.w-96
|
||||||
:placeholder "CA"
|
(com/date-input {:name (fc/field-name)
|
||||||
:value (fc/field-value)})))
|
:error? (fc/error?)
|
||||||
(fc/with-field :address/zip
|
:class "w-96"
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
:placeholder "10000"
|
||||||
:class "w-24 shrink-0"}
|
:value (-> (fc/field-value)
|
||||||
(com/text-input {:name (fc/field-name)
|
(atime/unparse-local atime/normal-date))})]))
|
||||||
:error? (fc/error?)
|
|
||||||
:placeholder "98101"
|
|
||||||
:class "w-full"
|
|
||||||
:value (fc/field-value)})))]))]]
|
|
||||||
|
|
||||||
|
|
||||||
(fc/with-field-default :outgoing-invoice/line-items [{} {} {}]
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
|
||||||
:label "Line items"}
|
|
||||||
(com/data-grid {:headers [(com/data-grid-header {} "Description")
|
|
||||||
(com/data-grid-header {} "Quantity")
|
|
||||||
(com/data-grid-header {} "Unit Price")]}
|
|
||||||
(fc/cursor-map
|
|
||||||
(fn [z]
|
|
||||||
(com/data-grid-row
|
|
||||||
{}
|
|
||||||
(com/data-grid-cell
|
|
||||||
{}
|
|
||||||
(fc/with-field :outgoing-invoice-line-item/description
|
|
||||||
(com/text-input {:name (fc/field-name)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:class "w-full"
|
|
||||||
:placeholder "Catered sandwiches"})))
|
|
||||||
(com/data-grid-cell
|
|
||||||
{}
|
|
||||||
(fc/with-field :outgoing-invoice-line-item/quantity
|
|
||||||
(com/money-input {:name (fc/field-name)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:placeholder "20"})))
|
|
||||||
(com/data-grid-cell
|
|
||||||
{}
|
|
||||||
(fc/with-field :outgoing-invoice-line-item/unit-price
|
|
||||||
(com/money-input {:name (fc/field-name)
|
|
||||||
:value (fc/field-value)
|
|
||||||
:placeholder "23.50"})))))))))
|
|
||||||
(fc/with-field-default :outgoing-invoice/tax 10.0
|
|
||||||
|
|
||||||
(com/validated-field {:errors (fc/field-errors)
|
(fc/with-field-default :outgoing-invoice/line-items [{:db/id "first"}]
|
||||||
:label "Tax %"}
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
(com/money-input {:name (fc/field-name)
|
:label "Line items"}
|
||||||
:value (fc/field-value)})))
|
(com/data-grid {:headers [(com/data-grid-header {} "Description")
|
||||||
|
(com/data-grid-header {} "Quantity")
|
||||||
|
(com/data-grid-header {} "Unit Price")
|
||||||
|
(com/data-grid-header {} "")]}
|
||||||
|
(fc/cursor-map line-item)
|
||||||
|
(com/data-grid-new-row {:colspan 4
|
||||||
|
:index (count (fc/field-value))
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||||
|
|
||||||
[:div.flex.flex-row-reverse (com/button {:color :primary :class "w-24"} "Generate")]]))
|
::route/new-line-item)} "Add line"))))
|
||||||
|
|
||||||
|
(fc/with-field-default :outgoing-invoice/tax 10.0
|
||||||
|
|
||||||
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
|
:label "Tax %"}
|
||||||
|
(com/money-input {:name (fc/field-name)
|
||||||
|
:value (fc/field-value)})))
|
||||||
|
|
||||||
|
[:div.flex.flex-row-reverse (com/button {:color :primary :class "w-24"} "Generate")]])
|
||||||
|
(com/content-card {:max-w "max-w-24"}
|
||||||
|
[:div.p-4
|
||||||
|
[:h3.text-lg "Recipient details"]
|
||||||
|
[:div.flex.flex-col.gap-2
|
||||||
|
(fc/with-field :outgoing-invoice/to
|
||||||
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
|
:label "To"}
|
||||||
|
[:div.w-96
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-96"
|
||||||
|
:placeholder "Hello Company"
|
||||||
|
:value (fc/field-value)})]))
|
||||||
|
|
||||||
|
(fc/with-field-default :outgoing-invoice/to-address {}
|
||||||
|
(list
|
||||||
|
(fc/with-field :address/street1
|
||||||
|
(com/validated-field {:label "Address"
|
||||||
|
:errors (fc/field-errors)}
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-full"
|
||||||
|
:placeholder "1200 Pennsylvania Avenue"
|
||||||
|
:value (fc/field-value)})))
|
||||||
|
(fc/with-field :address/street2
|
||||||
|
(com/validated-field {:errors (fc/field-errors)}
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-full"
|
||||||
|
:placeholder "Suite 300"
|
||||||
|
:value (fc/field-value)})))
|
||||||
|
[:div.flex.w-full.space-x-4
|
||||||
|
(fc/with-field :address/city
|
||||||
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
|
:class "w-full grow shrink"}
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-full"
|
||||||
|
:placeholder "Cupertino"
|
||||||
|
:value (fc/field-value)})))
|
||||||
|
(fc/with-field :address/state
|
||||||
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
|
:class "w-16 shrink-0"}
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:class "w-full"
|
||||||
|
:placeholder "CA"
|
||||||
|
:value (fc/field-value)})))
|
||||||
|
(fc/with-field :address/zip
|
||||||
|
(com/validated-field {:errors (fc/field-errors)
|
||||||
|
:class "w-24 shrink-0"}
|
||||||
|
(com/text-input {:name (fc/field-name)
|
||||||
|
:error? (fc/error?)
|
||||||
|
:placeholder "98101"
|
||||||
|
:class "w-full"
|
||||||
|
:value (fc/field-value)})))]))]])]))
|
||||||
|
|
||||||
|
(defn- fmt-money [total]
|
||||||
|
(format "$%,.2f" (or total 0)))
|
||||||
|
|
||||||
(defn submit [{:keys [form-params]}]
|
(defn submit [{:keys [form-params]}]
|
||||||
(let [line-items (->> form-params
|
(let [line-items (->> form-params
|
||||||
@@ -180,16 +215,26 @@
|
|||||||
subtotal (reduce + 0.0 (map :outgoing-invoice-line-item/total line-items))
|
subtotal (reduce + 0.0 (map :outgoing-invoice-line-item/total line-items))
|
||||||
tax (* subtotal (:outgoing-invoice/tax form-params))
|
tax (* subtotal (:outgoing-invoice/tax form-params))
|
||||||
total (+ subtotal tax)
|
total (+ subtotal tax)
|
||||||
|
final-outgoing-invoice (-> form-params
|
||||||
|
(assoc :outgoing-invoice/line-items
|
||||||
|
line-items
|
||||||
|
:outgoing-invoice/total total
|
||||||
|
:outgoing-invoice/tax tax))
|
||||||
result
|
result
|
||||||
|
|
||||||
(-> (lambda/invoke {:function-name "genpdf" :payload
|
(-> (lambda/invoke {:function-name "genpdf" :payload
|
||||||
(json/write-str
|
(json/write-str
|
||||||
(-> form-params
|
(-> final-outgoing-invoice
|
||||||
(assoc :outgoing-invoice/line-items
|
(update :outgoing-invoice/total fmt-money)
|
||||||
line-items
|
(update :outgoing-invoice/tax fmt-money)
|
||||||
:outgoing-invoice/total total
|
(update :outgoing-invoice/line-items
|
||||||
:outgoing-invoice/tax tax)
|
(fn [lis]
|
||||||
(update :outgoing-invoice/date
|
(mapv
|
||||||
|
#(-> %
|
||||||
|
(update :outgoing-invoice-line-item/total fmt-money)
|
||||||
|
(update :outgoing-invoice-line-item/unit-price fmt-money))
|
||||||
|
lis)))
|
||||||
|
(update :outgoing-invoice/date
|
||||||
#(some-> % (atime/unparse-local atime/normal-date)))))})
|
#(some-> % (atime/unparse-local atime/normal-date)))))})
|
||||||
:payload
|
:payload
|
||||||
slurp
|
slurp
|
||||||
@@ -219,13 +264,7 @@
|
|||||||
[:a {:href (bidi/path-for ssr-routes/only-routes
|
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||||
::route/new)}
|
::route/new)}
|
||||||
"New"]])
|
"New"]])
|
||||||
(com/content-card {}
|
(form* request))
|
||||||
[:div {:class "flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-center lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
|
|
||||||
[:div
|
|
||||||
[:h1.text-2xl.mb-3.font-bold "New outgoing invoice"]]
|
|
||||||
[:div {:class "flex flex-col flex-shrink-0 space-y-3 md:flex-row md:items-center lg:justify-end md:space-y-0 md:space-x-3"}]]
|
|
||||||
[:div.px-4
|
|
||||||
(form* request)]))
|
|
||||||
"New outgoing invoice"))
|
"New outgoing invoice"))
|
||||||
(wrap-trim-client-ids)
|
(wrap-trim-client-ids)
|
||||||
(wrap-secure)
|
(wrap-secure)
|
||||||
@@ -234,5 +273,13 @@
|
|||||||
(def route->handler
|
(def route->handler
|
||||||
{::route/new page
|
{::route/new page
|
||||||
::route/new-submit (-> submit
|
::route/new-submit (-> submit
|
||||||
(wrap-schema-decode :form-schema form-schema)
|
(wrap-schema-decode :form-schema form-schema)
|
||||||
(wrap-nested-form-params))})
|
(wrap-nested-form-params))
|
||||||
|
::route/new-line-item (->
|
||||||
|
|
||||||
|
(add-new-entity-handler [:outgoing-invoice/line-items]
|
||||||
|
(fn render [cursor request]
|
||||||
|
(line-item
|
||||||
|
{:value cursor }))
|
||||||
|
(fn build-new-row [base _]
|
||||||
|
base)))})
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
(ns auto-ap.routes.outgoing-invoice)
|
(ns auto-ap.routes.outgoing-invoice)
|
||||||
(def routes {"/" {"new" {:get ::new
|
(def routes {"/" {"new" {:get ::new
|
||||||
:post ::new-submit} }})
|
:post ::new-submit}
|
||||||
|
"line-item/new" {:get ::new-line-item}}})
|
||||||
Reference in New Issue
Block a user