(ns auto-ap.ssr.company (:require [amazonica.aws.s3 :as s3] [auto-ap.datomic :refer [conn pull-attr]] [auto-ap.datomic.clients :refer [full-read]] [auto-ap.graphql.utils :refer [assert-can-see-client]] [auto-ap.permissions :as permissions] [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [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]] [bidi.bidi :as bidi] [cemerick.url :as url] [clojure.java.io :as io] [clojure.string :as str] [config.core :refer [env]] [datomic.api :as dc] [ring.middleware.json :refer [wrap-json-response]]) (:import [java.util UUID] (org.apache.commons.codec.binary Base64))) (defn please-select-client-screen* [] [:div.grid.grid-cols-3 (com/content-card {} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} "Please select a company"]])]) (defn signature [request] (let [signature-file (pull-attr (dc/db conn) :client/signature-file (:db/id (:client request)))] (com/content-card {:class " w-[748px]" :hx-target "this" :hx-swap "outerHTML"} [:div.col-span-1.p-4 {:class "p-4 sm:p-6 space-y-4 overflow-visible " } [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} "Signature"] [:div#signature-notification.notification.block {:style {:display "none"}}] [:div {:x-data (hx/json {"signature" nil "editing" false "existing" (boolean signature-file)}) :hx-put (bidi/path-for ssr-routes/only-routes :company-update-signature) :hx-trigger "accepted" :hx-vals "js:{signatureData: event.detail.signatureData}"} [:div.htmx-indicator [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} (svg/spinner {:class "w-4 h-4 text-primary-300"}) [:div.ml-3 "Loading..."]]] [:div.htmx-indicator-hidden (when signature-file [:img.rounded.rounded-lg.border.border-gray-300.bg-gray-50 {:src signature-file :width 696 :height 261 :x-show "existing && !editing"}]) [:canvas.rounded.rounded-lg.border.border-gray-300 {:style {:width 696 :height 261} :x-init "signature= new SignaturePad($el); signature.off()" ":class" "editing ? 'bg-white' : 'bg-gray-50' " :width 696 :height 261 :x-show "existing ? editing: true"}]] [:div.flex.gap-2.justify-end (com/button {:color :primary :x-show "!editing" "@click" "signature.clear(); signature.on(); editing=true;"} "New signature") (com/button {:color :primary :x-show "editing" "@click" "signature.clear();"} "Clear") (com/button {:color :primary "@click" "$data.signatureData=signature.toDataURL('image/png'); signature.off(); editing=false; $dispatch('accepted', {signatureData: $data.signatureData}) " :x-show "editing"} "Accept")]] [:div [:div.flex.justify-center " - or -"] [:form {:hx-post (bidi/path-for ssr-routes/only-routes :company-upload-signature) :hx-disinherit "hx-vals" :hx-encoding "multipart/form-data" #_#_:hx-target "#signature-notification" :hx-swap "outerHTML" :id "upload" :hx-trigger "z" } [:div.htmx-indicator [:div.bg-gray-100.flex.items-center.text-green-500.justify-center.rounded.rounded-lg.border.border-gray-400 {:style {:width "696px" :height "261px"}} (svg/spinner {:class "w-4 h-4 text-primary-300"}) [:div.ml-3 "Loading..."]]] [:div.htmx-indicator-hidden [:div.border-2.border-dashed.rounded-lg.p-4.w-full.text-center.cursor-pointer.h-64.flex.items-center.justify-center.text-lg.relative {:x-data (hx/json {"files" nil "hovering" false}) :x-dispatch:z "files" ":class" "{'bg-blue-100': !hovering, 'border-blue-300': !hovering, 'text-blue-700': !hovering, 'bg-green-100': hovering, 'border-green-300': hovering, 'text-green-700': hovering }"} [:input {:type "file" :name "file" :class "absolute inset-0 m-0 p-0 w-full h-full outline-none opacity-0", :x-on:change "files = $event.target.files;", :x-on:dragover "hovering = true", :x-on:dragleave "hovering = false", :x-on:drop "hovering = false"}] [:div.flex.flex-col.space-2 [:div [:ul {:x-show "files != null"} [:template {:x-for "f in files"} [:li (com/pill {:color :primary :x-text "f.name"})]]]] [:div.htmx-indicator-hidden "Drop a signature file (696x261 pixels jpeg) here."]]]] ]]]))) (defn upload-signature-data [{{:strs [signatureData]} :form-params client :client :as request}] (let [prefix "data:image/png;base64,"] (when signatureData (when-not (str/starts-with? signatureData prefix) (throw (ex-info "Invalid signature image" {:validation-error (str "Invalid signature image.")}))) (let [signature-id (str (UUID/randomUUID)) raw-bytes (Base64/decodeBase64 (subs signatureData (count prefix)))] (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) :key (str signature-id ".png") :input-stream (io/make-input-stream raw-bytes {}) :metadata {:content-type "image/png" :content-length (count raw-bytes)} :canned-acl "public-read") @(dc/transact conn [{:db/id (:db/id client) :client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".png")}]) (html-response (signature request)))))) (defn upload-signature-file [{{:strs [signatureData]} :form-params client :client user :identity :as request}] (assert-can-see-client user client) (let [{:strs [file]} (:multipart-params request) ] (try (let [signature-id (str (UUID/randomUUID)) ] (s3/put-object :bucket-name "integreat-signature-images" #_(:data-bucket env) :key (str signature-id ".jpg") :input-stream (io/input-stream (:tempfile file)) :metadata {:content-type "image/jpeg" :content-length (:length (:tempfile file))} :canned-acl "public-read") @(dc/transact conn [{:db/id (:db/id client) :client/signature-file (str "https://integreat-signature-images.s3.amazonaws.com/" signature-id ".jpg")}]) (html-response (signature request))) (catch Exception e (println e) #_(-> result (assoc :error? true) (update :results conj {:filename filename :response (.getMessage e) :sample (:sample (ex-data e)) :template (:template (ex-data e))})))) #_(html-response [:div#page-notification.p-4.rounded-lg {:class (if (:error? results) "bg-red-50 text-red-700" "bg-primary-50 text-primary-700")} [:table [:thead [:tr [:td "File"] [:td "Result"] [:td "Template"] (if (:error? results) [:td "Sample match"])] #_[:tr "Result"] #_[:tr "Template"]] (for [r (:results results)] [:tr [:td.p-2.border {:class (if (:error? results) "bg-red-50 text-red-700 border-red-300" "bg-primary-50 text-primary-700 border-green-500")} (:filename r)] [:td.p-2.border {:class (if (:error? results) "bg-red-50 text-red-700 border-red-300" "bg-primary-50 text-primary-700 border-green-500")} (:response r)] [:td.p-2.border {:class (if (:error? results) "bg-red-50 text-red-700 border-red-300" "bg-primary-50 text-primary-700 border-green-500")} "Template: " (:template r)] (if (:error? results) [:td.p-2.border {:class "bg-red-50 text-red-700 border-red-300"} [:ul (for [[k v] (dissoc (:sample r) :template :source-url :full-text :text)] [:li (name k) ": " (str v)])] #_(:template r)])])]] :headers {"hx-trigger" "invalidated"}))) (defn main-content* [{:keys [client identity] :as request}] (if-not client (please-select-client-screen*) (let [client (dc/pull (dc/db conn) full-read (:db/id client))] [:div [:div.grid.grid-cols-3.gap-4 (com/content-card {} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} (:client/name client)] (when-let [address (-> client :client/address)] [:div.flex.flex-col.gap-1.text-lg.dark:text-white.text-gray-700 [:p (-> address :address/street1)] [:p (-> address :address/street2)] [:p (-> address :address/city) " " (-> address :address/state) ", " (-> address :address/zip)]])]) (com/content-card {} [:div.col-span-1.p-4 {:class "p-4 sm:p-6"} [:h3 {:class "mb-4 text-xl font-semibold dark:text-white"} "Downloads"] [:a {:href (str (assoc (url/url (str (:base-url env) "/api/vendors/company/export")) :query {"client" (:client/code client)}))} (com/button {:color :primary} "Download vendor list" (com/button-icon {} svg/download))]]) [:div]] (when (permissions/can? identity {:client client :subject :signature :activity :edit}) (signature request))]))) (defn page [{:keys [identity matched-route] :as request}] (base-page request (com/page {:nav com/company-aside-nav :client-selection (:client-selection request) :request request :client (:client request) :clients (:clients request) :identity (:identity request) :app-params {:hx-get (bidi/path-for ssr-routes/only-routes :company) :hx-trigger "clientSelected from:body" :hx-select "#app-contents" :hx-swap "outerHTML swap:300ms"}} (com/breadcrumbs {} [:a {:href (bidi/path-for ssr-routes/only-routes :company)} "My Company"]) (main-content* request)) "My Company")) (defn search [{:keys [clients query-params]}] (let [valid-client-ids (set (map :db/id clients)) name-like-ids (when (not-empty (get query-params "q")) (set (map (comp #(Long/parseLong %) :id) (solr/query solr/impl "clients" {"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q")))) "fields" "id" "limit" 300})))) valid-clients (for [n name-like-ids :when (valid-client-ids n)] {"value" n "label" (pull-attr (dc/db conn) :client/name n)})] {:body (take 10 valid-clients)})) (def search (wrap-json-response search)) (defn bank-account-search [{:keys [route-params query-params clients]}] (let [valid-client-ids (set (map :db/id clients)) selected-client-id (Long/parseLong (get route-params :db/id)) bank-accounts (when (valid-client-ids selected-client-id) (->> (dc/pull (dc/db conn) [{:client/bank-accounts [:db/id :bank-account/name]}] selected-client-id) :client/bank-accounts (filter (fn [{:keys [bank-account/name]}] (str/includes? (or (some-> name str/upper-case) "") (or (some-> query-params (get "q") str/upper-case) "__")))) (map (fn [{:keys [db/id bank-account/name]}] {"value" id "label" name}))))] {:body (take 10 bank-accounts)})) (def bank-account-search (wrap-json-response bank-account-search)) (defn bank-account-typeahead* [{:keys [client-id name value]}] (if client-id (com/typeahead {:name name :class "w-96" :placeholder "Search..." :url (bidi/path-for ssr-routes/only-routes :bank-account-search :db/id client-id) :value value :value-fn (some-fn :db/id identity) :content-fn (some-fn :bank-account/name #(pull-attr (dc/db conn) :bank-account/name %))}) [:span.text-xs.text-gray-500 "Please select a client before selecting a bank account." [:input {:type "hidden" :name name}]])) (defn bank-account-typeahead [{:keys [query-params clients]}] (html-response (bank-account-typeahead* {:client-id ((set (map :db/id clients)) (some->> "client-id" (get query-params) not-empty Long/parseLong)) :name (get query-params "name")})))