Makes the entire form work but it just looks janky
This commit is contained in:
@@ -13,6 +13,31 @@
|
|||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.htmx-settling .fade-in-settle {
|
||||||
|
opacity: 0.0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-settling.fade-in-settle {
|
||||||
|
opacity: 0.0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-settle {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.htmx-settling .slide-up-settle {
|
||||||
|
@apply translate-y-5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-settling.slide-up-settle {
|
||||||
|
@apply translate-y-5 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-settle {
|
||||||
|
@apply translate-y-0;
|
||||||
|
}
|
||||||
|
|
||||||
.htmx-added .slide-up {
|
.htmx-added .slide-up {
|
||||||
@apply translate-y-5 !important;
|
@apply translate-y-5 !important;
|
||||||
}
|
}
|
||||||
@@ -87,6 +112,20 @@
|
|||||||
@apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:bg-gray-700 p-1 !important;
|
@apply bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 dark:bg-gray-700 p-1 !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.choices:focus-within .choices__inner {
|
||||||
|
@apply ring-blue-500 border-blue-500 dark:ring-blue-500 dark:border-blue-500 !important;
|
||||||
|
outline: 2px solid transparent !important;
|
||||||
|
outline-offset: 2px;
|
||||||
|
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: #007dbb;
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
border-color: #007dbb;
|
||||||
|
}
|
||||||
|
|
||||||
.choices__inner .choices__input {
|
.choices__inner .choices__input {
|
||||||
@apply m-0 bg-gray-50 dark:bg-gray-700 dark:text-white !important;
|
@apply m-0 bg-gray-50 dark:bg-gray-700 dark:text-white !important;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,108 @@ htmx.defineExtension('disable-submit', {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
htmx.defineExtension('rename-params', {
|
||||||
|
onEvent: function(name , evt) {
|
||||||
|
if (name === "htmx:configRequest") {
|
||||||
|
var normal = evt.detail.elt.getAttribute("hx-rename-params");
|
||||||
|
if (normal) {
|
||||||
|
normal = JSON.parse(normal);
|
||||||
|
for (const [key, value] of Object.entries(normal)) {
|
||||||
|
console.log(evt.detail.parameters)
|
||||||
|
evt.detail.parameters[value] = evt.detail.parameters[key];
|
||||||
|
delete evt.detail.parameters[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var exclusive = evt.detail.elt.getAttribute("hx-rename-params-ex");
|
||||||
|
if (exclusive) {
|
||||||
|
exclusive = JSON.parse(exclusive);
|
||||||
|
var params = {};
|
||||||
|
for (const [key, value] of Object.entries(exclusive)) {
|
||||||
|
var v = evt.detail.parameters[key]
|
||||||
|
if (v) {
|
||||||
|
params[value] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evt.detail.parameters = params;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
htmx.defineExtension('reactive-trigger', {
|
||||||
|
|
||||||
|
onEvent: function(name , evt) {
|
||||||
|
if (name === "htmx:beforeProcessNode") {
|
||||||
|
var element = evt.detail.elt;
|
||||||
|
var reactiveTrigger = element.getAttribute("hx-reactive-trigger");
|
||||||
|
if (reactiveTrigger) {
|
||||||
|
reactiveTrigger = JSON.parse(reactiveTrigger);
|
||||||
|
var reactiveSource = element.getAttribute("hx-reactive-source");
|
||||||
|
reactiveSource = reactiveSource ? document.querySelector(reactiveSource) : element.closest("form");
|
||||||
|
|
||||||
|
// var triggerString = Object.keys(reactiveTrigger).map(k => "change from:#" + reactiveSource.getAttribute("id") + " [name=\"" + k + "\"]").join(", ");
|
||||||
|
var triggerString = reactiveTrigger.map(k => "change from:" + "[name=\"" + k + "\"]").join(", ");
|
||||||
|
element.setAttribute("hx-trigger", triggerString);
|
||||||
|
}
|
||||||
|
} else if (name=="htmx:configRequest") {
|
||||||
|
var element = evt.detail.elt;
|
||||||
|
var reactiveTrigger = element.getAttribute("hx-reactive-trigger");
|
||||||
|
if (reactiveTrigger) {
|
||||||
|
reactiveTrigger = JSON.parse(reactiveTrigger);
|
||||||
|
var reactiveSource = element.getAttribute("hx-reactive-source");
|
||||||
|
reactiveSource = reactiveSource ? document.querySelector(reactiveSource) : element.closest("form");
|
||||||
|
for (key of reactiveTrigger) {
|
||||||
|
console.log( reactiveSource.querySelector(`[name="${key}"]`).value)
|
||||||
|
evt.detail.parameters[key] = reactiveSource.querySelector(`[name="${key}"]`).value || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function deepEqual(obj1, obj2) {
|
||||||
|
if (obj1 === obj2) {
|
||||||
|
return true; // Check for equality
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof obj1 !== 'object' || typeof obj2 !== 'object' || obj1 === null || obj2 === null) {
|
||||||
|
return false; // Check if both are objects
|
||||||
|
}
|
||||||
|
|
||||||
|
const keys1 = Object.keys(obj1);
|
||||||
|
const keys2 = Object.keys(obj2);
|
||||||
|
|
||||||
|
if (keys1.length !== keys2.length) {
|
||||||
|
return false; // Check if they have the same number of properties
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const key of keys1) {
|
||||||
|
if (!keys2.includes(key) || !deepEqual(obj1[key], obj2[key])) {
|
||||||
|
return false; // Recursively compare properties
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true; // Objects are deeply equal
|
||||||
|
}
|
||||||
|
|
||||||
|
htmx.defineExtension('trigger-filter', {
|
||||||
|
|
||||||
|
onEvent: function(name , evt) {
|
||||||
|
if (name=="htmx:beforeRequest") {
|
||||||
|
var element = evt.detail.elt;
|
||||||
|
console.log("HEREEE", element.lastParams, evt.detail.requestConfig.parameters)
|
||||||
|
if (!deepEqual(element.lastParams, evt.detail.requestConfig.parameters)) {
|
||||||
|
element.lastParams = evt.detail.requestConfig.parameters;
|
||||||
|
} else {
|
||||||
|
console.log("unchanged")
|
||||||
|
evt.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
initDatepicker = function(elem) {
|
initDatepicker = function(elem) {
|
||||||
elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true});
|
elem.dp = new Datepicker(elem, {format: "mm/dd/yyyy", autohide: true});
|
||||||
}
|
}
|
||||||
@@ -1208,6 +1208,10 @@ input:checked + .toggle-bg {
|
|||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mr-1 {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
.mr-10 {
|
.mr-10 {
|
||||||
margin-right: 2.5rem;
|
margin-right: 2.5rem;
|
||||||
}
|
}
|
||||||
@@ -1248,10 +1252,6 @@ input:checked + .toggle-bg {
|
|||||||
margin-top: 1.25rem;
|
margin-top: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mr-1 {
|
|
||||||
margin-right: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.block {
|
.block {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
@@ -1352,6 +1352,10 @@ input:checked + .toggle-bg {
|
|||||||
width: 4rem;
|
width: 4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-24 {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
|
||||||
.w-3 {
|
.w-3 {
|
||||||
width: 0.75rem;
|
width: 0.75rem;
|
||||||
}
|
}
|
||||||
@@ -1392,18 +1396,22 @@ input:checked + .toggle-bg {
|
|||||||
width: 2rem;
|
width: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.w-full {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.w-96 {
|
.w-96 {
|
||||||
width: 24rem;
|
width: 24rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.w-full {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.max-w-2xl {
|
.max-w-2xl {
|
||||||
max-width: 42rem;
|
max-width: 42rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.max-w-4xl {
|
||||||
|
max-width: 56rem;
|
||||||
|
}
|
||||||
|
|
||||||
.max-w-lg {
|
.max-w-lg {
|
||||||
max-width: 32rem;
|
max-width: 32rem;
|
||||||
}
|
}
|
||||||
@@ -1420,10 +1428,6 @@ input:checked + .toggle-bg {
|
|||||||
max-width: 1024px;
|
max-width: 1024px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.max-w-4xl {
|
|
||||||
max-width: 56rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.flex-1 {
|
.flex-1 {
|
||||||
flex: 1 1 0%;
|
flex: 1 1 0%;
|
||||||
}
|
}
|
||||||
@@ -1440,14 +1444,6 @@ input:checked + .toggle-bg {
|
|||||||
flex-shrink: 1;
|
flex-shrink: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.shrink-0 {
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.grow-0 {
|
|
||||||
flex-grow: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.basis-1\/4 {
|
.basis-1\/4 {
|
||||||
flex-basis: 25%;
|
flex-basis: 25%;
|
||||||
}
|
}
|
||||||
@@ -1546,10 +1542,6 @@ input:checked + .toggle-bg {
|
|||||||
appearance: none;
|
appearance: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.grid-cols-2 {
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
}
|
|
||||||
|
|
||||||
.grid-cols-3 {
|
.grid-cols-3 {
|
||||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||||
}
|
}
|
||||||
@@ -1638,10 +1630,6 @@ input:checked + .toggle-bg {
|
|||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.gap-3 {
|
|
||||||
gap: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.gap-x-4 {
|
.gap-x-4 {
|
||||||
-moz-column-gap: 1rem;
|
-moz-column-gap: 1rem;
|
||||||
column-gap: 1rem;
|
column-gap: 1rem;
|
||||||
@@ -1707,12 +1695,6 @@ input:checked + .toggle-bg {
|
|||||||
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
margin-bottom: calc(1.5rem * var(--tw-space-y-reverse));
|
||||||
}
|
}
|
||||||
|
|
||||||
.space-x-3 > :not([hidden]) ~ :not([hidden]) {
|
|
||||||
--tw-space-x-reverse: 0;
|
|
||||||
margin-right: calc(0.75rem * var(--tw-space-x-reverse));
|
|
||||||
margin-left: calc(0.75rem * calc(1 - var(--tw-space-x-reverse)));
|
|
||||||
}
|
|
||||||
|
|
||||||
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
.divide-y > :not([hidden]) ~ :not([hidden]) {
|
||||||
--tw-divide-y-reverse: 0;
|
--tw-divide-y-reverse: 0;
|
||||||
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
border-top-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||||
@@ -2441,6 +2423,33 @@ input:checked + .toggle-bg {
|
|||||||
opacity: 1.0;
|
opacity: 1.0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.htmx-settling .fade-in-settle {
|
||||||
|
opacity: 0.0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-settling.fade-in-settle {
|
||||||
|
opacity: 0.0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.fade-in-settle {
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-settling .slide-up-settle {
|
||||||
|
--tw-translate-y: 1.25rem !important;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.htmx-settling.slide-up-settle {
|
||||||
|
--tw-translate-y: 1.25rem !important;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-up-settle {
|
||||||
|
--tw-translate-y: 0px;
|
||||||
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
|
||||||
|
}
|
||||||
|
|
||||||
.htmx-added .slide-up {
|
.htmx-added .slide-up {
|
||||||
--tw-translate-y: 1.25rem !important;
|
--tw-translate-y: 1.25rem !important;
|
||||||
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
|
transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)) !important;
|
||||||
@@ -2588,6 +2597,33 @@ input:checked + .toggle-bg {
|
|||||||
--tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important;
|
--tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.choices:focus-within .choices__inner {
|
||||||
|
--tw-border-opacity: 1 !important;
|
||||||
|
border-color: rgb(0 156 234 / var(--tw-border-opacity)) !important;
|
||||||
|
--tw-ring-opacity: 1 !important;
|
||||||
|
--tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:is(.dark .choices:focus-within .choices__inner) {
|
||||||
|
--tw-border-opacity: 1 !important;
|
||||||
|
border-color: rgb(0 156 234 / var(--tw-border-opacity)) !important;
|
||||||
|
--tw-ring-opacity: 1 !important;
|
||||||
|
--tw-ring-color: rgb(0 156 234 / var(--tw-ring-opacity)) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.choices:focus-within .choices__inner {
|
||||||
|
outline: 2px solid transparent !important;
|
||||||
|
outline-offset: 2px;
|
||||||
|
--tw-ring-inset: var(--tw-empty,/*!*/ /*!*/);
|
||||||
|
--tw-ring-offset-width: 0px;
|
||||||
|
--tw-ring-offset-color: #fff;
|
||||||
|
--tw-ring-color: #007dbb;
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color);
|
||||||
|
box-shadow: var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
border-color: #007dbb;
|
||||||
|
}
|
||||||
|
|
||||||
.choices__inner .choices__input {
|
.choices__inner .choices__input {
|
||||||
margin: 0px !important;
|
margin: 0px !important;
|
||||||
--tw-bg-opacity: 1 !important;
|
--tw-bg-opacity: 1 !important;
|
||||||
|
|||||||
@@ -195,12 +195,6 @@
|
|||||||
:sent {:type 'String}
|
:sent {:type 'String}
|
||||||
:vendor {:type :vendor}}}
|
:vendor {:type :vendor}}}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
:yodlee_merchant {:fields {:id {:type :id}
|
:yodlee_merchant {:fields {:id {:type :id}
|
||||||
:yodlee_id {:type 'String}
|
:yodlee_id {:type 'String}
|
||||||
:name {:type 'String}}}
|
:name {:type 'String}}}
|
||||||
|
|||||||
104
src/clj/auto_ap/ssr/account.clj
Normal file
104
src/clj/auto_ap/ssr/account.clj
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
(ns auto-ap.ssr.account
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic :refer [conn]]
|
||||||
|
[auto-ap.graphql.utils
|
||||||
|
:refer [assert-can-see-client can-see-client? cleanse-query is-admin?]]
|
||||||
|
[auto-ap.solr :as solr]
|
||||||
|
[auto-ap.ssr.utils
|
||||||
|
:refer [entity-id ref->enum-schema wrap-schema-decode]]
|
||||||
|
[com.brunobonacci.mulog :as mu]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[ring.middleware.json :refer [wrap-json-response]]))
|
||||||
|
|
||||||
|
;; TODO this is basically duplicative of graphql version, make sure to keep in sync
|
||||||
|
;; TODO use valid clients from request rather than stuff like assert-can-see-client
|
||||||
|
|
||||||
|
(def search-pattern [:db/id
|
||||||
|
:account/numeric-code
|
||||||
|
:account/location
|
||||||
|
{:account/vendor-allowance [:db/ident]
|
||||||
|
:account/default-allowance [:db/ident]
|
||||||
|
:account/invoice-allowance [:db/ident]}])
|
||||||
|
|
||||||
|
(defn search- [id query client]
|
||||||
|
(let [client-part (if (some->> client (can-see-client? id))
|
||||||
|
(format "((applicability:(global OR optional) AND -client_id:*) OR (account_client_override_id:* AND client_id:%s))" client)
|
||||||
|
"(applicability:(global OR optional) AND -client_id:*)")
|
||||||
|
query (format "_text_:(%s) AND %s" (cleanse-query query) client-part)]
|
||||||
|
(mu/log ::searching :search-query query)
|
||||||
|
(for [{:keys [account_id name] :as g} (solr/query solr/impl "accounts"
|
||||||
|
{"query" query
|
||||||
|
"fields" "id, name, client_id, numeric_code, applicability, account_id"})]
|
||||||
|
|
||||||
|
{:account_id (first account_id)
|
||||||
|
:name (first name)})))
|
||||||
|
|
||||||
|
|
||||||
|
(defn account-search [{{:keys [q client-id allowance vendor-id] :as qp} :query-params id :identity}]
|
||||||
|
|
||||||
|
(when client-id
|
||||||
|
(assert-can-see-client id client-id))
|
||||||
|
(let [num (some-> (re-find #"([0-9]+)" q)
|
||||||
|
second
|
||||||
|
(not-empty )
|
||||||
|
Integer/parseInt)
|
||||||
|
|
||||||
|
valid-allowances (cond-> #{:allowance/allowed
|
||||||
|
:allowance/warn}
|
||||||
|
(is-admin? id) (conj :allowance/admin-only))
|
||||||
|
allowance (cond (= allowance :vendor)
|
||||||
|
:account/vendor-allowance
|
||||||
|
(= allowance :invoice)
|
||||||
|
:account/invoice-allowance
|
||||||
|
:else
|
||||||
|
:account/default-allowance)
|
||||||
|
|
||||||
|
vendor-account (when vendor-id
|
||||||
|
(-> (dc/q '[:find ?da
|
||||||
|
:in $ ?v
|
||||||
|
:where [?v :vendor/default-account ?da]]
|
||||||
|
(dc/db conn)
|
||||||
|
vendor-id)
|
||||||
|
ffirst))
|
||||||
|
xform (comp
|
||||||
|
(filter (fn [[_ a]]
|
||||||
|
(or
|
||||||
|
(valid-allowances (-> a allowance :db/ident))
|
||||||
|
(= (:db/id a) vendor-account))))
|
||||||
|
(map (fn [[n a]]
|
||||||
|
{:label (str (:account/numeric-code a) " - " n)
|
||||||
|
:value (:db/id a)
|
||||||
|
:location (:account/location a)
|
||||||
|
:warning (when (= :allowance/warn (-> a allowance :db/ident))
|
||||||
|
"This account is not typically used for this purpose.")})))]
|
||||||
|
{:body (take 10 (if q
|
||||||
|
(if num
|
||||||
|
(->> (dc/q '[:find ?n (pull ?i pattern)
|
||||||
|
:in $ ?numeric-code ?allowance pattern
|
||||||
|
:where [?i :account/numeric-code ?numeric-code]
|
||||||
|
[?i :account/name ?n]
|
||||||
|
(or [?i :account/applicability :account-applicability/global]
|
||||||
|
[?i :account/applicability :account-applicability/optional]
|
||||||
|
[?i :account/applicability :account-applicability/customized])]
|
||||||
|
(dc/db conn)
|
||||||
|
num
|
||||||
|
allowance
|
||||||
|
search-pattern)
|
||||||
|
(sequence xform))
|
||||||
|
(->> (search- id q client-id)
|
||||||
|
(sequence
|
||||||
|
(comp (map (fn [i] [(:name i) (dc/pull (dc/db conn) search-pattern (:account_id i))]))
|
||||||
|
xform))))
|
||||||
|
[]))}))
|
||||||
|
|
||||||
|
(def account-search (wrap-json-response (wrap-schema-decode account-search
|
||||||
|
:query-schema [:map
|
||||||
|
[:q :string]
|
||||||
|
[:client-id {:optional true
|
||||||
|
:default nil}
|
||||||
|
[:maybe entity-id]]
|
||||||
|
[:vendor-id {:optional true}
|
||||||
|
[:maybe entity-id]]
|
||||||
|
[:allowance {:optional true}
|
||||||
|
[:maybe (ref->enum-schema "allowance")]]])))
|
||||||
|
|
||||||
627
src/clj/auto_ap/ssr/admin/transaction_rules.clj
Normal file
627
src/clj/auto_ap/ssr/admin/transaction_rules.clj
Normal file
@@ -0,0 +1,627 @@
|
|||||||
|
(ns auto-ap.ssr.admin.transaction-rules
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic
|
||||||
|
:refer [add-sorter-fields
|
||||||
|
apply-pagination
|
||||||
|
apply-sort-3
|
||||||
|
audit-transact
|
||||||
|
conn
|
||||||
|
merge-query
|
||||||
|
pull-attr
|
||||||
|
pull-many
|
||||||
|
query2]]
|
||||||
|
[auto-ap.datomic.accounts :as d-accounts]
|
||||||
|
[auto-ap.graphql.utils :refer [extract-client-ids]]
|
||||||
|
[auto-ap.query-params :as query-params]
|
||||||
|
[auto-ap.routes.utils
|
||||||
|
:refer [wrap-admin wrap-client-redirect-unauthenticated]]
|
||||||
|
[auto-ap.ssr-routes :as ssr-routes]
|
||||||
|
[auto-ap.ssr.company :refer [bank-account-typeahead*]]
|
||||||
|
[auto-ap.ssr.components :as com]
|
||||||
|
[auto-ap.ssr.grid-page-helper :as helper]
|
||||||
|
[auto-ap.ssr.hx :as hx]
|
||||||
|
[auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]]
|
||||||
|
[auto-ap.ssr.svg :as svg]
|
||||||
|
[auto-ap.ssr.utils
|
||||||
|
:refer [apply-middleware-to-all-handlers
|
||||||
|
entity-id
|
||||||
|
field-validation-error
|
||||||
|
form-validation-error
|
||||||
|
html-response
|
||||||
|
many-entity
|
||||||
|
money
|
||||||
|
path->name2
|
||||||
|
percentage
|
||||||
|
ref->enum-schema
|
||||||
|
ref->radio-options
|
||||||
|
regex
|
||||||
|
temp-id
|
||||||
|
wrap-form-4xx-2
|
||||||
|
wrap-schema-decode]]
|
||||||
|
[auto-ap.utils :refer [dollars=]]
|
||||||
|
[bidi.bidi :as bidi]
|
||||||
|
[cheshire.core :as cheshire]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[hiccup2.core :as hiccup]
|
||||||
|
[iol-ion.query :refer [ident]]
|
||||||
|
[malli.core :as mc]))
|
||||||
|
|
||||||
|
;; TODO with dependencies, I really don't like that you have to be ultra specific in what
|
||||||
|
;; you want to include, and generating the routes and interconnection is weird too.
|
||||||
|
;; I'm tempted to say to include a full snapshot of the form, and the indicator
|
||||||
|
;; as to which one to generate.
|
||||||
|
|
||||||
|
|
||||||
|
;; TODO lots of escaping concerns (urls in javascript), all these weird name filters
|
||||||
|
|
||||||
|
;; TODO better generation of names?
|
||||||
|
|
||||||
|
|
||||||
|
(defn filters [request]
|
||||||
|
[:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms"
|
||||||
|
"hx-get" (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-transaction-rule-table)
|
||||||
|
"hx-target" "#transaction-rule-table"
|
||||||
|
"hx-indicator" "#transaction-rule-table"}
|
||||||
|
|
||||||
|
[:fieldset.space-y-6
|
||||||
|
(com/field {:label "Vendor"}
|
||||||
|
(com/typeahead {:name "vendor"
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes
|
||||||
|
:vendor-search)
|
||||||
|
:id (str "vendor-search")
|
||||||
|
:value [(:db/id (:vendor (:parsed-query-params request)))
|
||||||
|
(:vendor/name (:vendor (:parsed-query-params request)))]}))
|
||||||
|
(com/field {:label "Note"}
|
||||||
|
(com/text-input {:name "note"
|
||||||
|
:id "note"
|
||||||
|
:class "hot-filter"
|
||||||
|
:value (:note (:parsed-query-params request))
|
||||||
|
:placeholder "HOME DEPOT lte 250.0"
|
||||||
|
:size :small}))
|
||||||
|
|
||||||
|
(com/field {:label "Description"}
|
||||||
|
(com/text-input {:name "description"
|
||||||
|
:id "description"
|
||||||
|
:class "hot-filter"
|
||||||
|
:value (:description (:parsed-query-params request))
|
||||||
|
:placeholder "LOWES"
|
||||||
|
:size :small}))]])
|
||||||
|
|
||||||
|
(def default-read '[:db/id
|
||||||
|
:transaction-rule/description
|
||||||
|
:transaction-rule/note
|
||||||
|
:transaction-rule/amount-lte
|
||||||
|
:transaction-rule/amount-gte
|
||||||
|
:transaction-rule/dom-lte
|
||||||
|
:transaction-rule/dom-gte
|
||||||
|
{:transaction-rule/client [:client/name :db/id :client/code :client/locations]}
|
||||||
|
{:transaction-rule/bank-account [:db/id :bank-account/name]}
|
||||||
|
{:transaction-rule/yodlee-merchant [:db/id :yodlee-merchant/name :yodlee-merchant/yodlee-id]}
|
||||||
|
{[:transaction-rule/transaction-approval-status :xform iol-ion.query/ident] [:db/id :db/ident]}
|
||||||
|
{:transaction-rule/vendor [:vendor/name :db/id :vendor/default-account]}
|
||||||
|
{:transaction-rule/accounts [:transaction-rule-account/percentage
|
||||||
|
:transaction-rule-account/location
|
||||||
|
{:transaction-rule-account/account [:account/name :db/id :account/numeric-code :account/location
|
||||||
|
{:account/client-overrides [:db/id
|
||||||
|
:account-client-override/name
|
||||||
|
{:account-client-override/client [:db/id :client/name]}]}]}
|
||||||
|
:db/id]}])
|
||||||
|
|
||||||
|
(defn fetch-ids [db request]
|
||||||
|
(let [query-params (:parsed-query-params request)
|
||||||
|
valid-clients (extract-client-ids (:clients request)
|
||||||
|
(:client request)
|
||||||
|
(:client-id query-params)
|
||||||
|
(when (:client-code query-params)
|
||||||
|
[:client/code (:client-code query-params)]))
|
||||||
|
query (cond-> {:query {:find []
|
||||||
|
:in ['$]
|
||||||
|
:where []}
|
||||||
|
:args [db]}
|
||||||
|
(:sort query-params) (add-sorter-fields {"client" ['[?e :transaction-rule/client ?c]
|
||||||
|
'[?c :client/name ?sort-client]]
|
||||||
|
|
||||||
|
"yodlee-merchant" ['[?e :transaction-rule/yodlee-merchant ?ym]
|
||||||
|
'[?ym :yodlee-merchant/name ?sort-yodlee-merchant]]
|
||||||
|
"bank-account" ['[?e :transaction-rule/bank-account ?ba]
|
||||||
|
'[?ba :bank-account/name ?sort-bank-account]]
|
||||||
|
"description" ['[?e :transaction-rule/description ?sort-description]]
|
||||||
|
"note" ['[?e :transaction-rule/note ?sort-note]]
|
||||||
|
"amount-lte" ['[?e :transaction-rule/amount-lte ?sort-amount-lte]]
|
||||||
|
"amount-gte" ['[?e :transaction-rule/amount-gte ?sort-amount-gte]]}
|
||||||
|
query-params)
|
||||||
|
|
||||||
|
(seq valid-clients)
|
||||||
|
(merge-query {:query {:in ['[?xx ...]]
|
||||||
|
:where ['(or-join [?e]
|
||||||
|
(and [?e :transaction-rule/client ?xx])
|
||||||
|
(and (not [?e :transaction-rule/client])
|
||||||
|
[?e :transaction-rule/note]))]}
|
||||||
|
:args [valid-clients]})
|
||||||
|
|
||||||
|
(-> query-params :vendor :db/id)
|
||||||
|
(merge-query {:query {:in ['?vendor-id]
|
||||||
|
:where ['[?e :transaction-rule/vendor ?vendor-id]]}
|
||||||
|
:args [(-> query-params :vendor :db/id)]})
|
||||||
|
|
||||||
|
(not (str/blank? (:note query-params)))
|
||||||
|
(merge-query {:query {:in ['?note-pattern]
|
||||||
|
:where ['[?e :transaction-rule/note ?n]
|
||||||
|
'[(re-find ?note-pattern ?n)]]}
|
||||||
|
:args [(re-pattern (str "(?i)" (:note query-params)))]})
|
||||||
|
|
||||||
|
(not (str/blank? (:description query-params)))
|
||||||
|
(merge-query {:query {:in ['?description]
|
||||||
|
:where ['[?e :transaction-rule/description ?d]
|
||||||
|
'[(clojure.string/lower-case ?d) ?d2]
|
||||||
|
'[(clojure.string/includes? ?d2 ?description)]]}
|
||||||
|
:args [(clojure.string/lower-case (:description query-params))]})
|
||||||
|
|
||||||
|
true
|
||||||
|
(merge-query {:query {:find ['?e]
|
||||||
|
:where ['[?e :transaction-rule/transaction-approval-status]]}}))]
|
||||||
|
|
||||||
|
(cond->> (query2 query)
|
||||||
|
true (apply-sort-3 query-params)
|
||||||
|
true (apply-pagination query-params))))
|
||||||
|
|
||||||
|
(defn hydrate-results [ids db _]
|
||||||
|
(let [results (->> (pull-many db default-read ids)
|
||||||
|
(group-by :db/id))
|
||||||
|
refunds (->> ids
|
||||||
|
(map results)
|
||||||
|
(map first))]
|
||||||
|
refunds))
|
||||||
|
|
||||||
|
(defn fetch-page [request]
|
||||||
|
(let [db (dc/db conn)
|
||||||
|
{ids-to-retrieve :ids matching-count :count} (fetch-ids db request)]
|
||||||
|
|
||||||
|
[(->> (hydrate-results ids-to-retrieve db request))
|
||||||
|
matching-count]))
|
||||||
|
|
||||||
|
(def grid-page
|
||||||
|
(helper/build {:id "transaction-rule-table"
|
||||||
|
:nav (com/admin-aside-nav)
|
||||||
|
:page-specific-nav filters
|
||||||
|
:fetch-page fetch-page
|
||||||
|
:parse-query-params (comp
|
||||||
|
(query-params/parse-key :vendor #(dc/pull (dc/db conn) '[:vendor/name :db/id] (Long/parseLong %)))
|
||||||
|
(helper/default-parse-query-params grid-page))
|
||||||
|
:action-buttons (fn [request]
|
||||||
|
[(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-transaction-rule-new-dialog))
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
:hx-swap "innerHTML"
|
||||||
|
:color :primary}
|
||||||
|
"New Transaction Rule")])
|
||||||
|
:row-buttons (fn [request entity]
|
||||||
|
[(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-transaction-rule-edit-dialog
|
||||||
|
:db/id (:db/id entity)))
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
:hx-swap "innerHTML"}
|
||||||
|
svg/pencil)])
|
||||||
|
:breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin)}
|
||||||
|
"Admin"]
|
||||||
|
|
||||||
|
[:a {:href (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-transaction-rules)}
|
||||||
|
"Transaction Rules"]]
|
||||||
|
:title "Rules"
|
||||||
|
:entity-name "Rule"
|
||||||
|
:route :admin-transaction-rule-table
|
||||||
|
:headers [{:key "client"
|
||||||
|
:name "Client"
|
||||||
|
:sort-key "client"
|
||||||
|
:render #(-> % :transaction-rule/client :client/name)}
|
||||||
|
{:key "bank-account"
|
||||||
|
:name "Bank account"
|
||||||
|
:sort-key "bank-account"
|
||||||
|
:render #(-> % :transaction-rule/bank-account :bank-account/name)}
|
||||||
|
{:key "description"
|
||||||
|
:name "Description"
|
||||||
|
:sort-key "description"
|
||||||
|
:render :transaction-rule/description}
|
||||||
|
{:key "amount"
|
||||||
|
:name "Amount"
|
||||||
|
:sort-key "amount"
|
||||||
|
:render (fn [{:transaction-rule/keys [amount-gte amount-lte]}]
|
||||||
|
[:div.flex.gap-2 (when amount-gte
|
||||||
|
(com/pill {:color :red} (format "more than $%.2f" amount-gte)))
|
||||||
|
|
||||||
|
(when amount-lte
|
||||||
|
(com/pill {:color :primary} (format "less than $%.2f" amount-lte)))])}
|
||||||
|
{:key "note"
|
||||||
|
:name "Note"
|
||||||
|
:sort-key "note"
|
||||||
|
:render :transaction-rule/note}
|
||||||
|
]}))
|
||||||
|
|
||||||
|
(def row* (partial helper/row* grid-page))
|
||||||
|
(def table* (partial helper/table* grid-page))
|
||||||
|
|
||||||
|
(defn entity->note [{:transaction-rule/keys [amount-lte amount-gte description client dom-lte dom-gte]}]
|
||||||
|
(str/join " - " (filter (complement str/blank?)
|
||||||
|
[(when client (pull-attr (dc/db conn) :client/code client))
|
||||||
|
description
|
||||||
|
(when (or amount-lte amount-gte)
|
||||||
|
(str (when amount-gte
|
||||||
|
(str amount-gte "<"))
|
||||||
|
"amt"
|
||||||
|
(when amount-lte
|
||||||
|
(str "<" amount-lte))))
|
||||||
|
|
||||||
|
(when (or dom-lte dom-gte)
|
||||||
|
(str (when dom-gte
|
||||||
|
(str dom-gte "<"))
|
||||||
|
"dom"
|
||||||
|
(when dom-lte
|
||||||
|
(str "<" dom-lte))))])))
|
||||||
|
|
||||||
|
(defn bank-account-belongs-to-client? [bank-account-id client-id]
|
||||||
|
(get (->> (dc/pull (dc/db conn) [{:client/bank-accounts [:db/id]}] client-id)
|
||||||
|
:client/bank-accounts
|
||||||
|
(map :db/id)
|
||||||
|
(set))
|
||||||
|
bank-account-id))
|
||||||
|
|
||||||
|
(defn transaction-rule-save [{:keys [form-params request-method identity] :as request}]
|
||||||
|
(let [entity (cond-> form-params
|
||||||
|
(= :post request-method) (assoc :db/id "new")
|
||||||
|
true (assoc :transaction-rule/note (entity->note form-params)))
|
||||||
|
_ (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts entity) (range))
|
||||||
|
:let [account-location (pull-attr (dc/db conn) :account/location account)]
|
||||||
|
:when (and account-location (not= account-location location))]
|
||||||
|
(field-validation-error (str "must be " account-location)
|
||||||
|
[:transaction-rule/accounts i :transaction-rule-account/location]
|
||||||
|
:form entity))
|
||||||
|
|
||||||
|
total (reduce +
|
||||||
|
0.0
|
||||||
|
(map :transaction-rule-account/percentage
|
||||||
|
(:transaction-rule/accounts entity)))
|
||||||
|
_ (when-not (dollars= 1.0 total)
|
||||||
|
(form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total)))
|
||||||
|
:form entity))
|
||||||
|
|
||||||
|
_ (when (and (:transaction-rule/bank-account entity)
|
||||||
|
(not (bank-account-belongs-to-client? (:transaction-rule/bank-account entity)
|
||||||
|
(:transaction-rule/client entity))))
|
||||||
|
(field-validation-error "does not belong to client"
|
||||||
|
[:transaction-rule/bank-account]
|
||||||
|
:form entity))
|
||||||
|
|
||||||
|
|
||||||
|
{:keys [tempids]} (audit-transact [[:upsert-entity entity]]
|
||||||
|
(:identity request))
|
||||||
|
updated-account (dc/pull (dc/db conn)
|
||||||
|
default-read
|
||||||
|
(or (get tempids (:db/id entity)) (:db/id entity)))]
|
||||||
|
(html-response
|
||||||
|
(row* identity updated-account {:flash? true})
|
||||||
|
:headers {"hx-trigger" "modalClosing"
|
||||||
|
"hx-retarget" (format "#transaction-rule-table tr[data-id=\"%d\"]" (:db/id updated-account))})))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn- location-select*
|
||||||
|
[{:keys [ name account-location client-locations value]}]
|
||||||
|
(com/select {:options (into [["" ""]]
|
||||||
|
(cond account-location
|
||||||
|
[[account-location account-location]]
|
||||||
|
|
||||||
|
(seq client-locations)
|
||||||
|
(into [["Shared" "Shared"]]
|
||||||
|
(for [cl client-locations]
|
||||||
|
[cl cl]))
|
||||||
|
:else
|
||||||
|
[["Shared" "Shared"]]))
|
||||||
|
:name name
|
||||||
|
:value value}))
|
||||||
|
|
||||||
|
(defn- account-typeahead*
|
||||||
|
[{:keys [name value client-id]}]
|
||||||
|
[:div.flex.flex-col
|
||||||
|
(com/typeahead {:name name
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id)
|
||||||
|
:id name
|
||||||
|
:value value
|
||||||
|
:value-fn (some-fn :db/id identity)
|
||||||
|
:content-fn (fn [value]
|
||||||
|
(:account/name (d-accounts/clientize (cond->> value
|
||||||
|
(nat-int? value) (dc/pull (dc/db conn) d-accounts/default-read))
|
||||||
|
client-id)))})])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn- transaction-rule-account-row*
|
||||||
|
[transaction-rule account]
|
||||||
|
(com/data-grid-row {}
|
||||||
|
(let [account-name (path->name2 :transaction-rule/accounts (:db/id account) :transaction-rule-account/account)
|
||||||
|
location-name (path->name2 :transaction-rule/accounts (:db/id account) :transaction-rule-account/location)]
|
||||||
|
(list
|
||||||
|
(com/data-grid-cell {}
|
||||||
|
[:div {:hx-trigger (hx/trigger-field-change :name "transaction-rule/client"
|
||||||
|
:from "#edit-form")
|
||||||
|
:hx-include "#edit-form"
|
||||||
|
:hx-vals (hx/vals {:name account-name})
|
||||||
|
:hx-ext "rename-params"
|
||||||
|
:hx-rename-params-ex (hx/json {:transaction-rule/client "client-id"
|
||||||
|
:name "name"
|
||||||
|
account-name "value"})
|
||||||
|
:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead))
|
||||||
|
:hx-swap "innerHTML"}
|
||||||
|
(account-typeahead* {:value (:transaction-rule-account/account account)
|
||||||
|
:client-id (:db/id (:transaction-rule/client transaction-rule))
|
||||||
|
:name account-name})
|
||||||
|
(com/field-errors {:source account
|
||||||
|
:key :transaction-rule-account/account})])
|
||||||
|
(com/data-grid-cell {}
|
||||||
|
[:div {:hx-trigger (hx/triggers
|
||||||
|
(hx/trigger-field-change :name "transaction-rule/client"
|
||||||
|
:from "#edit-form")
|
||||||
|
(hx/trigger-field-change :name account-name
|
||||||
|
:from "#edit-form"))
|
||||||
|
:hx-include "#edit-form"
|
||||||
|
:hx-vals (hx/vals {:name location-name})
|
||||||
|
:hx-ext "rename-params"
|
||||||
|
:hx-rename-params-ex (hx/json {"transaction-rule/client" "client-id"
|
||||||
|
account-name "account-id"
|
||||||
|
"name" "name"
|
||||||
|
location-name "value"})
|
||||||
|
:hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select)
|
||||||
|
:hx-swap "innerHTML"}
|
||||||
|
(location-select* {:name location-name
|
||||||
|
:account-location (:account/location (cond->> (:transaction-rule-account/account account)
|
||||||
|
(nat-int? (:transaction-rule-account/account account)) (dc/pull (dc/db conn)
|
||||||
|
'[:account/location])))
|
||||||
|
:client-locations (:client/locations (:transaction-rule/client transaction-rule))
|
||||||
|
:value (:transaction-rule-account/location account)})
|
||||||
|
(com/field-errors {:source account
|
||||||
|
:key :transaction-rule-account/location})])
|
||||||
|
(com/data-grid-cell (com/money-input {:name (format "transaction-rule/accounts[%s][transaction-rule-account/percentage]" (:db/id account))
|
||||||
|
:class "w-16"
|
||||||
|
:value (some-> account
|
||||||
|
:transaction-rule-account/percentage
|
||||||
|
(* 100 )
|
||||||
|
(long ))})
|
||||||
|
(com/field-errors {:source account
|
||||||
|
:key :transaction-rule-account/percentage}))))
|
||||||
|
(com/data-grid-cell
|
||||||
|
(com/a-icon-button
|
||||||
|
{"_" (hiccup/raw "on click halt the event then transition the closest <tr />'s opacity to 0 then remove closest <tr />")
|
||||||
|
:href "#"}
|
||||||
|
svg/x))))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn dialog* [& {:keys [ entity form-params]}]
|
||||||
|
(com/modal
|
||||||
|
{:modal-class "max-w-4xl"}
|
||||||
|
(com/modal-card
|
||||||
|
{}
|
||||||
|
[:div.flex [:div.p-2 "Transaction Rule"] ]
|
||||||
|
[:form#edit-form (merge {:hx-ext "response-targets"
|
||||||
|
:hx-swap "outerHTML swap:300ms"
|
||||||
|
:hx-target "#modal-holder"
|
||||||
|
:hx-target-400 "#form-errors .error-content"}
|
||||||
|
form-params)
|
||||||
|
[:fieldset {:class "hx-disable" :hx-disinherit "hx-target"}
|
||||||
|
|
||||||
|
[:div.space-y-6
|
||||||
|
(when-let [id (:db/id entity)]
|
||||||
|
(com/hidden {:name "db/id"
|
||||||
|
:value id}))
|
||||||
|
(com/field {:label "Client"}
|
||||||
|
[:div.w-96
|
||||||
|
(com/typeahead {:name "transaction-rule/client"
|
||||||
|
:class "w-96"
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :company-search)
|
||||||
|
:id (str "form-client-search")
|
||||||
|
:value (:transaction-rule/client entity)
|
||||||
|
:value-fn (some-fn :db/id identity)
|
||||||
|
:content-fn (fn [c] (cond->> c
|
||||||
|
(nat-int? c) (dc/pull (dc/db conn) '[:client/name])
|
||||||
|
true :client/name))})])
|
||||||
|
|
||||||
|
|
||||||
|
(com/field {:label "Bank Account"}
|
||||||
|
[:div#bank-account-spot.w-96 {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead)
|
||||||
|
:hx-trigger (hx/trigger-field-change :name "transaction-rule/client"
|
||||||
|
:from "#edit-form")
|
||||||
|
:hx-swap "innerHTML"
|
||||||
|
:hx-ext "rename-params"
|
||||||
|
:hx-include "#edit-form"
|
||||||
|
:hx-vals (hx/vals {:name "transaction-rule/bank-account"})
|
||||||
|
:hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id"
|
||||||
|
"name" "name"})}
|
||||||
|
(bank-account-typeahead* {:client-id ((some-fn :db/id identity) (:transaction-rule/client entity))
|
||||||
|
:name "transaction-rule/bank-account"
|
||||||
|
:value (:transaction-rule/bank-account entity)})
|
||||||
|
(com/field-errors {:source entity
|
||||||
|
:key :transaction-rule/bank-account})])
|
||||||
|
|
||||||
|
(com/field {:label "Description"
|
||||||
|
:error-source entity
|
||||||
|
:error-key :transaction-rule/description}
|
||||||
|
(com/text-input {:name "transaction-rule/description"
|
||||||
|
"_" (hiccup/raw "on load call me.focus()")
|
||||||
|
:placeholder "HOME DEPOT"
|
||||||
|
:class "w-96"
|
||||||
|
:value (:transaction-rule/description entity)}))
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(com/field {:label "Amount"}
|
||||||
|
[:div.flex.gap-2
|
||||||
|
(com/money-input {:name "transaction-rule/amount-gte"
|
||||||
|
:placeholder ">="
|
||||||
|
:class "w-24"
|
||||||
|
:value (:transaction-rule/amount-gte entity)})
|
||||||
|
(com/money-input {:name "transaction-rule/amount-lte"
|
||||||
|
:placeholder "<="
|
||||||
|
:class "w-24"
|
||||||
|
:value (:transaction-rule/amount-lte entity)})])
|
||||||
|
|
||||||
|
(com/field {:label "Day of month"}
|
||||||
|
[:div.flex.gap-2
|
||||||
|
(com/int-input {:name "transaction-rule/dom-gte"
|
||||||
|
:placeholder ">="
|
||||||
|
:class "w-24"
|
||||||
|
:value (:transaction-rule/dom-gte entity)})
|
||||||
|
(com/int-input {:name "transaction-rule/dom-lte"
|
||||||
|
:placeholder "<="
|
||||||
|
:class "w-24"
|
||||||
|
:value (:transaction-rule/dom-lte entity)})])
|
||||||
|
|
||||||
|
[:h2.text-lg "Outcomes"]
|
||||||
|
(com/field {:label "Assign Vendor"
|
||||||
|
:error-source entity
|
||||||
|
:error-key :transaction-rule/vendor}
|
||||||
|
[:div.w-96
|
||||||
|
(com/typeahead {:name "transaction-rule/vendor"
|
||||||
|
:placeholder "Search..."
|
||||||
|
:url (bidi/path-for ssr-routes/only-routes :vendor-search)
|
||||||
|
:id (str "form-vendor-search")
|
||||||
|
:value (:transaction-rule/vendor entity)
|
||||||
|
:value-fn (some-fn :db/id identity)
|
||||||
|
:content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])
|
||||||
|
|
||||||
|
(com/data-grid {:headers [(com/data-grid-header {}
|
||||||
|
"Account")
|
||||||
|
(com/data-grid-header {:class "w-16"} "Location")
|
||||||
|
(com/data-grid-header {:class "w-16"} "%")
|
||||||
|
(com/data-grid-header {:class "w-16"})]
|
||||||
|
:id "transaction-rule-account-table"}
|
||||||
|
(for [tra (:transaction-rule/accounts entity)]
|
||||||
|
(transaction-rule-account-row* entity tra)))
|
||||||
|
(com/field-errors {:source entity
|
||||||
|
:key :transaction-rule/accounts})
|
||||||
|
(com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-transaction-rule-new-account)
|
||||||
|
:hx-include "#edit-form"
|
||||||
|
:hx-ext "rename-params"
|
||||||
|
:hx-rename-params-ex (cheshire/generate-string {"transaction-rule/client" "client-id"})
|
||||||
|
:hx-target "#transaction-rule-account-table tbody"
|
||||||
|
:hx-swap "beforeend"}
|
||||||
|
"New account")
|
||||||
|
(com/radio {:options (ref->radio-options "transaction-approval-status")
|
||||||
|
:value (:transaction-rule/transaction-approval-status entity)
|
||||||
|
:name (path->name2 :transaction-rule/transaction-approval-status)})
|
||||||
|
|
||||||
|
[:div#form-errors [:span.error-content
|
||||||
|
(com/field-errors {:source entity})]]
|
||||||
|
(com/button {:color :primary :form "edit-form" :type "submit"}
|
||||||
|
"Save")]]]
|
||||||
|
[:div])))
|
||||||
|
|
||||||
|
(defn new-account [{{:keys [client-id]} :query-params}]
|
||||||
|
(html-response
|
||||||
|
(transaction-rule-account-row*
|
||||||
|
{:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id]
|
||||||
|
client-id)}
|
||||||
|
{:db/id (str (java.util.UUID/randomUUID))
|
||||||
|
:transaction-rule-account/location "shared"})))
|
||||||
|
|
||||||
|
(defn location-select [{{:keys [name account-id client-id value] :as qp} :query-params}]
|
||||||
|
(html-response (location-select* {:name name
|
||||||
|
:value value
|
||||||
|
:account-location (some->> account-id
|
||||||
|
(pull-attr (dc/db conn) :account/location))
|
||||||
|
:client-locations (some->> client-id
|
||||||
|
(pull-attr (dc/db conn) :client/locations))})))
|
||||||
|
|
||||||
|
(defn account-typeahead [{{:keys [name value client-id] :as qp} :query-params}]
|
||||||
|
(let [account (some->> value (dc/pull (dc/db conn) [:account/name :db/id
|
||||||
|
{:account/client-overrides [:db/id
|
||||||
|
:account-client-override/name
|
||||||
|
{:account-client-override/client [:db/id :client/name]}]}]))
|
||||||
|
client-id client-id]
|
||||||
|
(html-response (account-typeahead* {:name name
|
||||||
|
:value account
|
||||||
|
:client-id client-id}))))
|
||||||
|
|
||||||
|
(defn transaction-rule-edit-dialog [request]
|
||||||
|
(let [entity (or
|
||||||
|
(some-> request :last-form)
|
||||||
|
(some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %))))]
|
||||||
|
(html-response (dialog* :entity entity
|
||||||
|
:form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-transaction-rule-edit-save))})
|
||||||
|
:headers {"hx-trigger" "modalOpening"})))
|
||||||
|
|
||||||
|
(defn transaction-rule-error [request]
|
||||||
|
(let [entity (some-> request :last-form)]
|
||||||
|
(html-response (dialog* :entity entity)
|
||||||
|
:headers {"hx-retarget" "#edit-form fieldset"
|
||||||
|
"hx-reselect" "#edit-form fieldset"})))
|
||||||
|
|
||||||
|
|
||||||
|
(defn account-new-dialog [_]
|
||||||
|
(html-response (dialog* :account nil
|
||||||
|
:form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes
|
||||||
|
:admin-account-new-save))})
|
||||||
|
:headers {"hx-trigger" "modalOpening"}))
|
||||||
|
|
||||||
|
(def transaction-rule-schema (mc/schema
|
||||||
|
[:map
|
||||||
|
[:db/id {:optional true} [:maybe entity-id]]
|
||||||
|
[:transaction-rule/client {:optional true} [:maybe entity-id]]
|
||||||
|
[:transaction-rule/description [:and regex
|
||||||
|
[:string {:min 3}]]]
|
||||||
|
[:transaction-rule/bank-account [:maybe entity-id]]
|
||||||
|
[:transaction-rule/amount-gte {:optional true} [:maybe money]]
|
||||||
|
[:transaction-rule/amount-lte {:optional true} [:maybe money]]
|
||||||
|
[:transaction-rule/dom-gte {:optional true} [:maybe :int]]
|
||||||
|
[:transaction-rule/dom-lte {:optional true} [:maybe :int]]
|
||||||
|
[:transaction-rule/vendor {:optional true} [:maybe entity-id]]
|
||||||
|
[:transaction-rule/transaction-approval-status (ref->enum-schema "transaction-approval-status")]
|
||||||
|
[:transaction-rule/accounts
|
||||||
|
(many-entity {:min 1}
|
||||||
|
[:db/id [:or entity-id temp-id]]
|
||||||
|
[:transaction-rule-account/account entity-id]
|
||||||
|
[:transaction-rule-account/location :string]
|
||||||
|
[:transaction-rule-account/percentage percentage])]]))
|
||||||
|
|
||||||
|
(def key->handler
|
||||||
|
(apply-middleware-to-all-handlers
|
||||||
|
(->>
|
||||||
|
{:admin-transaction-rules (helper/page-route grid-page)
|
||||||
|
:admin-transaction-rule-table (helper/table-route grid-page)
|
||||||
|
:admin-transaction-rule-new-account (-> new-account
|
||||||
|
(wrap-schema-decode :query-schema [:map
|
||||||
|
[:client-id {:optional true}
|
||||||
|
[:maybe entity-id]]])
|
||||||
|
wrap-admin wrap-client-redirect-unauthenticated)
|
||||||
|
:admin-transaction-rule-location-select (-> location-select
|
||||||
|
(wrap-schema-decode :query-schema [:map
|
||||||
|
[:name :string]
|
||||||
|
[:client-id {:optional true}
|
||||||
|
[:maybe entity-id]]
|
||||||
|
[:account-id {:optional true}
|
||||||
|
[:maybe entity-id]]]))
|
||||||
|
:admin-transaction-rule-account-typeahead (-> account-typeahead
|
||||||
|
(wrap-schema-decode :query-schema [:map
|
||||||
|
[:name :string]
|
||||||
|
[:client-id {:optional true}
|
||||||
|
[:maybe entity-id]]
|
||||||
|
[:value {:optional true}
|
||||||
|
[:maybe entity-id]]]))
|
||||||
|
:admin-transaction-rule-save (-> transaction-rule-save
|
||||||
|
(wrap-schema-decode :form-schema transaction-rule-schema)
|
||||||
|
(wrap-nested-form-params)
|
||||||
|
(wrap-form-4xx-2 transaction-rule-error))
|
||||||
|
:admin-transaction-rule-edit-dialog (-> transaction-rule-edit-dialog
|
||||||
|
(wrap-schema-decode :route-schema [:map [:db/id entity-id]]))
|
||||||
|
:admin-transaction-rule-new-dialog account-new-dialog})
|
||||||
|
(fn [h]
|
||||||
|
(-> h
|
||||||
|
(wrap-admin)
|
||||||
|
(wrap-client-redirect-unauthenticated)))))
|
||||||
@@ -7,9 +7,9 @@
|
|||||||
[auto-ap.ssr.components :as com]
|
[auto-ap.ssr.components :as com]
|
||||||
[auto-ap.ssr.svg :as svg]
|
[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 [html-response]]
|
||||||
[bidi.bidi :as bidi]
|
[bidi.bidi :as bidi]
|
||||||
[cemerick.url :as url]
|
[cemerick.url :as url]
|
||||||
[clojure.set :as set]
|
|
||||||
[clojure.string :as str]
|
[clojure.string :as str]
|
||||||
[config.core :refer [env]]
|
[config.core :refer [env]]
|
||||||
[datomic.api :as dc]
|
[datomic.api :as dc]
|
||||||
@@ -72,17 +72,59 @@
|
|||||||
|
|
||||||
(defn search [{:keys [clients query-params]}]
|
(defn search [{:keys [clients query-params]}]
|
||||||
(let [valid-client-ids (set (map :db/id clients))
|
(let [valid-client-ids (set (map :db/id clients))
|
||||||
name-like-ids (when (not-empty (get query-params "q"))
|
name-like-ids (when (not-empty (get query-params "q"))
|
||||||
(set (map (comp #(Long/parseLong %) :id)
|
(set (map (comp #(Long/parseLong %) :id)
|
||||||
(solr/query solr/impl "clients"
|
(solr/query solr/impl "clients"
|
||||||
{"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q"))))
|
{"query" (format "_text_:(%s*)" (str/upper-case (solr/escape (get query-params "q"))))
|
||||||
"fields" "id"
|
"fields" "id"
|
||||||
"limit" 300}))))
|
"limit" 300}))))
|
||||||
valid-clients (for [n name-like-ids
|
valid-clients (for [n name-like-ids
|
||||||
:when (valid-client-ids n)]
|
:when (valid-client-ids n)]
|
||||||
{"value" n "label" (pull-attr (dc/db conn) :client/name n)}
|
{"value" n "label" (pull-attr (dc/db conn) :client/name n)}
|
||||||
)]
|
)]
|
||||||
{:body (take 10 valid-clients)}))
|
{:body (take 10 valid-clients)}))
|
||||||
|
|
||||||
(def search (wrap-json-response search))
|
(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? (str/upper-case name)
|
||||||
|
(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)
|
||||||
|
:id (str "form-bank-account-search")
|
||||||
|
: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")})))
|
||||||
|
|||||||
@@ -26,10 +26,12 @@
|
|||||||
|
|
||||||
(def text-input inputs/text-input-)
|
(def text-input inputs/text-input-)
|
||||||
(def money-input inputs/money-input-)
|
(def money-input inputs/money-input-)
|
||||||
|
(def int-input inputs/int-input-)
|
||||||
(def date-input inputs/date-input-)
|
(def date-input inputs/date-input-)
|
||||||
(def hidden inputs/hidden-)
|
(def hidden inputs/hidden-)
|
||||||
(def select inputs/select-)
|
(def select inputs/select-)
|
||||||
(def typeahead inputs/typeahead-)
|
(def typeahead inputs/typeahead-)
|
||||||
|
(def field-errors inputs/field-errors-)
|
||||||
(def field inputs/field-)
|
(def field inputs/field-)
|
||||||
|
|
||||||
(def left-aside aside/left-aside-)
|
(def left-aside aside/left-aside-)
|
||||||
|
|||||||
@@ -37,8 +37,8 @@
|
|||||||
[:input {:id "checkbox-all", :type "checkbox", :class "w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}]
|
[:input {:id "checkbox-all", :type "checkbox", :class "w-4 h-4 bg-gray-100 border-gray-300 rounded text-primary-600 focus:ring-primary-500 dark:focus:ring-primary-600 dark:ring-offset-gray-800 focus:ring-2 dark:bg-gray-700 dark:border-gray-600"}]
|
||||||
[:label {:for "checkbox-all", :class "sr-only"} "checkbox"]]])
|
[:label {:for "checkbox-all", :class "sr-only"} "checkbox"]]])
|
||||||
|
|
||||||
(defn data-grid- [{:keys [headers thead-params]} & rest]
|
(defn data-grid- [{:keys [headers thead-params id]} & rest]
|
||||||
[:table {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400"}
|
[:table {:class "w-full text-sm text-left text-gray-500 dark:text-gray-400" :id id}
|
||||||
[:thead (assoc thead-params :class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400")
|
[:thead (assoc thead-params :class "text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400")
|
||||||
(into
|
(into
|
||||||
[:tr]
|
[:tr]
|
||||||
|
|||||||
@@ -2,43 +2,15 @@
|
|||||||
(:require [hiccup2.core :as hiccup]))
|
(:require [hiccup2.core :as hiccup]))
|
||||||
|
|
||||||
(defn modal- [params & children]
|
(defn modal- [params & children]
|
||||||
[:div
|
[:div {:class (str "relative w-full max-h-full " (or (:modal-class params) " max-w-2xl "))}
|
||||||
[:div#modal-holder { :tabindex "-1", :class "fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full flex justify-center hidden" :aria-hidden true
|
(into [:div#modal-content]
|
||||||
"_" (hiccup/raw "on closeModal transition <#modal-holder .modal-content /> opacity to 0.0 over 300ms then call hideModal() ")}
|
children)]
|
||||||
[:div {:class (str "relative w-full max-h-full " (or (:modal-class params) " max-w-2xl "))}
|
)
|
||||||
(into [:div#modal-content]
|
|
||||||
children)]
|
|
||||||
]
|
|
||||||
[:script {:lang "text/javascript"}
|
|
||||||
(hiccup/raw "
|
|
||||||
var modal_element = document.getElementById('modal-holder');
|
|
||||||
var modal_options = {
|
|
||||||
placement: 'center',
|
|
||||||
backdrop: 'dynamic',
|
|
||||||
backdropClasess: 'bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40',
|
|
||||||
closable: true,
|
|
||||||
onOpen: function() {
|
|
||||||
modal_element.dispatchEvent('openModal');
|
|
||||||
|
|
||||||
},
|
|
||||||
onHide: function() {
|
|
||||||
modal_element.outerHTML='<div id=\"modal-holder\"><div class=\"relative w-full max-w-2xl max-h-full\"><div id=\"modal-content\"></div></div></div>';
|
|
||||||
},
|
|
||||||
};
|
|
||||||
var curModal = new Modal(modal_element, modal_options);
|
|
||||||
curModal.show();
|
|
||||||
function hideModal() {
|
|
||||||
curModal.hide();
|
|
||||||
}
|
|
||||||
")
|
|
||||||
|
|
||||||
]])
|
|
||||||
|
|
||||||
(defn modal-card- [params header content footer]
|
(defn modal-card- [params header content footer]
|
||||||
[:div#modal-card params
|
[:div#modal-card params
|
||||||
[:div {:class "relative bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white fade-in slide-up duration-300 transition-all modal-content"}
|
[:div {:class "relative bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white fade-in-settle slide-up-settle duration-300 transition-all modal-content"}
|
||||||
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"} header]
|
[:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600"} header]
|
||||||
[:div {:class "p-6 space-y-6"}
|
[:div {:class "p-6 space-y-6"}
|
||||||
content]
|
content]
|
||||||
[:div footer]]
|
[:div footer]]])
|
||||||
])
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
(ns auto-ap.ssr.components.inputs
|
(ns auto-ap.ssr.components.inputs
|
||||||
(:require
|
(:require
|
||||||
[hiccup2.core :as hiccup]))
|
[hiccup2.core :as hiccup]
|
||||||
|
[clojure.string :as str]))
|
||||||
|
|
||||||
|
|
||||||
(defn select- [params & children]
|
(defn select- [params & children]
|
||||||
@@ -22,22 +23,24 @@
|
|||||||
[:select (-> params
|
[:select (-> params
|
||||||
(dissoc :url)
|
(dissoc :url)
|
||||||
(dissoc :value)
|
(dissoc :value)
|
||||||
)
|
(dissoc :value-fn)
|
||||||
(for [[k v] (if (:multiple params)
|
(dissoc :content-fn))
|
||||||
(:value params)
|
(for [value (if (:multiple params)
|
||||||
|
(:value params)
|
||||||
[(:value params)])
|
[(:value params)])
|
||||||
:when k]
|
:when ((:value-fn params first) value)]
|
||||||
[:option {:value k :selected true} v]
|
[:option {:value ((:value-fn params first) value) :selected true} ((:content-fn params second) value)])
|
||||||
)
|
|
||||||
|
|
||||||
[:script {:lang "javascript"}
|
[:script {:lang "javascript"}
|
||||||
(hiccup/raw (format "
|
(hiccup/raw (format "
|
||||||
(function () {
|
(function () {
|
||||||
var element = document.getElementById('%s');
|
var element = document.getElementById('%s');
|
||||||
var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3, searchPlaceholderValue: '%s'});
|
var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3, searchPlaceholderValue: '%s'});
|
||||||
|
let baseUrl = '%s';
|
||||||
|
|
||||||
element.addEventListener('search', function (e) {
|
element.addEventListener('search', function (e) {
|
||||||
let data = fetch('%s?q=' + e.detail.value)
|
let fullUrl = baseUrl + (baseUrl.includes(\"?\") ? \"&\" : \"?\") + \"q=\" + e.detail.value;
|
||||||
|
let data = fetch(fullUrl)
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
.then(data => {
|
.then(data => {
|
||||||
c.setChoices(data, 'value', 'label', true)
|
c.setChoices(data, 'value', 'label', true)
|
||||||
@@ -77,6 +80,17 @@ c.clearChoices();
|
|||||||
(update :class #(str % (use-size size)))
|
(update :class #(str % (use-size size)))
|
||||||
(assoc :type "number"
|
(assoc :type "number"
|
||||||
:step "0.01")
|
:step "0.01")
|
||||||
|
(dissoc :size))])
|
||||||
|
|
||||||
|
(defn int-input- [{:keys [size] :as params}]
|
||||||
|
[:input
|
||||||
|
(-> params
|
||||||
|
(update
|
||||||
|
:class str " bg-gray-50 border border-gray-300 text-gray-900 rounded-lg focus:ring-blue-500 focus:border-blue-500 block dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 text-right appearance-none"
|
||||||
|
)
|
||||||
|
(update :class #(str % (use-size size)))
|
||||||
|
(assoc :type "number"
|
||||||
|
:step "1")
|
||||||
(dissoc :size))
|
(dissoc :size))
|
||||||
])
|
])
|
||||||
|
|
||||||
@@ -93,12 +107,21 @@ c.clearChoices();
|
|||||||
(update :class #(str % (use-size size)))
|
(update :class #(str % (use-size size)))
|
||||||
(dissoc :size))]])
|
(dissoc :size))]])
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
(defn field-errors- [{:keys [source key]} & rest]
|
||||||
|
(if-let [errors (:errors (cond-> (meta source)
|
||||||
|
key (get key)))]
|
||||||
|
[:div.text-red-500 (str/join ", " errors)]
|
||||||
|
[:div (hiccup/raw " ")]))
|
||||||
|
|
||||||
(defn field- [params & rest]
|
(defn field- [params & rest]
|
||||||
(into
|
[:div {:id (:id params)}
|
||||||
[:div {:id (:id params)}
|
[:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)]
|
||||||
[:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)]]
|
rest
|
||||||
rest))
|
(when (:error-source params)
|
||||||
|
(field-errors- {:source (:error-source params)
|
||||||
|
:key (:error-key params)}))])
|
||||||
|
|
||||||
(defn hidden- [{:keys [name value]}]
|
(defn hidden- [{:keys [name value]}]
|
||||||
[:input {:type "hidden" :value value :name name}]
|
[:input {:type "hidden" :value value :name name}])
|
||||||
)
|
|
||||||
|
|||||||
@@ -49,4 +49,4 @@
|
|||||||
(into
|
(into
|
||||||
[:div.p-4]
|
[:div.p-4]
|
||||||
children)]]
|
children)]]
|
||||||
[:div#modal-holder]])
|
])
|
||||||
|
|||||||
@@ -1,22 +1,23 @@
|
|||||||
(ns auto-ap.ssr.components.radio)
|
(ns auto-ap.ssr.components.radio)
|
||||||
|
|
||||||
(defn radio- [{:keys [options name title size] :or {size :medium}}]
|
(defn radio- [{:keys [options name title size] :or {size :medium} selected-value :value}]
|
||||||
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
|
[:h3 {:class "mb-4 font-semibold text-gray-900 dark:text-white"} title]
|
||||||
[:ul {:class "w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"}
|
[:ul {:class "w-48 text-sm font-medium text-gray-900 bg-white border border-gray-200 rounded-lg dark:bg-gray-700 dark:border-gray-600 dark:text-white"}
|
||||||
(for [{:keys [value content]} options]
|
(for [{:keys [value content]} options]
|
||||||
[:li {:class "w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600"}
|
[:li {:class "w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600"}
|
||||||
[:div {:class "flex items-center pl-3"}
|
[:div {:class "flex items-center pl-3"}
|
||||||
[:input {:id (str "list-" name "-" value)
|
[:input (cond-> {:id (str "list-" name "-" value)
|
||||||
:type "radio",
|
:type "radio",
|
||||||
:value value
|
:value value
|
||||||
:name name
|
:name name
|
||||||
:class
|
:class
|
||||||
(cond-> "w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
|
(cond-> "w-4 h-4 text-blue-600 bg-gray-100 border-gray-300 focus:ring-blue-500 dark:focus:ring-blue-600 dark:ring-offset-gray-700 dark:focus:ring-offset-gray-700 focus:ring-2 dark:bg-gray-600 dark:border-gray-500"
|
||||||
(= size :small)
|
(= size :small)
|
||||||
(str " " "text-xs")
|
(str " " "text-xs")
|
||||||
|
|
||||||
(= size :medium)
|
(= size :medium)
|
||||||
(str " " "text-sm"))}]
|
(str " " "text-sm"))}
|
||||||
|
(= (cond-> selected-value (keyword? selected-value) clojure.core/name) value) (assoc :checked true))]
|
||||||
[:label {:for (str "list-" name "-" value)
|
[:label {:for (str "list-" name "-" value)
|
||||||
:class
|
:class
|
||||||
(cond-> "w-full ml-2 font-medium text-gray-900 dark:text-gray-300"
|
(cond-> "w-full ml-2 font-medium text-gray-900 dark:text-gray-300"
|
||||||
|
|||||||
@@ -19,10 +19,13 @@
|
|||||||
[auto-ap.ssr.pos.refunds :as pos-refunds]
|
[auto-ap.ssr.pos.refunds :as pos-refunds]
|
||||||
[auto-ap.ssr.pos.sales-orders :as pos-sales]
|
[auto-ap.ssr.pos.sales-orders :as pos-sales]
|
||||||
[auto-ap.ssr.pos.tenders :as pos-tenders]
|
[auto-ap.ssr.pos.tenders :as pos-tenders]
|
||||||
|
[auto-ap.ssr.admin.transaction-rules :as admin-rules]
|
||||||
|
[auto-ap.ssr.account :as account]
|
||||||
[auto-ap.ssr.search :as search]
|
[auto-ap.ssr.search :as search]
|
||||||
[auto-ap.ssr.transaction.insights :as insights]
|
[auto-ap.ssr.transaction.insights :as insights]
|
||||||
[auto-ap.ssr.users :as users]
|
[auto-ap.ssr.users :as users]
|
||||||
[ring.middleware.json :refer [wrap-json-response]]))
|
[ring.middleware.json :refer [wrap-json-response]]
|
||||||
|
[auto-ap.ssr.vendor :as vendors]))
|
||||||
|
|
||||||
;; from auto-ap.ssr-routes, because they're shared
|
;; from auto-ap.ssr-routes, because they're shared
|
||||||
|
|
||||||
@@ -38,6 +41,10 @@
|
|||||||
(wrap-client-redirect-unauthenticated (wrap-secure (wrap-json-response company-dropdown/dropdown-search-results {})))
|
(wrap-client-redirect-unauthenticated (wrap-secure (wrap-json-response company-dropdown/dropdown-search-results {})))
|
||||||
:company-search
|
:company-search
|
||||||
(wrap-client-redirect-unauthenticated (wrap-secure company/search))
|
(wrap-client-redirect-unauthenticated (wrap-secure company/search))
|
||||||
|
:bank-account-search (wrap-client-redirect-unauthenticated (wrap-secure company/bank-account-search))
|
||||||
|
:account-search (wrap-client-redirect-unauthenticated (wrap-secure account/account-search))
|
||||||
|
:bank-account-typeahead (wrap-client-redirect-unauthenticated (wrap-secure company/bank-account-typeahead))
|
||||||
|
|
||||||
:company (wrap-client-redirect-unauthenticated (wrap-secure company/page))
|
:company (wrap-client-redirect-unauthenticated (wrap-secure company/page))
|
||||||
:company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page))
|
:company-1099 (wrap-client-redirect-unauthenticated (wrap-secure company-1099/page))
|
||||||
:company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table))
|
:company-1099-vendor-table (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-table))
|
||||||
@@ -60,6 +67,7 @@
|
|||||||
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice))
|
:invoice-glimpse-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/textract-invoice))
|
||||||
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice))
|
:invoice-glimpse-create-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/create-invoice))
|
||||||
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice))
|
:invoice-glimpse-update-textract-invoice (wrap-client-redirect-unauthenticated (wrap-admin invoice-glimpse/update-textract-invoice))
|
||||||
|
:vendor-search (wrap-client-redirect-unauthenticated (wrap-secure vendors/search))
|
||||||
:transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page))
|
:transaction-insights (wrap-client-redirect-unauthenticated (wrap-admin insights/page))
|
||||||
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table))
|
:transaction-insight-table (wrap-client-redirect-unauthenticated (wrap-admin insights/insight-table))
|
||||||
:transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows))
|
:transaction-insight-rows (wrap-client-redirect-unauthenticated (wrap-admin insights/transaction-rows))
|
||||||
@@ -75,5 +83,6 @@
|
|||||||
(into pos-refunds/key->handler)
|
(into pos-refunds/key->handler)
|
||||||
(into users/key->handler)
|
(into users/key->handler)
|
||||||
(into admin-accounts/key->handler)
|
(into admin-accounts/key->handler)
|
||||||
(into admin-jobs/key->handler)))
|
(into admin-jobs/key->handler)
|
||||||
|
(into admin-rules/key->handler)))
|
||||||
|
|
||||||
|
|||||||
20
src/clj/auto_ap/ssr/hx.clj
Normal file
20
src/clj/auto_ap/ssr/hx.clj
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
(ns auto-ap.ssr.hx
|
||||||
|
(:require [cheshire.core :as cheshire]
|
||||||
|
[clojure.string :as str]))
|
||||||
|
|
||||||
|
|
||||||
|
(defn vals [m]
|
||||||
|
(cheshire/generate-string m))
|
||||||
|
|
||||||
|
|
||||||
|
(defn json [m]
|
||||||
|
(cheshire/generate-string m))
|
||||||
|
|
||||||
|
(defn trigger-field-change [& {:keys [name
|
||||||
|
from]}]
|
||||||
|
(format "change[target.name==\"%s\"] %s"
|
||||||
|
name
|
||||||
|
(when from (str "from:" from))))
|
||||||
|
|
||||||
|
(defn triggers [& triggers]
|
||||||
|
(str/join ", " triggers))
|
||||||
@@ -42,6 +42,7 @@
|
|||||||
|
|
||||||
[:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}]
|
[:script {:src "https://unpkg.com/dropzone@5.9.3/dist/min/dropzone.min.js"}]
|
||||||
[:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}]
|
[:link {:rel "stylesheet" :href "https://unpkg.com/dropzone@5/dist/min/dropzone.min.css" :type "text/css"}]
|
||||||
|
#_[:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}]
|
||||||
[:style
|
[:style
|
||||||
"
|
"
|
||||||
input::-webkit-outer-spin-button,
|
input::-webkit-outer-spin-button,
|
||||||
@@ -59,4 +60,31 @@ input[type=number] {
|
|||||||
]
|
]
|
||||||
[:body {:hx-ext "disable-submit"}
|
[:body {:hx-ext "disable-submit"}
|
||||||
contents
|
contents
|
||||||
[:script {:src "/js/flowbite.min.js"}]]]))
|
[:script {:src "/js/flowbite.min.js"}]
|
||||||
|
[:div [:div#modal-holder
|
||||||
|
{ :tabindex "-1", :class "fixed top-0 left-0 right-0 z-50 w-full p-4 overflow-x-hidden overflow-y-auto md:inset-0 h-[calc(100%-1rem)] max-h-full flex justify-center hidden"
|
||||||
|
:aria-hidden true
|
||||||
|
"_" (hiccup/raw "on \"modalClosed\" remove my children
|
||||||
|
on \"modalOpening\" from <body /> call curModal.show()
|
||||||
|
on \"modalClosing\" from <body /> call curModal.hide()")
|
||||||
|
}]
|
||||||
|
[:script {:lang "text/javascript"}
|
||||||
|
(hiccup/raw "
|
||||||
|
var modal_element = document.getElementById('modal-holder');
|
||||||
|
var modal_options = {
|
||||||
|
placement: 'center',
|
||||||
|
backdrop: 'dynamic',
|
||||||
|
backdropClasess: 'bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40',
|
||||||
|
closable: true,
|
||||||
|
onOpen: function() {
|
||||||
|
htmx.trigger(document.getElementById('modal-holder'), 'modalOpened', {});
|
||||||
|
|
||||||
|
},
|
||||||
|
onHide: function() {
|
||||||
|
htmx.trigger(document.getElementById('modal-holder'), 'modalClosed', {});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var curModal = new Modal(modal_element, modal_options);
|
||||||
|
")
|
||||||
|
|
||||||
|
]]]]))
|
||||||
|
|||||||
@@ -326,7 +326,9 @@
|
|||||||
:form-schema (mc/schema
|
:form-schema (mc/schema
|
||||||
[:map
|
[:map
|
||||||
[:db/id entity-id]
|
[:db/id entity-id]
|
||||||
[:user/clients (forced-vector entity-id)]
|
[:user/clients {:optional true}
|
||||||
|
[:maybe
|
||||||
|
(forced-vector entity-id)]]
|
||||||
[:user/role (ref->enum-schema "user-role")]])))
|
[:user/role (ref->enum-schema "user-role")]])))
|
||||||
:user-edit-dialog (-> user-edit-dialog
|
:user-edit-dialog (-> user-edit-dialog
|
||||||
(wrap-schema-decode
|
(wrap-schema-decode
|
||||||
|
|||||||
@@ -8,7 +8,8 @@
|
|||||||
[malli.core :as mc]
|
[malli.core :as mc]
|
||||||
[malli.error :as me]
|
[malli.error :as me]
|
||||||
[malli.transform :as mt2]
|
[malli.transform :as mt2]
|
||||||
[slingshot.slingshot :refer [throw+ try+]]))
|
[slingshot.slingshot :refer [throw+ try+]]
|
||||||
|
[manifold.time :as mt]))
|
||||||
|
|
||||||
(defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}]
|
(defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}]
|
||||||
{:status status
|
{:status status
|
||||||
@@ -83,8 +84,55 @@
|
|||||||
)}}
|
)}}
|
||||||
x])
|
x])
|
||||||
|
|
||||||
(def entity-id (mc/schema nat-int?))
|
(defn empty->nil [v]
|
||||||
|
(if (and (string? v) (clojure.string/blank? v))
|
||||||
|
nil
|
||||||
|
v))
|
||||||
|
|
||||||
|
(defn parse-empty-as-nil []
|
||||||
|
(mt2/transformer
|
||||||
|
{:decoders
|
||||||
|
{:double empty->nil
|
||||||
|
:int empty->nil
|
||||||
|
:long empty->nil
|
||||||
|
'nat-int? empty->nil}}))
|
||||||
|
|
||||||
|
(def entity-id (mc/schema [nat-int? {:error/message "required"} ]))
|
||||||
|
|
||||||
(def temp-id (mc/schema :string))
|
(def temp-id (mc/schema :string))
|
||||||
|
(def money (mc/schema [:double]))
|
||||||
|
(def percentage (mc/schema [:double {:decode/arbitrary (fn [x] (some-> x (* 0.01)))
|
||||||
|
:max 1.0
|
||||||
|
:error/message "1-100"}]))
|
||||||
|
|
||||||
|
(def regex (mc/schema [:fn {:error/message "not a regex"}
|
||||||
|
(fn check-regx [x]
|
||||||
|
(try
|
||||||
|
(and (string? x)
|
||||||
|
(. java.util.regex.Pattern (compile x java.util.regex.Pattern/CASE_INSENSITIVE)))
|
||||||
|
true
|
||||||
|
(catch Exception _
|
||||||
|
false)))]))
|
||||||
|
|
||||||
|
(def map->db-id-decoder
|
||||||
|
{:enter (fn [x]
|
||||||
|
(into []
|
||||||
|
(for [[k v] x]
|
||||||
|
(assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k))
|
||||||
|
(Long/parseLong k)
|
||||||
|
(keyword? k)
|
||||||
|
(name k)
|
||||||
|
:else
|
||||||
|
k)))))})
|
||||||
|
|
||||||
|
(defn many-entity [params & keys]
|
||||||
|
(mc/schema
|
||||||
|
[:vector (merge params {:decode/json map->db-id-decoder
|
||||||
|
:decode/arbitrary (fn [x]
|
||||||
|
(if (sequential? x)
|
||||||
|
x
|
||||||
|
[x]))})
|
||||||
|
(into [:map] keys)]))
|
||||||
|
|
||||||
(defn str->keyword [s]
|
(defn str->keyword [s]
|
||||||
(if (string? s)
|
(if (string? s)
|
||||||
@@ -97,65 +145,76 @@
|
|||||||
(defn keyword->str [k]
|
(defn keyword->str [k]
|
||||||
(subs (str k) 1))
|
(subs (str k) 1))
|
||||||
|
|
||||||
(defn validation-error [m & [data]]
|
(defn validation-error [m & {:as data}]
|
||||||
(throw+ (ex-info m (merge data {:type :validation}))))
|
(throw+ (ex-info m (merge data {:type :validation}))))
|
||||||
|
|
||||||
|
(defn field-validation-error [m path & {:as data}]
|
||||||
|
(throw+ (ex-info m (merge data {:type :field-validation
|
||||||
|
:field-validation-errors [{:path path
|
||||||
|
:message [m]}]}))))
|
||||||
|
|
||||||
|
(defn form-validation-error [m & {:as data}]
|
||||||
|
(throw+ (ex-info m (merge data {:type :form-validation
|
||||||
|
:form-validation-errors [m]}))))
|
||||||
|
|
||||||
|
(def main-transformer
|
||||||
|
(mt2/transformer
|
||||||
|
parse-empty-as-nil
|
||||||
|
(mt2/key-transformer {:encode keyword->str :decode str->keyword})
|
||||||
|
mt2/string-transformer
|
||||||
|
mt2/json-transformer
|
||||||
|
(mt2/transformer {:name :arbitrary})))
|
||||||
|
|
||||||
(defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}]
|
(defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}]
|
||||||
(fn [{:keys [form-params query-params params] :as request}]
|
(fn [{:keys [form-params query-params params] :as request}]
|
||||||
(let [request (try
|
(let [request (try
|
||||||
(cond-> request
|
(cond-> request
|
||||||
(and (:params request) params-schema)
|
(and (:params request) params-schema)
|
||||||
(assoc :params
|
(assoc :params
|
||||||
(mc/coerce
|
(mc/coerce
|
||||||
params-schema
|
params-schema
|
||||||
(:params request)
|
(:params request)
|
||||||
(mt2/transformer
|
main-transformer))
|
||||||
(mt2/key-transformer {:encode keyword->str :decode str->keyword})
|
|
||||||
mt2/string-transformer
|
|
||||||
mt2/json-transformer) ))
|
|
||||||
|
|
||||||
(and (:route-params request) route-schema)
|
(and (:route-params request) route-schema)
|
||||||
(assoc :route-params
|
(assoc :route-params
|
||||||
(mc/coerce
|
(mc/coerce
|
||||||
route-schema
|
route-schema
|
||||||
(:route-params request)
|
(:route-params request)
|
||||||
(mt2/transformer
|
main-transformer))
|
||||||
(mt2/key-transformer {:encode keyword->str :decode str->keyword})
|
|
||||||
mt2/string-transformer
|
|
||||||
mt2/json-transformer) ))
|
|
||||||
|
|
||||||
(and form-schema form-params)
|
(and form-schema form-params)
|
||||||
(assoc :form-params
|
(assoc :form-params
|
||||||
(mc/coerce
|
(mc/coerce
|
||||||
form-schema
|
form-schema
|
||||||
form-params
|
form-params
|
||||||
(mt2/transformer
|
main-transformer))
|
||||||
(mt2/key-transformer {:encode keyword->str :decode str->keyword})
|
|
||||||
mt2/string-transformer
|
(and query-schema query-params)
|
||||||
mt2/json-transformer) ))
|
(assoc :query-params
|
||||||
|
(mc/coerce
|
||||||
|
query-schema
|
||||||
|
query-params
|
||||||
|
main-transformer)))
|
||||||
|
(catch Exception e
|
||||||
|
(alog/warn ::validation-error :error e)
|
||||||
|
|
||||||
|
(throw (ex-info (->> (-> e
|
||||||
|
(ex-data )
|
||||||
|
:data
|
||||||
|
:explain
|
||||||
|
(me/humanize {:errors (assoc me/default-errors
|
||||||
|
::mc/missing-key {:error/message {:en "required"}})}))
|
||||||
|
(map (fn [[k v]]
|
||||||
|
(str (if (keyword? k)
|
||||||
|
(name k)
|
||||||
|
k) ": " (str/join ", " v))))
|
||||||
|
(str/join ", "))
|
||||||
|
{:type :schema-validation
|
||||||
|
:decoded (:value (:data (ex-data e)))
|
||||||
|
:error (:data (ex-data e))}))))]
|
||||||
|
(handler request))))
|
||||||
|
|
||||||
(and query-schema query-params)
|
|
||||||
(assoc :parsed-query-params
|
|
||||||
(mc/coerce
|
|
||||||
query-schema
|
|
||||||
query-params
|
|
||||||
(mt2/transformer
|
|
||||||
(mt2/key-transformer {:encode name :decode keyword})
|
|
||||||
mt2/string-transformer
|
|
||||||
mt2/json-transformer) )))
|
|
||||||
(catch Exception e
|
|
||||||
(alog/warn ::validation-error :error e)
|
|
||||||
(validation-error (str/join ", "
|
|
||||||
(->> e
|
|
||||||
(ex-data )
|
|
||||||
:data
|
|
||||||
:explain
|
|
||||||
me/humanize
|
|
||||||
(map (fn [[k v]]
|
|
||||||
(str (if (keyword? k)
|
|
||||||
(name k)
|
|
||||||
k) ": " (str/join ", " v))
|
|
||||||
)))))))] (handler request))))
|
|
||||||
(defn ref->enum-schema [n]
|
(defn ref->enum-schema [n]
|
||||||
(into [:enum {:decode/string #(keyword n %)}]
|
(into [:enum {:decode/string #(keyword n %)}]
|
||||||
(for [{:db/keys [ident]} (all-schema)
|
(for [{:db/keys [ident]} (all-schema)
|
||||||
@@ -170,16 +229,15 @@
|
|||||||
:when (= n (namespace ident))]
|
:when (= n (namespace ident))]
|
||||||
[(name ident) (str/replace (str/capitalize (name ident)) "-" " ")])))
|
[(name ident) (str/replace (str/capitalize (name ident)) "-" " ")])))
|
||||||
|
|
||||||
(def map->db-id-decoder
|
(defn ref->radio-options [n & {:keys [allow-nil?]}]
|
||||||
{:enter (fn [x]
|
(into (if allow-nil?
|
||||||
(into []
|
[{:value nil :content ""}]
|
||||||
(for [[k v] x]
|
[])
|
||||||
(assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k))
|
(for [{:db/keys [ident]} (all-schema)
|
||||||
(Long/parseLong k)
|
:when (= n (namespace ident))]
|
||||||
(keyword? k)
|
{:value (name ident) :content (str/replace (str/capitalize (name ident)) "-" " ")})))
|
||||||
(name k)
|
|
||||||
:else
|
|
||||||
k)))))})
|
|
||||||
|
|
||||||
#_(defn namespaceize-decoder [n]
|
#_(defn namespaceize-decoder [n]
|
||||||
{:exit (fn [m]
|
{:exit (fn [m]
|
||||||
@@ -202,8 +260,48 @@
|
|||||||
(catch [:type :validation] e
|
(catch [:type :validation] e
|
||||||
(alog/warn ::form-4xx :error e)
|
(alog/warn ::form-4xx :error e)
|
||||||
(html-response [:span.error-content.text-red-500 (:message &throw-context)]
|
(html-response [:span.error-content.text-red-500 (:message &throw-context)]
|
||||||
:status 400))))
|
:status 400)))))
|
||||||
)
|
|
||||||
|
(defn assoc-errors-into-meta [entity errors]
|
||||||
|
(reduce
|
||||||
|
(fn add-error [entity {:keys [path message] :as se}]
|
||||||
|
(if (= (count path) 1)
|
||||||
|
(with-meta entity (assoc (meta entity) (last path) {:errors message}))
|
||||||
|
|
||||||
|
(update-in entity (butlast path)
|
||||||
|
(fn [terminal]
|
||||||
|
(with-meta terminal (assoc (meta terminal) (last path) {:errors message}))))))
|
||||||
|
entity
|
||||||
|
errors))
|
||||||
|
|
||||||
|
(defn wrap-form-4xx-2 [handler form-handler]
|
||||||
|
(fn [request]
|
||||||
|
(try+
|
||||||
|
(handler request)
|
||||||
|
(catch [:type :schema-validation] e
|
||||||
|
|
||||||
|
(let [humanized (-> e :error :explain (me/humanize {:errors (assoc me/default-errors
|
||||||
|
::mc/missing-key {:error/message {:en "required"}})}))
|
||||||
|
errors (map
|
||||||
|
(fn [e]
|
||||||
|
{:path (:in e)
|
||||||
|
:message (get-in humanized (:in e))})
|
||||||
|
(:errors (:explain (:error e))))]
|
||||||
|
(alog/warn ::form-4xx :errors errors)
|
||||||
|
(form-handler (assoc request
|
||||||
|
:last-form (assoc-errors-into-meta (:decoded e) errors)
|
||||||
|
:field-validation-errors errors)))
|
||||||
|
#_(html-response [:span.error-content.text-red-500 (:message &throw-context)]
|
||||||
|
:status 400))
|
||||||
|
(catch [:type :field-validation] e
|
||||||
|
(form-handler (assoc request
|
||||||
|
:last-form (assoc-errors-into-meta (:form e) (:field-validation-errors e))
|
||||||
|
:field-validation-errors (:field-validation-errors e))))
|
||||||
|
(catch [:type :form-validation] e
|
||||||
|
(form-handler (assoc request
|
||||||
|
:last-form (with-meta (:form e) {:errors (:form-validation-errors e)})
|
||||||
|
:form-validation-errors (:form-validation-errors e)))))))
|
||||||
|
|
||||||
|
|
||||||
(defn apply-middleware-to-all-handlers [key->handler f]
|
(defn apply-middleware-to-all-handlers [key->handler f]
|
||||||
(->> key->handler
|
(->> key->handler
|
||||||
@@ -212,3 +310,14 @@
|
|||||||
(assoc key-handler k (f v)))
|
(assoc key-handler k (f v)))
|
||||||
key->handler)
|
key->handler)
|
||||||
))
|
))
|
||||||
|
|
||||||
|
(defn path->name2 [k & rest]
|
||||||
|
(let [k->n (fn [k]
|
||||||
|
(if (keyword? k)
|
||||||
|
(str (namespace k) "/" (name k))
|
||||||
|
k))]
|
||||||
|
(str (k->n k)
|
||||||
|
(str/join ""
|
||||||
|
(map (fn [k]
|
||||||
|
(str "[" (k->n k) "]"))
|
||||||
|
rest)))))
|
||||||
|
|||||||
29
src/clj/auto_ap/ssr/vendor.clj
Normal file
29
src/clj/auto_ap/ssr/vendor.clj
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
(ns auto-ap.ssr.vendor
|
||||||
|
(:require
|
||||||
|
[auto-ap.datomic :refer [conn pull-attr]]
|
||||||
|
[auto-ap.graphql.utils :refer [is-admin?]]
|
||||||
|
[auto-ap.solr :as solr]
|
||||||
|
[clojure.string :as str]
|
||||||
|
[datomic.api :as dc]
|
||||||
|
[ring.middleware.json :refer [wrap-json-response]]))
|
||||||
|
|
||||||
|
(defn search [{:keys [clients query-params identity]}]
|
||||||
|
|
||||||
|
(doto (let [name-like-ids (when (not-empty (get query-params "q"))
|
||||||
|
(set (map (comp #(Long/parseLong %) :id)
|
||||||
|
(doto
|
||||||
|
(solr/query solr/impl "vendors"
|
||||||
|
(doto
|
||||||
|
{"query" (cond-> (format "name:(%s*)" (str/upper-case (solr/escape (get query-params "q"))))
|
||||||
|
(not (is-admin? identity)) (str " hidden:false"))
|
||||||
|
"fields" "id"
|
||||||
|
"limit" 300}
|
||||||
|
clojure.pprint/pprint))
|
||||||
|
clojure.pprint/pprint))))
|
||||||
|
_ (clojure.pprint/pprint name-like-ids)
|
||||||
|
valid-clients (for [n name-like-ids]
|
||||||
|
{"value" n "label" (pull-attr (dc/db conn) :vendor/name n)})]
|
||||||
|
{:body (take 10 valid-clients)})
|
||||||
|
clojure.pprint/pprint))
|
||||||
|
|
||||||
|
(def search (wrap-json-response search))
|
||||||
@@ -62,9 +62,9 @@
|
|||||||
:icon-style {:font-size "25px"}})
|
:icon-style {:font-size "25px"}})
|
||||||
(menu-item {:label "Rules"
|
(menu-item {:label "Rules"
|
||||||
:icon-class "icon icon-cog-play-1"
|
:icon-class "icon icon-cog-play-1"
|
||||||
:test-route #{:admin-rules}
|
:test-route #{:admin-transaction-rules}
|
||||||
:active-route active-route
|
:active-route active-route
|
||||||
:route :admin-rules
|
:route :admin-transaction-rules
|
||||||
:icon-style {:font-size "25px"}})
|
:icon-style {:font-size "25px"}})
|
||||||
(menu-item {:label "History"
|
(menu-item {:label "History"
|
||||||
:icon-class "icon icon-cog-play-1"
|
:icon-class "icon icon-cog-play-1"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
["/" [#"\w+" :textract-invoice-id]] {:get :invoice-glimpse-textract-invoice
|
["/" [#"\w+" :textract-invoice-id]] {:get :invoice-glimpse-textract-invoice
|
||||||
"/create" {:post :invoice-glimpse-create-invoice}
|
"/create" {:post :invoice-glimpse-create-invoice}
|
||||||
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}}
|
"/update" {:patch :invoice-glimpse-update-textract-invoice}}}}}
|
||||||
|
"account" {"/search" {:get :account-search}}
|
||||||
"admin" {"/history" {"" :admin-history
|
"admin" {"/history" {"" :admin-history
|
||||||
"/" :admin-history
|
"/" :admin-history
|
||||||
#"/search/?" :admin-history-search
|
#"/search/?" :admin-history-search
|
||||||
@@ -30,7 +31,17 @@
|
|||||||
"/table" :admin-job-table
|
"/table" :admin-job-table
|
||||||
"/new" {:get :admin-job-start-dialog}
|
"/new" {:get :admin-job-start-dialog}
|
||||||
"/subform" :admin-job-subform}
|
"/subform" :admin-job-subform}
|
||||||
"/ezcater-xls" :admin-ezcater-xls}
|
"/ezcater-xls" :admin-ezcater-xls
|
||||||
|
"/transaction-rule" {"" {:get :admin-transaction-rules
|
||||||
|
:put :admin-transaction-rule-save
|
||||||
|
:post :admin-transaction-rule-save}
|
||||||
|
"/table" :admin-transaction-rule-table
|
||||||
|
"/account/new" :admin-transaction-rule-new-account
|
||||||
|
"/account/location-select" :admin-transaction-rule-location-select
|
||||||
|
"/account/typeahead" :admin-transaction-rule-account-typeahead
|
||||||
|
"/new" {:get :admin-transaction-rule-new-dialog}
|
||||||
|
[[#"\d+" :db/id] "/edit"] :admin-transaction-rule-edit-dialog
|
||||||
|
}}
|
||||||
"transaction" {"/insights" {"" :transaction-insights
|
"transaction" {"/insights" {"" :transaction-insights
|
||||||
"/table" :transaction-insight-table
|
"/table" :transaction-insight-table
|
||||||
["/code/" [#"\d+" :transaction-id]] {:post :transaction-insight-code}
|
["/code/" [#"\d+" :transaction-id]] {:post :transaction-insight-code}
|
||||||
@@ -48,9 +59,12 @@
|
|||||||
"/cash-drawer-shifts" {"" {:get :pos-cash-drawer-shifts}
|
"/cash-drawer-shifts" {"" {:get :pos-cash-drawer-shifts}
|
||||||
"/table" {:get :pos-cash-drawer-shift-table}}}
|
"/table" {:get :pos-cash-drawer-shift-table}}}
|
||||||
|
|
||||||
|
"vendor" {"/search" :vendor-search}
|
||||||
"company" {"" :company
|
"company" {"" :company
|
||||||
"/dropdown" :company-dropdown-search-results
|
"/dropdown" :company-dropdown-search-results
|
||||||
"/search" :company-search
|
"/search" :company-search
|
||||||
|
"/bank-account/typeahead" :bank-account-typeahead
|
||||||
|
["/" [#"\d+" :db/id] "/bank-account"] {"/search" :bank-account-search}
|
||||||
"/active" {:put :active-client}
|
"/active" {:put :active-client}
|
||||||
"/1099" :company-1099
|
"/1099" :company-1099
|
||||||
"/1099/table" {:get :company-1099-vendor-table}
|
"/1099/table" {:get :company-1099-vendor-table}
|
||||||
|
|||||||
Reference in New Issue
Block a user