adds csv capability
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -112,6 +112,7 @@
|
||||
true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center")
|
||||
(= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700")
|
||||
(= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ")
|
||||
(= :secondary-light (:color params)) (str " text-blue-800 bg-white-200 border-gray-100 border hover:bg-blue-100 focus:ring-blue-100 dark:bg-blue-400 dark:hover:bg-blue-800 ")
|
||||
(nil? (:color params))
|
||||
(str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700")))
|
||||
(assoc :tabindex 0)
|
||||
|
||||
@@ -88,7 +88,7 @@
|
||||
:request-method :get)
|
||||
:hx-trigger "clientSelected from:body, invalidated from:body"
|
||||
:hx-swap "outerHTML swap:300ms"))
|
||||
[:div {:class " group-[.raw]:hidden 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 {:class " group-[.raw]:hidden flex flex-col px-4 py-3 space-y-3 lg:flex-row lg:items-baseline lg:justify-between lg:space-y-0 lg:space-x-4 text-gray-800 dark:text-gray-100"}
|
||||
[:h1.text-2xl.mb-3.font-bold title]
|
||||
[:div {:class "flex items-center flex-1 space-x-4"}
|
||||
[:h5
|
||||
|
||||
@@ -1,25 +1,27 @@
|
||||
(ns auto-ap.ssr.grid-page-helper
|
||||
(:require [auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :as query-params]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [html-response main-transformer]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clojure.string :as str]
|
||||
[hiccup2.core :as hiccup]
|
||||
[malli.core :as m]
|
||||
[malli.transform :as mt2]
|
||||
[malli.transform :as mt]
|
||||
[taoensso.encore :refer [filter-vals]]))
|
||||
(ns auto-ap.ssr.grid-page-helper
|
||||
(:require
|
||||
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||
[auto-ap.logging :as alog]
|
||||
[auto-ap.query-params :as query-params]
|
||||
[auto-ap.routes.utils
|
||||
:refer [wrap-client-redirect-unauthenticated wrap-secure]]
|
||||
[auto-ap.ssr-routes :as ssr-routes]
|
||||
[auto-ap.ssr.components :as com]
|
||||
[auto-ap.ssr.hiccup-helper :as hh]
|
||||
[auto-ap.ssr.hx :as hx]
|
||||
[auto-ap.ssr.svg :as svg]
|
||||
[auto-ap.ssr.ui :refer [base-page]]
|
||||
[auto-ap.ssr.utils :refer [html-response main-transformer]]
|
||||
[auto-ap.time :as atime]
|
||||
[bidi.bidi :as bidi]
|
||||
[cemerick.url :as url]
|
||||
[clojure.string :as str]
|
||||
[hiccup.util :as hu]
|
||||
[malli.core :as m]
|
||||
[malli.transform :as mt2]
|
||||
[taoensso.encore :refer [filter-vals]]
|
||||
[clojure.java.io :as io]
|
||||
[clojure.data.csv :as csv]))
|
||||
|
||||
|
||||
|
||||
@@ -40,6 +42,8 @@
|
||||
[])
|
||||
cells (->> gridspec
|
||||
:headers
|
||||
(filter (fn [h]
|
||||
((:render-for h #{:html :csv}) :html)))
|
||||
(filter (fn [h]
|
||||
(if (and (:hide? h)
|
||||
((:hide? h) request))
|
||||
@@ -96,6 +100,12 @@
|
||||
(:check-boxes? grid-spec) inc)}
|
||||
[:span.font-bold.text-gray-600.text-lg break-table-value])))))))
|
||||
|
||||
(defn sort->query [s]
|
||||
(str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k))
|
||||
"asc"
|
||||
"desc")))
|
||||
s)))
|
||||
|
||||
(defn table* [grid-spec user {{:keys [start per-page flash-id sort]} :parsed-query-params :as request}]
|
||||
(alog/info ::TABLE-QP
|
||||
:qp (:query-params request)
|
||||
@@ -109,9 +119,19 @@
|
||||
|
||||
(com/data-grid-card {:id (:id grid-spec)
|
||||
:raw? (:raw? grid-spec)
|
||||
:title (if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request))
|
||||
:title [:div.flex.gap-2 (if (string? (:title grid-spec))
|
||||
(:title grid-spec)
|
||||
((:title grid-spec) request))
|
||||
(when (:csv-route grid-spec)
|
||||
(com/a-button {:href (hu/url (bidi/path-for ssr-routes/only-routes (:csv-route grid-spec))
|
||||
(dissoc (update (filter-vals #(not (nil? %))
|
||||
(m/encode (:query-schema grid-spec)
|
||||
(:query-params request)
|
||||
main-transformer))
|
||||
"sort" sort->query)
|
||||
"selected" "all-selected"))
|
||||
:color :secondary-light}
|
||||
[:div.w-4.h-4 svg/download]))]
|
||||
:route (:route grid-spec)
|
||||
:start start
|
||||
:per-page per-page
|
||||
@@ -149,6 +169,8 @@
|
||||
(conj
|
||||
(->> grid-spec
|
||||
:headers
|
||||
(filter (fn [h]
|
||||
((:render-for h #{:html :csv}) :html)))
|
||||
(map
|
||||
(fn [h]
|
||||
(cond
|
||||
@@ -179,11 +201,7 @@
|
||||
(com/data-grid-header {}))})))
|
||||
|
||||
|
||||
(defn sort->query [s]
|
||||
(str/join "," (map (fn [k] (format "%s:%s" (:sort-key k) (if (= true (:asc k))
|
||||
"asc"
|
||||
"desc")))
|
||||
s)))
|
||||
|
||||
|
||||
(defn default-unparse-query-params [query-params]
|
||||
(reduce
|
||||
@@ -267,8 +285,30 @@
|
||||
true (wrap-secure)
|
||||
true (wrap-client-redirect-unauthenticated)))
|
||||
|
||||
(defn page-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
|
||||
(defn csv-route [{:keys [fetch-page headers page->csv-entities]} & {:keys [parse-query-params?] :or {parse-query-params? true}}]
|
||||
(cond-> (fn csv-route [{:keys [identity] :as request}]
|
||||
|
||||
(let [page-results (fetch-page (assoc-in request [:query-params :per-page] Long/MAX_VALUE))
|
||||
csv-entities ((or page->csv-entities (fn [[entities]] entities)) page-results)
|
||||
csv-content (with-open [i (java.io.StringWriter.)]
|
||||
(csv/write-csv i
|
||||
(into [(for [h headers
|
||||
:when ((:render-for h #{:html :csv}) :csv)]
|
||||
(:name h))]
|
||||
(for [e csv-entities ]
|
||||
(for [h headers
|
||||
:when ((:render-for h #{:html :csv})
|
||||
:csv)]
|
||||
((or (:render-csv h) (comp str (:render h))) e)))))
|
||||
(.toString i))]
|
||||
|
||||
{:headers {"Content-Type" "text/csv"}
|
||||
:body csv-content}))
|
||||
true (wrap-trim-client-ids)
|
||||
true (wrap-secure)
|
||||
true (wrap-client-redirect-unauthenticated)))
|
||||
|
||||
(defn page-route [grid-spec & {:keys [parse-query-params?] :or {parse-query-params? true}}]
|
||||
(cond-> (fn page [{:keys [identity] :as request}]
|
||||
(alog/info ::page-route
|
||||
:pqp (:parsed-query-params request)
|
||||
@@ -312,7 +352,7 @@
|
||||
[:name :string]
|
||||
[:header-class {:optional true} [:maybe :string]]
|
||||
[:sort-key {:optional true} :string]
|
||||
[:render [:=> [:cat entity-spec] :any]]
|
||||
[:render {:optional true} [:=> [:cat entity-spec] :any]]
|
||||
[:hide? {:optional true} [:=> [:cat entity-spec] :boolean]]]))
|
||||
(def grid-spec (m/schema [:map
|
||||
[:id :string]
|
||||
@@ -356,6 +396,7 @@
|
||||
:string]]]
|
||||
[:entity-name :string]
|
||||
[:route :keyword]
|
||||
[:csv-route {:optional true} [:maybe :keyword]]
|
||||
[:action-buttons
|
||||
{:default (fn [request])
|
||||
:optional true}
|
||||
|
||||
@@ -74,10 +74,6 @@
|
||||
|
||||
|
||||
|
||||
;; TODO test as a real user
|
||||
|
||||
|
||||
|
||||
|
||||
(defn delete [{invoice :entity :as request identity :identity}]
|
||||
(exception->notification
|
||||
@@ -702,6 +698,7 @@
|
||||
(wrap-implied-route-param :external? true))
|
||||
|
||||
::route/table (helper/table-route grid-page :parse-query-params? false)
|
||||
::route/csv (helper/csv-route grid-page :parse-query-params? false)
|
||||
::route/external-import-page external-import-page
|
||||
::route/bank-account-filter bank-account-filter
|
||||
::route/external-import-parse (-> external-import-parse
|
||||
|
||||
@@ -140,6 +140,11 @@
|
||||
:size :small})])
|
||||
(exact-match-id* request)]])
|
||||
|
||||
;; TODO
|
||||
;; 1. Sorting in investigate dialog
|
||||
;; 2. actual date range filtering in investigate dialog
|
||||
;; 3. CSVs
|
||||
;; 4. better date range / advanced mode for dialog
|
||||
|
||||
(defn fetch-ids [db {:keys [query-params route-params] :as request}]
|
||||
(let [valid-clients (extract-client-ids (:clients request)
|
||||
@@ -438,9 +443,9 @@ args
|
||||
(mc/decode query-schema p main-transformer))
|
||||
:action-buttons (fn [request]
|
||||
[(com/button {:color :primary
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
:hx-get (bidi/path-for ssr-routes/only-routes
|
||||
::route/new)}
|
||||
"Add journal entry")])
|
||||
"Add journal entry")])
|
||||
:row-buttons (fn [request entity]
|
||||
[(when (and (= :invoice-status/unpaid (:invoice/status entity))
|
||||
(can? (:identity request) {:subject :invoice :activity :delete}))
|
||||
@@ -470,60 +475,86 @@ args
|
||||
"Register"))
|
||||
:entity-name "register"
|
||||
:route ::route/table
|
||||
:csv-route ::route/csv
|
||||
:break-table (fn [request entity]
|
||||
(cond
|
||||
(cond
|
||||
(= (-> request :query-params :sort first :name) "Vendor")
|
||||
(or (-> entity :journal-entry/vendor :vendor/name)
|
||||
"No vendor")
|
||||
|
||||
|
||||
(= (-> request :query-params :sort first :name) "Source")
|
||||
(or (-> entity :journal-entry/source)
|
||||
"No external source")
|
||||
|
||||
|
||||
:else nil))
|
||||
:headers [{:key "client"
|
||||
:page->csv-entities (fn [[journal-entries]]
|
||||
(for [je journal-entries
|
||||
jel (:journal-entry/line-items je)]
|
||||
(merge jel je)))
|
||||
:headers [{:key "id"
|
||||
:name "Id"
|
||||
:render-csv :db/id
|
||||
:render-for #{:csv}}
|
||||
{:key "client"
|
||||
:name "Client"
|
||||
:sort-key "client"
|
||||
:hide? (fn [args]
|
||||
(and (= (count (:clients args)) 1)
|
||||
(= 1 (count (:client/locations (:client args))))))
|
||||
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :journal-entry/client :client/name) ])}
|
||||
:render (fn [x] [:div.flex.items-center.gap-2 (-> x :journal-entry/client :client/name)])
|
||||
:render-csv (fn [x] (-> x :journal-entry/client :client/name))}
|
||||
|
||||
{:key "vendor"
|
||||
:name "Vendor"
|
||||
:sort-key "vendor"
|
||||
:render (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||
[:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)]))}
|
||||
[:span.italic.text-gray-400 (-> e :journal-entry/alternate-description)]))
|
||||
:render-csv (fn [e] (or (-> e :journal-entry/vendor :vendor/name)
|
||||
(-> e :journal-entry/alternate-description)))}
|
||||
{:key "source"
|
||||
:name "Source"
|
||||
:sort-key "source"
|
||||
:hide? (fn [args]
|
||||
(not (:external? (:route-params args))))
|
||||
:render :journal-entry/source}
|
||||
{:key "external-id"
|
||||
:render :journal-entry/source
|
||||
:render-csv :journal-entry/source}
|
||||
{:key "external-id"
|
||||
:name "External Id"
|
||||
:sort-key "external-id"
|
||||
:class "max-w-[12rem]"
|
||||
:hide? (fn [args]
|
||||
(not (:external? (:route-params args))))
|
||||
:render (fn [x] [:p.truncate ( :journal-entry/external-id x)])}
|
||||
:render (fn [x] [:p.truncate (:journal-entry/external-id x)])
|
||||
:render-csv :journal-entry/external-id}
|
||||
{:key "date"
|
||||
:sort-key "date"
|
||||
:name "Date"
|
||||
:show-starting "lg"
|
||||
:render (fn [{:journal-entry/keys [date]}]
|
||||
(some-> date (atime/unparse-local atime/normal-date)))}
|
||||
|
||||
{:key "account"
|
||||
:name "Account"
|
||||
:sort-key "account"
|
||||
:class "text-right"
|
||||
:render-csv #(or (-> % :journal-entry-line/account :account/name)
|
||||
(-> % :journal-entry-line/account :bank-account/name))
|
||||
:render-for #{:csv}}
|
||||
{:key "debit"
|
||||
:name "Debit"
|
||||
:sort-key "debit"
|
||||
:class "text-right"
|
||||
:render (partial render-lines :journal-entry-line/debit)}
|
||||
:render (partial render-lines :journal-entry-line/debit)
|
||||
:render-csv :journal-entry-line/debit }
|
||||
|
||||
|
||||
|
||||
{:key "credit"
|
||||
:name "Credit"
|
||||
:sort-key "credit"
|
||||
:class "text-right"
|
||||
:render (partial render-lines :journal-entry-line/credit)}
|
||||
:render (partial render-lines :journal-entry-line/credit)
|
||||
:render-csv :journal-entry-line/credit }
|
||||
|
||||
{:key "links"
|
||||
:name "Links"
|
||||
:show-starting "lg"
|
||||
@@ -542,13 +573,14 @@ args
|
||||
{:link (-> i :journal-entry/original-entity :invoice/source-url)
|
||||
:color :secondary
|
||||
:content (str "File")}
|
||||
|
||||
|
||||
(-> i :journal-entry/original-entity :transaction/description-original)
|
||||
(conj
|
||||
{:link (hu/url (bidi/path-for client-routes/routes
|
||||
:transactions)
|
||||
{:exact-match-id (:db/id (:journal-entry/original-entity i))})
|
||||
:color :primary
|
||||
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))}))))}]}))
|
||||
:content (format "Transaction '%s'" (-> i :journal-entry/original-entity :transaction/description-original))}))))
|
||||
:render-for #{:html}}]}))
|
||||
|
||||
(def row* (partial helper/row* grid-page))
|
||||
@@ -70,6 +70,7 @@
|
||||
;; 2. Don't throw crazy errors when missing a field
|
||||
;; 3. General cleanup of the patterns in run-balance-sheet
|
||||
;; 4. Review ledger dialog
|
||||
;; 5. pagination and filtering within dialog. looks weird with the full screen refresh
|
||||
|
||||
(defn get-report [{ {:keys [periods client] :as qp} :form-params :as request}]
|
||||
(when (and (seq periods) client)
|
||||
|
||||
@@ -26,17 +26,15 @@
|
||||
:else
|
||||
(str (:value c)))
|
||||
cell-contents (if (:filters c)
|
||||
(do
|
||||
(println (:filters c))
|
||||
(com/link {:hx-get (hu/url investigate-url
|
||||
(cond-> {}
|
||||
(:numeric-code (:filters c)) (assoc :numeric-code (into [] (:numeric-code (:filters c))))
|
||||
(inst? (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:date-range (:filters c))) atime/normal-date))
|
||||
(:end (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:end (:date-range (:filters c)))) atime/normal-date))
|
||||
(:start (:date-range (:filters c))) (assoc :start-date (atime/unparse-local (coerce/to-date-time (:start (:date-range (:filters c)))) atime/normal-date))
|
||||
(:client-id (:filters c)) (assoc :client-id (:client-id (:filters c))))
|
||||
)}
|
||||
cell-contents))
|
||||
(com/link {:hx-get (hu/url investigate-url
|
||||
(cond-> {}
|
||||
(:numeric-code (:filters c)) (assoc :numeric-code (into [] (:numeric-code (:filters c))))
|
||||
(inst? (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:date-range (:filters c))) atime/normal-date))
|
||||
(:end (:date-range (:filters c))) (assoc :end-date (atime/unparse-local (coerce/to-date-time (:end (:date-range (:filters c)))) atime/normal-date))
|
||||
(:start (:date-range (:filters c))) (assoc :start-date (atime/unparse-local (coerce/to-date-time (:start (:date-range (:filters c)))) atime/normal-date))
|
||||
(:client-id (:filters c)) (assoc :client-id (:client-id (:filters c))))
|
||||
)}
|
||||
cell-contents)
|
||||
cell-contents)]
|
||||
[:td.px-4.py-2
|
||||
(cond-> {:style (cond-> {:width (str width "em")}
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
"/investigate" {"" ::investigate
|
||||
"/results" ::investigate-results}
|
||||
"/table" ::table
|
||||
"/csv" ::csv
|
||||
"/bank-account-filter" ::bank-account-filter
|
||||
"/reports/balance-sheet" {"" ::balance-sheet
|
||||
"/run" ::run-balance-sheet
|
||||
|
||||
Reference in New Issue
Block a user