adds csv capability

This commit is contained in:
2024-11-11 21:39:36 -08:00
parent aed9113306
commit cb4f88f02a
9 changed files with 136 additions and 65 deletions

View File

@@ -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)

View File

@@ -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

View File

@@ -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}

View File

@@ -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

View File

@@ -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))

View File

@@ -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)

View File

@@ -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")}

View File

@@ -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