diff --git a/resources/input.css b/resources/input.css index 921b1ff9..0f061e61 100644 --- a/resources/input.css +++ b/resources/input.css @@ -26,6 +26,29 @@ } + +.htmx-added.swipe-left-swap , .htmx-added .swipe-left-swap{ + @apply opacity-100 !important; + @apply scale-100 !important; + @apply -translate-x-1/2 !important; + +} + +.htmx-added.swipe-left-swap , .htmx-added .swipe-left-swap{ + @apply opacity-100; + @apply scale-100; + @apply translate-x-0; + +} +.htmx-settling.htmx-added .swipe-left-swap, .htmx-settling.htmx-added.swipe-left-swap{ + + @apply opacity-0 !important; + @apply scale-75 !important; + @apply translate-x-1/2 !important; + +} + + .htmx-settling .slide-up-settle { @apply translate-y-5 !important; } @@ -165,3 +188,26 @@ .choices[data-type*="select-one"] .choices__button { right:auto !important; } + +.arrow, +.arrow::before { + position: absolute; + width: 24px; + height: 24px; + background: inherit; +} + +.arrow { + visibility: hidden; +} + + +.arrow::before { + visibility: visible; + content: ''; + transform: rotate(45deg); +} + +.arrow { + bottom: -4px; +} diff --git a/resources/public/output.css b/resources/public/output.css index 7dbdaa67..269994ff 100644 --- a/resources/public/output.css +++ b/resources/public/output.css @@ -1 +1 @@ -/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Calibri,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;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}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#007dbb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{color:#fff;background:#1f2937;border:0;font-weight:500;font-size:.875rem;cursor:pointer;padding:.625rem 1rem .625rem 2rem;-webkit-margin-start:-1rem;margin-inline-start:-1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}input[type=file]::file-selector-button:hover{background:#374151}.dark input[type=file]::file-selector-button{color:#fff;background:#4b5563}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{outline:2px solid #0000;outline-offset:2px;--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(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}input[type=range]::-moz-range-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#009cea}input[type=range]::-ms-fill-lower{background:#009cea}.toggle-bg:after{content:"";position:absolute;top:.125rem;left:.125rem;background:#fff;border-color:#d1d5db;border-width:1px;border-radius:9999px;height:1.25rem;width:1.25rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}input:checked+.toggle-bg:after{transform:translateX(100%);;border-color:#fff}input:checked+.toggle-bg{background:#007dbb;border-color:#007dbb}.tooltip-arrow,.tooltip-arrow:before{position:absolute;width:8px;height:8px;background:inherit}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";visibility:visible;transform:rotate(45deg)}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-style:solid;border-color:#e5e7eb}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-top-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-top-width:1px;border-right-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{position:absolute;width:8px;height:8px;background:inherit}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";visibility:visible;transform:rotate(45deg)}[data-popper-arrow]:after{position:absolute;width:9px;height:9px;background:inherit}[role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#4b5563}[role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#4b5563}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-top-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-top-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#009cea80;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-2{top:.5rem}.top-2\/4{top:50%}.top-5{top:1.25rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-start-1{grid-column-start:1}.m-4{margin:1rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-1{margin-bottom:-.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-10{margin-right:2.5rem}.mr-16{margin-right:4rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-0{margin-top:0}.mt-0\.5{margin-top:.125rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-3{height:.75rem}.h-3\.5{height:.875rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-8{height:2rem}.h-96{height:24rem}.h-\[calc\(100\%-1rem\)\]{height:calc(100% - 1rem)}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.max-h-full{max-height:100%}.w-1\/2{width:50%}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-3\.5{width:.875rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-full{width:100%}.max-w-2xl{max-width:42rem}.max-w-4xl{max-width:56rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.basis-1\/4{flex-basis:25%}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{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))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-full{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))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{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))}.translate-x-full{--tw-translate-x:100%}.translate-y-full{--tw-translate-y:100%}.rotate-180,.translate-y-full{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))}.rotate-180{--tw-rotate:180deg}.transform{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))}.transform-none{transform:none}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-x-hidden{overflow-x:hidden}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-l-lg{border-top-left-radius:.5rem;border-bottom-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-t{border-top-width:1px}.border-dashed{border-style:dashed}.border-blue-300{--tw-border-opacity:1;border-color:rgb(102 196 242/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(0 94 140/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-primary-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(255 104 104/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(153 215 247/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(230 245 253/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(0 156 234/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(148 196 88/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(48 72 18/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(255 205 205/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(255 154 154/var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.bg-opacity-50{--tw-bg-opacity:0.5}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.p-6{padding:1.5rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-5{padding-top:1.25rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:initial}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(0 62 94/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(204 2 2/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(102 1 1/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-lg{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-md,.shadow-sm{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color)}.outline{outline-style:solid}.ring{--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(3px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-75{transition-duration:75ms}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .fade-in,.htmx-added.fade-in{opacity:0!important}.fade-in{opacity:1}.htmx-settling .fade-in-settle,.htmx-settling.fade-in-settle{opacity:0!important}.fade-in-settle{opacity:1}.htmx-settling .slide-up-settle,.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))}.hidden .slide-up,.htmx-added .slide-up{--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{--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))}.live-added{animation:pulse-green .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-added{animation:pulse-dark-green .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}@keyframes pulse-green{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-green{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:inherit!important}.htmx-indicator{display:none}.htmx-request .htmx-indicator-hidden{display:none!important}.htmx-indicator-hidden{display:inherit}.htmx-swapping .fade-out{opacity:0!important}.fade-out{opacity:1}.min-h-content{min-height:calc(100vh - 4em)}.choices{margin-bottom:0!important;border-width:0!important}.choices__inner{display:block!important;width:100%!important;border-radius:.5rem!important;border-width:1px!important;--tw-border-opacity:1!important;border-color:rgb(209 213 219/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important;padding:.25rem!important;font-size:.875rem!important;line-height:1.25rem!important;--tw-text-opacity:1!important;color:rgb(17 24 39/var(--tw-text-opacity))!important}.choices__inner:focus{--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}.group.has-error .choices__inner{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(255 230 230/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(51 1 1/var(--tw-text-opacity))!important}.group.has-error .choices__inner::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner::placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner:focus{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))!important}:is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(75 85 99/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}:is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner:focus){--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}.group.has-error :is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 3 3/var(--tw-text-opacity))!important}.group.has-error :is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.group.has-error :is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.choices:focus-within .choices__inner,: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 #0000!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{margin:0!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important}:is(.dark .choices__inner .choices__input){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}.choices__inner .choices__item{white-space:nowrap!important;border-radius:.25rem!important;--tw-border-opacity:1!important;border-color:rgb(156 163 175/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;padding:.125rem .5rem!important;font-size:.75rem!important;line-height:1rem!important;font-weight:500!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__inner .choices__item){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices__list--dropdown{border-radius:.5rem!important;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a!important;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)!important;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)!important}:is(.dark .choices__list--dropdown){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important}.choices__list--dropdown .choices__item--selectable.is-highlighted{--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__list--dropdown .choices__item--selectable.is-highlighted){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices[data-type*=select-multiple] .choices__button{--tw-border-opacity:1!important;border-color:rgb(107 114 128/var(--tw-border-opacity))!important}.choices[data-type*=select-multiple] .choices__button:focus{--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__inner .choices__item:focus-within{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(121 181 46/var(--tw-bg-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices__list--single .choices__item{display:flex!important;width:auto!important}.choices__list--single{width:auto!important}.choices__list--single button{position:relative!important;margin:0!important;display:block!important;height:auto!important}.choices[data-type*=select-one] .choices__button{right:auto!important}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{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))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.hover\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.focus\:text-green-700:focus{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--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(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(153 215 247/var(--tw-ring-opacity))}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-gray-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.focus\:ring-green-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(201 225 171/var(--tw-ring-opacity))}.focus\:ring-green-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(175 211 130/var(--tw-ring-opacity))}.focus\:ring-green-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(73 109 28/var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 154 154/var(--tw-ring-opacity))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;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))}.group:hover .group-hover\:text-blue-500{--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}.group:hover .group-hover\:text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:border-red-500{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))}:is(.dark .dark\:block){display:block}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(75 85 99/var(--tw-divide-opacity))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-700){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity:1;background-color:rgb(0 31 47/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-600){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-700){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-900){--tw-bg-opacity:1;background-color:rgb(24 36 9/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-700){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity:1;background-color:rgb(51 1 1/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(99 49 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-80){--tw-bg-opacity:0.8}:is(.dark .dark\:text-blue-200){--tw-text-opacity:1;color:rgb(153 215 247/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-300){--tw-text-opacity:1;color:rgb(102 196 242/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-400){--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-50){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity:1;color:rgb(255 53 53/var(--tw-text-opacity))}:is(.dark .dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity:1;color:rgb(250 202 21/var(--tw-text-opacity))}:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:placeholder-gray-400)::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:ring-offset-gray-700){--tw-ring-offset-color:#374151}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .dark\:hover\:bg-blue-600:hover){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-600:hover){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-700:hover){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-600:hover){--tw-bg-opacity:1;background-color:rgb(204 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-blue-500:focus){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-500:focus){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:focus\:text-white:focus){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-blue-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 125 187/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-gray-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(48 72 18/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(97 145 37/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-offset-gray-700:focus){--tw-ring-offset-color:#374151}:is(.dark .group:hover .dark\:group-hover\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:border-red-500){--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}@media (min-width:640px){.sm\:block{display:block}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:inset-0{inset:0}.md\:ml-2{margin-left:.5rem}.md\:mr-24{margin-right:6rem}.md\:table-cell{display:table-cell}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}}@media (min-width:1024px){.lg\:block{display:block}.lg\:flex{display:flex}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:w-96{width:24rem}.lg\:translate-x-0{--tw-translate-x: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))}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-3{padding-left:.75rem}.lg\:pl-64{padding-left:16rem}} \ No newline at end of file +/*! tailwindcss v3.3.2 | MIT License | https://tailwindcss.com*/*,:after,:before{box-sizing:border-box;border:0 solid #e5e7eb}:after,:before{--tw-content:""}html{line-height:1.5;-webkit-text-size-adjust:100%;-moz-tab-size:4;-o-tab-size:4;tab-size:4;font-family:Calibri,ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji;font-feature-settings:normal;font-variation-settings:normal}body{margin:0;line-height:inherit}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,pre,samp{font-family:ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,Liberation Mono,Courier New,monospace;font-size:1em}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:initial}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}button,input,optgroup,select,textarea{font-family:inherit;font-size:100%;font-weight:inherit;line-height:inherit;color:inherit;margin:0;padding:0}button,select{text-transform:none}[type=button],[type=reset],[type=submit],button{-webkit-appearance:button;background-color:initial;background-image:none}:-moz-focusring{outline:auto}:-moz-ui-invalid{box-shadow:none}progress{vertical-align:initial}::-webkit-inner-spin-button,::-webkit-outer-spin-button{height:auto}[type=search]{-webkit-appearance:textfield;outline-offset:-2px}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-file-upload-button{-webkit-appearance:button;font:inherit}summary{display:list-item}blockquote,dd,dl,figure,h1,h2,h3,h4,h5,h6,hr,p,pre{margin:0}fieldset{margin:0}fieldset,legend{padding:0}menu,ol,ul{list-style:none;margin:0;padding:0}textarea{resize:vertical}input::-moz-placeholder,textarea::-moz-placeholder{opacity:1;color:#9ca3af}input::placeholder,textarea::placeholder{opacity:1;color:#9ca3af}[role=button],button{cursor:pointer}:disabled{cursor:default}audio,canvas,embed,iframe,img,object,svg,video{display:block;vertical-align:middle}img,video{max-width:100%;height:auto}[hidden]{display:none}[multiple],[type=date],[type=datetime-local],[type=email],[type=month],[type=number],[type=password],[type=search],[type=tel],[type=text],[type=time],[type=url],[type=week],select,textarea{-webkit-appearance:none;-moz-appearance:none;appearance:none;background-color:#fff;border-color:#6b7280;border-width:1px;border-radius:0;padding:.5rem .75rem;font-size:1rem;line-height:1.5rem;--tw-shadow:0 0 #0000}[multiple]:focus,[type=date]:focus,[type=datetime-local]:focus,[type=email]:focus,[type=month]:focus,[type=number]:focus,[type=password]:focus,[type=search]:focus,[type=tel]:focus,[type=text]:focus,[type=time]:focus,[type=url]:focus,[type=week]:focus,select:focus,textarea:focus{outline:2px solid #0000;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}input::-moz-placeholder,textarea::-moz-placeholder{color:#6b7280;opacity:1}input::placeholder,textarea::placeholder{color:#6b7280;opacity:1}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-date-and-time-value{min-height:1.5em}select:not([size]){background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 20 20'%3E%3Cpath stroke='%236B7280' stroke-linecap='round' stroke-linejoin='round' stroke-width='1.5' d='m6 8 4 4 4-4'/%3E%3C/svg%3E");background-position:right .5rem center;background-repeat:no-repeat;background-size:1.5em 1.5em;padding-right:2.5rem;-webkit-print-color-adjust:exact;print-color-adjust:exact}[multiple]{background-image:none;background-position:0 0;background-repeat:unset;background-size:initial;padding-right:.75rem;-webkit-print-color-adjust:unset;print-color-adjust:unset}[type=checkbox],[type=radio]{-webkit-appearance:none;-moz-appearance:none;appearance:none;padding:0;-webkit-print-color-adjust:exact;print-color-adjust:exact;display:inline-block;vertical-align:middle;background-origin:border-box;-webkit-user-select:none;-moz-user-select:none;user-select:none;flex-shrink:0;height:1rem;width:1rem;color:#007dbb;background-color:#fff;border-color:#6b7280;border-width:1px;--tw-shadow:0 0 #0000}[type=checkbox]{border-radius:0}[type=radio]{border-radius:100%}[type=checkbox]:focus,[type=radio]:focus{outline:2px solid #0000;outline-offset:2px;--tw-ring-inset:var(--tw-empty,/*!*/ /*!*/);--tw-ring-offset-width:2px;--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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow)}.dark [type=checkbox]:checked,.dark [type=radio]:checked,[type=checkbox]:checked,[type=radio]:checked{border-color:#0000;background-color:currentColor;background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Cpath d='M12.207 4.793a1 1 0 0 1 0 1.414l-5 5a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L6.5 9.086l4.293-4.293a1 1 0 0 1 1.414 0z'/%3E%3C/svg%3E")}[type=radio]:checked{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='%23fff' viewBox='0 0 16 16'%3E%3Ccircle cx='8' cy='8' r='3'/%3E%3C/svg%3E")}[type=checkbox]:indeterminate{background-image:url("data:image/svg+xml;charset=utf-8,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='none' viewBox='0 0 16 16'%3E%3Cpath stroke='%23fff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M4 8h8'/%3E%3C/svg%3E");background-size:100% 100%;background-position:50%;background-repeat:no-repeat}[type=checkbox]:indeterminate,[type=checkbox]:indeterminate:focus,[type=checkbox]:indeterminate:hover{border-color:#0000;background-color:currentColor}[type=file]{background:unset;border-color:inherit;border-width:0;border-radius:0;padding:0;font-size:unset;line-height:inherit}[type=file]:focus{outline:1px auto inherit}input[type=file]::file-selector-button{color:#fff;background:#1f2937;border:0;font-weight:500;font-size:.875rem;cursor:pointer;padding:.625rem 1rem .625rem 2rem;-webkit-margin-start:-1rem;margin-inline-start:-1rem;-webkit-margin-end:1rem;margin-inline-end:1rem}input[type=file]::file-selector-button:hover{background:#374151}.dark input[type=file]::file-selector-button{color:#fff;background:#4b5563}.dark input[type=file]::file-selector-button:hover{background:#6b7280}input[type=range]::-webkit-slider-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-webkit-slider-thumb{background:#9ca3af}.dark input[type=range]:disabled::-webkit-slider-thumb{background:#6b7280}input[type=range]:focus::-webkit-slider-thumb{outline:2px solid #0000;outline-offset:2px;--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(4px + var(--tw-ring-offset-width)) var(--tw-ring-color);box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000);--tw-ring-opacity:1px;--tw-ring-color:rgb(164 202 254/var(--tw-ring-opacity))}input[type=range]::-moz-range-thumb{height:1.25rem;width:1.25rem;background:#007dbb;border-radius:9999px;border:0;appearance:none;-moz-appearance:none;-webkit-appearance:none;cursor:pointer}input[type=range]:disabled::-moz-range-thumb{background:#9ca3af}.dark input[type=range]:disabled::-moz-range-thumb{background:#6b7280}input[type=range]::-moz-range-progress{background:#009cea}input[type=range]::-ms-fill-lower{background:#009cea}.toggle-bg:after{content:"";position:absolute;top:.125rem;left:.125rem;background:#fff;border-color:#d1d5db;border-width:1px;border-radius:9999px;height:1.25rem;width:1.25rem;transition-property:background-color,border-color,color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-duration:.15s;box-shadow:var(--tw-ring-inset) 0 0 0 calc(var(--tw-ring-offset-width)) var(--tw-ring-color)}input:checked+.toggle-bg:after{transform:translateX(100%);;border-color:#fff}input:checked+.toggle-bg{background:#007dbb;border-color:#007dbb}.tooltip-arrow,.tooltip-arrow:before{position:absolute;width:8px;height:8px;background:inherit}.tooltip-arrow{visibility:hidden}.tooltip-arrow:before{content:"";visibility:visible;transform:rotate(45deg)}[data-tooltip-style^=light]+.tooltip>.tooltip-arrow:before{border-style:solid;border-color:#e5e7eb}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=top]>.tooltip-arrow:before{border-bottom-width:1px;border-right-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=right]>.tooltip-arrow:before{border-bottom-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=bottom]>.tooltip-arrow:before{border-top-width:1px;border-left-width:1px}[data-tooltip-style^=light]+.tooltip[data-popper-placement^=left]>.tooltip-arrow:before{border-top-width:1px;border-right-width:1px}.tooltip[data-popper-placement^=top]>.tooltip-arrow{bottom:-4px}.tooltip[data-popper-placement^=bottom]>.tooltip-arrow{top:-4px}.tooltip[data-popper-placement^=left]>.tooltip-arrow{right:-4px}.tooltip[data-popper-placement^=right]>.tooltip-arrow{left:-4px}.tooltip.invisible>.tooltip-arrow:before{visibility:hidden}[data-popper-arrow],[data-popper-arrow]:before{position:absolute;width:8px;height:8px;background:inherit}[data-popper-arrow]{visibility:hidden}[data-popper-arrow]:after,[data-popper-arrow]:before{content:"";visibility:visible;transform:rotate(45deg)}[data-popper-arrow]:after{position:absolute;width:9px;height:9px;background:inherit}[role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:before{border-style:solid;border-color:#4b5563}[role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#e5e7eb}.dark [role=tooltip]>[data-popper-arrow]:after{border-style:solid;border-color:#4b5563}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]:before{border-bottom-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]:before{border-bottom-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]:before{border-top-width:1px;border-left-width:1px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:after,[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]:before{border-top-width:1px;border-right-width:1px}[data-popover][role=tooltip][data-popper-placement^=top]>[data-popper-arrow]{bottom:-5px}[data-popover][role=tooltip][data-popper-placement^=bottom]>[data-popper-arrow]{top:-5px}[data-popover][role=tooltip][data-popper-placement^=left]>[data-popper-arrow]{right:-5px}[data-popover][role=tooltip][data-popper-placement^=right]>[data-popper-arrow]{left:-5px}[role=tooltip].invisible>[data-popper-arrow]:after,[role=tooltip].invisible>[data-popper-arrow]:before{visibility:hidden}*,::backdrop,:after,:before{--tw-border-spacing-x:0;--tw-border-spacing-y:0;--tw-translate-x:0;--tw-translate-y:0;--tw-rotate:0;--tw-skew-x:0;--tw-skew-y:0;--tw-scale-x:1;--tw-scale-y:1;--tw-pan-x: ;--tw-pan-y: ;--tw-pinch-zoom: ;--tw-scroll-snap-strictness:proximity;--tw-gradient-from-position: ;--tw-gradient-via-position: ;--tw-gradient-to-position: ;--tw-ordinal: ;--tw-slashed-zero: ;--tw-numeric-figure: ;--tw-numeric-spacing: ;--tw-numeric-fraction: ;--tw-ring-inset: ;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-color:#009cea80;--tw-ring-offset-shadow:0 0 #0000;--tw-ring-shadow:0 0 #0000;--tw-shadow:0 0 #0000;--tw-shadow-colored:0 0 #0000;--tw-blur: ;--tw-brightness: ;--tw-contrast: ;--tw-grayscale: ;--tw-hue-rotate: ;--tw-invert: ;--tw-saturate: ;--tw-sepia: ;--tw-drop-shadow: ;--tw-backdrop-blur: ;--tw-backdrop-brightness: ;--tw-backdrop-contrast: ;--tw-backdrop-grayscale: ;--tw-backdrop-hue-rotate: ;--tw-backdrop-invert: ;--tw-backdrop-opacity: ;--tw-backdrop-saturate: ;--tw-backdrop-sepia: }.container{width:100%}@media (min-width:640px){.container{max-width:640px}}@media (min-width:768px){.container{max-width:768px}}@media (min-width:1024px){.container{max-width:1024px}}@media (min-width:1280px){.container{max-width:1280px}}@media (min-width:1536px){.container{max-width:1536px}}.sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border-width:0}.pointer-events-none{pointer-events:none}.visible{visibility:visible}.invisible{visibility:hidden}.collapse{visibility:collapse}.static{position:static}.fixed{position:fixed}.absolute{position:absolute}.relative{position:relative}.sticky{position:sticky}.inset-0{inset:0}.inset-y-0{top:0;bottom:0}.-right-2{right:-.5rem}.-top-2{top:-.5rem}.bottom-0{bottom:0}.bottom-\[60px\]{bottom:60px}.left-0{left:0}.left-1\/2{left:50%}.right-0{right:0}.right-2{right:.5rem}.top-0{top:0}.top-2{top:.5rem}.top-2\/4{top:50%}.top-5{top:1.25rem}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.z-40{z-index:40}.z-50{z-index:50}.z-\[99\]{z-index:99}.col-span-1{grid-column:span 1/span 1}.col-span-2{grid-column:span 2/span 2}.col-span-3{grid-column:span 3/span 3}.col-span-6{grid-column:span 6/span 6}.col-start-1{grid-column-start:1}.m-1{margin:.25rem}.m-2{margin:.5rem}.m-4{margin:1rem}.mx-2{margin-left:.5rem;margin-right:.5rem}.mx-4{margin-left:1rem;margin-right:1rem}.mx-auto{margin-left:auto;margin-right:auto}.my-0{margin-top:0;margin-bottom:0}.my-4{margin-top:1rem;margin-bottom:1rem}.-mb-1{margin-bottom:-.25rem}.mb-1{margin-bottom:.25rem}.mb-2{margin-bottom:.5rem}.mb-3{margin-bottom:.75rem}.mb-4{margin-bottom:1rem}.ml-1{margin-left:.25rem}.ml-2{margin-left:.5rem}.ml-3{margin-left:.75rem}.ml-4{margin-left:1rem}.ml-auto{margin-left:auto}.mr-1{margin-right:.25rem}.mr-10{margin-right:2.5rem}.mr-16{margin-right:4rem}.mr-2{margin-right:.5rem}.mr-3{margin-right:.75rem}.mt-1{margin-top:.25rem}.mt-2{margin-top:.5rem}.mt-4{margin-top:1rem}.mt-5{margin-top:1.25rem}.block{display:block}.inline-block{display:inline-block}.inline{display:inline}.flex{display:flex}.inline-flex{display:inline-flex}.table{display:table}.grid{display:grid}.contents{display:contents}.hidden{display:none}.h-10{height:2.5rem}.h-3{height:.75rem}.h-4{height:1rem}.h-48{height:12rem}.h-5{height:1.25rem}.h-6{height:1.5rem}.h-64{height:16rem}.h-8{height:2rem}.h-full{height:100%}.h-screen{height:100vh}.max-h-96{max-height:24rem}.max-h-full{max-height:100%}.w-1\/2{width:50%}.w-16{width:4rem}.w-24{width:6rem}.w-3{width:.75rem}.w-32{width:8rem}.w-36{width:9rem}.w-4{width:1rem}.w-48{width:12rem}.w-5{width:1.25rem}.w-6{width:1.5rem}.w-64{width:16rem}.w-8{width:2rem}.w-96{width:24rem}.w-auto{width:auto}.w-full{width:100%}.w-max{width:-moz-max-content;width:max-content}.w-screen{width:100vw}.min-w-\[700px\]{min-width:700px}.max-w-2xl{max-width:42rem}.max-w-6xl{max-width:72rem}.max-w-lg{max-width:32rem}.max-w-md{max-width:28rem}.max-w-screen-2xl{max-width:1536px}.max-w-screen-lg{max-width:1024px}.flex-1{flex:1 1 0%}.flex-shrink{flex-shrink:1}.flex-shrink-0{flex-shrink:0}.shrink{flex-shrink:1}.shrink-0{flex-shrink:0}.flex-grow{flex-grow:1}.basis-1\/4{flex-basis:25%}.\!translate-y-0{--tw-translate-y:0px!important}.\!translate-y-0,.\!translate-y-32{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}.\!translate-y-32{--tw-translate-y:8rem!important}.-translate-x-1\/2{--tw-translate-x:-50%}.-translate-x-1\/2,.-translate-x-full{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))}.-translate-x-full{--tw-translate-x:-100%}.-translate-y-1\/2{--tw-translate-y:-50%}.-translate-y-1\/2,.-translate-y-full{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))}.-translate-y-full{--tw-translate-y:-100%}.translate-x-0{--tw-translate-x:0px}.translate-x-0,.translate-x-full{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))}.translate-x-full{--tw-translate-x:100%}.translate-y-full{--tw-translate-y:100%}.rotate-180,.translate-y-full{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))}.rotate-180{--tw-rotate:180deg}.transform{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))}.transform-none{transform:none}@keyframes pulse{50%{opacity:.5}}.animate-pulse{animation:pulse 2s cubic-bezier(.4,0,.6,1) infinite}@keyframes shake{0%{transform:translateX(0)}12.5%{transform:translateX(-5px)}25%{transform:translateX(0)}37.5%{transform:translateX(5px)}50%{transform:translateX(0)}62.5%{transform:translateX(-5px)}75%{transform:translateX(5px)}87.5%{transform:translateX(5px)}to{transform:translateX(0)}}.animate-shake{animation:shake .5s ease-out 1}@keyframes spin{to{transform:rotate(1turn)}}.animate-spin{animation:spin 1s linear infinite}.cursor-default{cursor:default}.cursor-not-allowed{cursor:not-allowed}.cursor-pointer{cursor:pointer}.resize{resize:both}.list-none{list-style-type:none}.appearance-none{-webkit-appearance:none;-moz-appearance:none;appearance:none}.grid-cols-3{grid-template-columns:repeat(3,minmax(0,1fr))}.grid-cols-4{grid-template-columns:repeat(4,minmax(0,1fr))}.grid-cols-6{grid-template-columns:repeat(6,minmax(0,1fr))}.grid-cols-7{grid-template-columns:repeat(7,minmax(0,1fr))}.flex-row{flex-direction:row}.flex-row-reverse{flex-direction:row-reverse}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.place-items-center{place-items:center}.items-start{align-items:flex-start}.items-end{align-items:flex-end}.items-center{align-items:center}.items-baseline{align-items:baseline}.items-stretch{align-items:stretch}.justify-start{justify-content:flex-start}.justify-end{justify-content:flex-end}.justify-center{justify-content:center}.justify-between{justify-content:space-between}.justify-stretch{justify-content:stretch}.justify-items-stretch{justify-items:stretch}.gap-1{gap:.25rem}.gap-2{gap:.5rem}.gap-4{gap:1rem}.gap-8{gap:2rem}.gap-x-4{-moz-column-gap:1rem;column-gap:1rem}.gap-y-2{row-gap:.5rem}.-space-x-px>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(-1px*var(--tw-space-x-reverse));margin-left:calc(-1px*(1 - var(--tw-space-x-reverse)))}.space-x-1>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.25rem*var(--tw-space-x-reverse));margin-left:calc(.25rem*(1 - var(--tw-space-x-reverse)))}.space-x-2>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.5rem*var(--tw-space-x-reverse));margin-left:calc(.5rem*(1 - var(--tw-space-x-reverse)))}.space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.space-y-1>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.25rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.25rem*var(--tw-space-y-reverse))}.space-y-1\.5>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.375rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.375rem*var(--tw-space-y-reverse))}.space-y-2>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.5rem*var(--tw-space-y-reverse))}.space-y-3>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(.75rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(.75rem*var(--tw-space-y-reverse))}.space-y-4>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1rem*var(--tw-space-y-reverse))}.space-y-6>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(1.5rem*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(1.5rem*var(--tw-space-y-reverse))}.divide-y>:not([hidden])~:not([hidden]){--tw-divide-y-reverse:0;border-top-width:calc(1px*(1 - var(--tw-divide-y-reverse)));border-bottom-width:calc(1px*var(--tw-divide-y-reverse))}.divide-gray-100>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(243 244 246/var(--tw-divide-opacity))}.self-center{align-self:center}.self-stretch{align-self:stretch}.justify-self-end{justify-self:end}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-scroll{overflow:scroll}.overflow-x-auto{overflow-x:auto}.overflow-y-auto{overflow-y:auto}.overflow-y-scroll{overflow-y:scroll}.truncate{overflow:hidden;text-overflow:ellipsis}.truncate,.whitespace-nowrap{white-space:nowrap}.rounded{border-radius:.25rem}.rounded-full{border-radius:9999px}.rounded-lg{border-radius:.5rem}.rounded-md{border-radius:.375rem}.rounded-b-lg{border-bottom-right-radius:.5rem}.rounded-b-lg,.rounded-l-lg{border-bottom-left-radius:.5rem}.rounded-l-lg{border-top-left-radius:.5rem}.rounded-r-lg{border-top-right-radius:.5rem;border-bottom-right-radius:.5rem}.rounded-t{border-top-left-radius:.25rem;border-top-right-radius:.25rem}.rounded-t-lg{border-top-left-radius:.5rem;border-top-right-radius:.5rem}.border{border-width:1px}.border-0{border-width:0}.border-2{border-width:2px}.border-b{border-bottom-width:1px}.border-r{border-right-width:1px}.border-dashed{border-style:dashed}.border-blue-300{--tw-border-opacity:1;border-color:rgb(102 196 242/var(--tw-border-opacity))}.border-blue-600{--tw-border-opacity:1;border-color:rgb(0 125 187/var(--tw-border-opacity))}.border-blue-700{--tw-border-opacity:1;border-color:rgb(0 94 140/var(--tw-border-opacity))}.border-gray-100{--tw-border-opacity:1;border-color:rgb(243 244 246/var(--tw-border-opacity))}.border-gray-200{--tw-border-opacity:1;border-color:rgb(229 231 235/var(--tw-border-opacity))}.border-gray-300{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.border-primary-300{--tw-border-opacity:1;border-color:rgb(175 211 130/var(--tw-border-opacity))}.border-red-300{--tw-border-opacity:1;border-color:rgb(255 104 104/var(--tw-border-opacity))}.border-white{--tw-border-opacity:1;border-color:rgb(255 255 255/var(--tw-border-opacity))}.bg-blue-100{--tw-bg-opacity:1;background-color:rgb(204 235 251/var(--tw-bg-opacity))}.bg-blue-200{--tw-bg-opacity:1;background-color:rgb(153 215 247/var(--tw-bg-opacity))}.bg-blue-300{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.bg-blue-400{--tw-bg-opacity:1;background-color:rgb(51 176 238/var(--tw-bg-opacity))}.bg-blue-50{--tw-bg-opacity:1;background-color:rgb(230 245 253/var(--tw-bg-opacity))}.bg-blue-500{--tw-bg-opacity:1;background-color:rgb(0 156 234/var(--tw-bg-opacity))}.bg-blue-600{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.bg-blue-700{--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}.bg-blue-800{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.bg-gray-100{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.bg-gray-200{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.bg-gray-400{--tw-bg-opacity:1;background-color:rgb(156 163 175/var(--tw-bg-opacity))}.bg-gray-50{--tw-bg-opacity:1;background-color:rgb(249 250 251/var(--tw-bg-opacity))}.bg-gray-800{--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}.bg-gray-900{--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}.bg-green-100{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.bg-green-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-green-300{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.bg-green-400{--tw-bg-opacity:1;background-color:rgb(148 196 88/var(--tw-bg-opacity))}.bg-green-500{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}.bg-green-600{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.bg-green-700{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.bg-green-800{--tw-bg-opacity:1;background-color:rgb(48 72 18/var(--tw-bg-opacity))}.bg-primary-200{--tw-bg-opacity:1;background-color:rgb(201 225 171/var(--tw-bg-opacity))}.bg-primary-50{--tw-bg-opacity:1;background-color:rgb(242 248 234/var(--tw-bg-opacity))}.bg-red-100{--tw-bg-opacity:1;background-color:rgb(255 205 205/var(--tw-bg-opacity))}.bg-red-200{--tw-bg-opacity:1;background-color:rgb(255 154 154/var(--tw-bg-opacity))}.bg-red-300{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.bg-white{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.bg-white\/50{background-color:#ffffff80}.bg-yellow-100{--tw-bg-opacity:1;background-color:rgb(253 246 178/var(--tw-bg-opacity))}.\!bg-opacity-0{--tw-bg-opacity:0!important}.\!bg-opacity-100{--tw-bg-opacity:1!important}.\!bg-opacity-50{--tw-bg-opacity:0.5!important}.bg-opacity-50{--tw-bg-opacity:0.5}.p-1{padding:.25rem}.p-2{padding:.5rem}.p-2\.5{padding:.625rem}.p-3{padding:.75rem}.p-4{padding:1rem}.px-2{padding-left:.5rem;padding-right:.5rem}.px-3{padding-left:.75rem;padding-right:.75rem}.px-4{padding-left:1rem;padding-right:1rem}.px-5{padding-left:1.25rem;padding-right:1.25rem}.px-6{padding-left:1.5rem;padding-right:1.5rem}.py-0{padding-top:0;padding-bottom:0}.py-0\.5{padding-top:.125rem;padding-bottom:.125rem}.py-1{padding-top:.25rem;padding-bottom:.25rem}.py-2{padding-top:.5rem;padding-bottom:.5rem}.py-2\.5{padding-top:.625rem;padding-bottom:.625rem}.py-3{padding-top:.75rem;padding-bottom:.75rem}.py-4{padding-top:1rem;padding-bottom:1rem}.py-5{padding-top:1.25rem;padding-bottom:1.25rem}.pb-2{padding-bottom:.5rem}.pb-3{padding-bottom:.75rem}.pl-10{padding-left:2.5rem}.pl-11{padding-left:2.75rem}.pl-2{padding-left:.5rem}.pl-3{padding-left:.75rem}.pr-2{padding-right:.5rem}.pr-2\.5{padding-right:.625rem}.pt-16{padding-top:4rem}.pt-2{padding-top:.5rem}.pt-8{padding-top:2rem}.text-left{text-align:left}.text-center{text-align:center}.text-right{text-align:right}.align-baseline{vertical-align:initial}.align-top{vertical-align:top}.text-2xl{font-size:1.5rem;line-height:2rem}.text-base{font-size:1rem;line-height:1.5rem}.text-lg{font-size:1.125rem;line-height:1.75rem}.text-sm{font-size:.875rem;line-height:1.25rem}.text-xl{font-size:1.25rem;line-height:1.75rem}.text-xs{font-size:.75rem;line-height:1rem}.font-bold{font-weight:700}.font-medium{font-weight:500}.font-normal{font-weight:400}.font-semibold{font-weight:600}.uppercase{text-transform:uppercase}.italic{font-style:italic}.leading-6{line-height:1.5rem}.leading-9{line-height:2.25rem}.leading-none{line-height:1}.leading-tight{line-height:1.25}.text-black{--tw-text-opacity:1;color:rgb(0 0 0/var(--tw-text-opacity))}.text-blue-400{--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}.text-blue-600{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.text-blue-800{--tw-text-opacity:1;color:rgb(0 62 94/var(--tw-text-opacity))}.text-gray-400{--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}.text-gray-500{--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}.text-gray-600{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.text-gray-700{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.text-gray-800{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.text-gray-900{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.text-green-800{--tw-text-opacity:1;color:rgb(48 72 18/var(--tw-text-opacity))}.text-primary-600{--tw-text-opacity:1;color:rgb(97 145 37/var(--tw-text-opacity))}.text-red-500{--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.text-red-600{--tw-text-opacity:1;color:rgb(204 2 2/var(--tw-text-opacity))}.text-red-800{--tw-text-opacity:1;color:rgb(102 1 1/var(--tw-text-opacity))}.text-white{--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.text-yellow-800{--tw-text-opacity:1;color:rgb(114 59 19/var(--tw-text-opacity))}.underline{text-decoration-line:underline}.\!opacity-0{opacity:0!important}.\!opacity-100{opacity:1!important}.opacity-0{opacity:0}.opacity-100{opacity:1}.shadow{--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)}.shadow,.shadow-2xl{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-2xl{--tw-shadow:0 25px 50px -12px #00000040;--tw-shadow-colored:0 25px 50px -12px var(--tw-shadow-color)}.shadow-lg{--tw-shadow:0 10px 15px -3px #0000001a,0 4px 6px -4px #0000001a;--tw-shadow-colored:0 10px 15px -3px var(--tw-shadow-color),0 4px 6px -4px var(--tw-shadow-color)}.shadow-lg,.shadow-md{box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.shadow-md{--tw-shadow:0 4px 6px -1px #0000001a,0 2px 4px -2px #0000001a;--tw-shadow-colored:0 4px 6px -1px var(--tw-shadow-color),0 2px 4px -2px var(--tw-shadow-color)}.shadow-sm{--tw-shadow:0 1px 2px 0 #0000000d;--tw-shadow-colored:0 1px 2px 0 var(--tw-shadow-color);box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)}.outline{outline-style:solid}.outline-0{outline-width:0}.ring{--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(3px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.ring,.ring-1{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.ring-1{--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)}.blur{--tw-blur:blur(8px)}.blur,.filter{filter:var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow)}.transition{transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,-webkit-backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter,-webkit-backdrop-filter;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-all{transition-property:all;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-opacity{transition-property:opacity;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.transition-transform{transition-property:transform;transition-timing-function:cubic-bezier(.4,0,.2,1);transition-duration:.15s}.duration-100{transition-duration:.1s}.duration-200{transition-duration:.2s}.duration-300{transition-duration:.3s}.duration-500{transition-duration:.5s}.duration-75{transition-duration:75ms}.ease-\[cubic-bezier\(\.3\2c 2\.3\2c \.6\2c 1\)\]{transition-timing-function:cubic-bezier(.3,2.3,.6,1)}.ease-in-out{transition-timing-function:cubic-bezier(.4,0,.2,1)}.ease-out{transition-timing-function:cubic-bezier(0,0,.2,1)}.htmx-added .fade-in,.htmx-added.fade-in{opacity:0!important}.fade-in{opacity:1}.htmx-settling .fade-in-settle,.htmx-settling.fade-in-settle{opacity:0!important}.fade-in-settle{opacity:1}.htmx-added .swipe-left-swap,.htmx-added.swipe-left-swap{--tw-translate-x:-50%!important;opacity:1;--tw-scale-x:1;--tw-scale-y:1;--tw-translate-x: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-settling.htmx-added .swipe-left-swap,.htmx-settling.htmx-added.swipe-left-swap{opacity:0!important;--tw-scale-x:.75!important;--tw-scale-y:.75!important;--tw-translate-x:50%!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,.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))}.hidden .slide-up,.htmx-added .slide-up{--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{--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))}.live-added{animation:pulse-green .3s 2;animation-direction:alternate;animation-timing-function:ease-in-out}.dark .live-added{animation:pulse-dark-green .3s 2!important;animation-direction:alternate;animation-timing-function:ease-in-out}@keyframes pulse-green{0%{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}:is(.dark to){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}}@keyframes pulse-dark-green{:is(.dark 0%){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}to{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}}.htmx-request .htmx-indicator,.htmx-request.htmx-indicator{display:inherit!important}.htmx-indicator{display:none}.htmx-request .htmx-indicator-hidden{display:none!important}.htmx-indicator-hidden{display:inherit}.htmx-swapping .fade-out{opacity:0!important}.fade-out{opacity:1}.min-h-content{min-height:calc(100vh - 4em)}.choices{margin-bottom:0!important;border-width:0!important}.choices__inner{display:block!important;width:100%!important;border-radius:.5rem!important;border-width:1px!important;--tw-border-opacity:1!important;border-color:rgb(209 213 219/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important;padding:.25rem!important;font-size:.875rem!important;line-height:1.25rem!important;--tw-text-opacity:1!important;color:rgb(17 24 39/var(--tw-text-opacity))!important}.choices__inner:focus{--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}.group.has-error .choices__inner{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(255 230 230/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(51 1 1/var(--tw-text-opacity))!important}.group.has-error .choices__inner::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner::placeholder{--tw-placeholder-opacity:1!important;color:rgb(153 2 2/var(--tw-placeholder-opacity))!important}.group.has-error .choices__inner:focus{--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))!important}:is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(75 85 99/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}:is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(156 163 175/var(--tw-placeholder-opacity))!important}:is(.dark .choices__inner:focus){--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}.group.has-error :is(.dark .choices__inner){--tw-border-opacity:1!important;border-color:rgb(255 3 3/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 3 3/var(--tw-text-opacity))!important}.group.has-error :is(.dark .choices__inner)::-moz-placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.group.has-error :is(.dark .choices__inner)::placeholder{--tw-placeholder-opacity:1!important;color:rgb(255 3 3/var(--tw-placeholder-opacity))!important}.choices:focus-within .choices__inner,: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 #0000!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{margin:0!important;--tw-bg-opacity:1!important;background-color:rgb(249 250 251/var(--tw-bg-opacity))!important}:is(.dark .choices__inner .choices__input){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(255 255 255/var(--tw-text-opacity))!important}.choices__inner .choices__item{white-space:nowrap!important;border-radius:.25rem!important;--tw-border-opacity:1!important;border-color:rgb(156 163 175/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;padding:.125rem .5rem!important;font-size:.75rem!important;line-height:1rem!important;font-weight:500!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__inner .choices__item){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices__list--dropdown{border-radius:.5rem!important;--tw-bg-opacity:1!important;background-color:rgb(255 255 255/var(--tw-bg-opacity))!important;--tw-shadow:0 1px 3px 0 #0000001a,0 1px 2px -1px #0000001a!important;--tw-shadow-colored:0 1px 3px 0 var(--tw-shadow-color),0 1px 2px -1px var(--tw-shadow-color)!important;box-shadow:var(--tw-ring-offset-shadow,0 0 #0000),var(--tw-ring-shadow,0 0 #0000),var(--tw-shadow)!important}:is(.dark .choices__list--dropdown){--tw-bg-opacity:1!important;background-color:rgb(55 65 81/var(--tw-bg-opacity))!important}.choices__list--dropdown .choices__item--selectable.is-highlighted{--tw-bg-opacity:1!important;background-color:rgb(175 211 130/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(48 72 18/var(--tw-text-opacity))!important}:is(.dark .choices__list--dropdown .choices__item--selectable.is-highlighted){--tw-bg-opacity:1!important;background-color:rgb(24 36 9/var(--tw-bg-opacity))!important;--tw-text-opacity:1!important;color:rgb(175 211 130/var(--tw-text-opacity))!important}.choices[data-type*=select-multiple] .choices__button{--tw-border-opacity:1!important;border-color:rgb(107 114 128/var(--tw-border-opacity))!important}.choices[data-type*=select-multiple] .choices__button:focus{--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__inner .choices__item:focus-within{--tw-border-opacity:1!important;border-color:rgb(0 156 234/var(--tw-border-opacity))!important;--tw-bg-opacity:1!important;background-color:rgb(121 181 46/var(--tw-bg-opacity))!important;--tw-ring-opacity:1!important;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))!important}.choices__list--single .choices__item{display:flex!important;width:auto!important}.choices__list--single{width:auto!important}.choices__list--single button{position:relative!important;margin:0!important;display:block!important;height:auto!important}.choices[data-type*=select-one] .choices__button{right:auto!important}.arrow,.arrow:before{position:absolute;width:24px;height:24px;background:inherit}.arrow{visibility:hidden}.arrow:before{visibility:visible;content:"";transform:rotate(45deg)}.arrow{bottom:-4px}.hover\:scale-105:hover{--tw-scale-x:1.05;--tw-scale-y:1.05}.hover\:scale-105:hover,.hover\:scale-110:hover{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))}.hover\:scale-110:hover{--tw-scale-x:1.1;--tw-scale-y:1.1}.hover\:border-gray-300:hover{--tw-border-opacity:1;border-color:rgb(209 213 219/var(--tw-border-opacity))}.hover\:bg-blue-300:hover{--tw-bg-opacity:1;background-color:rgb(102 196 242/var(--tw-bg-opacity))}.hover\:bg-blue-600:hover{--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}.hover\:bg-blue-800:hover{--tw-bg-opacity:1;background-color:rgb(0 62 94/var(--tw-bg-opacity))}.hover\:bg-gray-100:hover{--tw-bg-opacity:1;background-color:rgb(243 244 246/var(--tw-bg-opacity))}.hover\:bg-gray-200:hover{--tw-bg-opacity:1;background-color:rgb(229 231 235/var(--tw-bg-opacity))}.hover\:bg-green-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-green-300:hover{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.hover\:bg-green-600:hover{--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}.hover\:bg-green-700:hover{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}.hover\:bg-neutral-100:hover{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.hover\:bg-primary-100:hover{--tw-bg-opacity:1;background-color:rgb(228 240 213/var(--tw-bg-opacity))}.hover\:bg-red-300:hover{--tw-bg-opacity:1;background-color:rgb(255 104 104/var(--tw-bg-opacity))}.hover\:bg-white:hover{--tw-bg-opacity:1;background-color:rgb(255 255 255/var(--tw-bg-opacity))}.hover\:text-blue-600:hover{--tw-text-opacity:1;color:rgb(0 125 187/var(--tw-text-opacity))}.hover\:text-gray-600:hover{--tw-text-opacity:1;color:rgb(75 85 99/var(--tw-text-opacity))}.hover\:text-gray-700:hover{--tw-text-opacity:1;color:rgb(55 65 81/var(--tw-text-opacity))}.hover\:text-gray-800:hover{--tw-text-opacity:1;color:rgb(31 41 55/var(--tw-text-opacity))}.hover\:text-gray-900:hover{--tw-text-opacity:1;color:rgb(17 24 39/var(--tw-text-opacity))}.hover\:text-primary-700:hover{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.hover\:underline:hover{text-decoration-line:underline}.focus\:z-10:focus{z-index:10}.focus\:border-blue-500:focus{--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}.focus\:border-primary-500:focus{--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}.focus\:bg-neutral-100:focus{--tw-bg-opacity:1;background-color:rgb(245 245 245/var(--tw-bg-opacity))}.focus\:text-green-700:focus{--tw-text-opacity:1;color:rgb(73 109 28/var(--tw-text-opacity))}.focus\:outline-none:focus{outline:2px solid #0000;outline-offset:2px}.focus\:ring-2:focus{--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(2px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-2:focus,.focus\:ring-4:focus{box-shadow:var(--tw-ring-offset-shadow),var(--tw-ring-shadow),var(--tw-shadow,0 0 #0000)}.focus\:ring-4:focus{--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(4px + var(--tw-ring-offset-width)) var(--tw-ring-color)}.focus\:ring-blue-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(153 215 247/var(--tw-ring-opacity))}.focus\:ring-blue-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(102 196 242/var(--tw-ring-opacity))}.focus\:ring-blue-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}.focus\:ring-gray-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(229 231 235/var(--tw-ring-opacity))}.focus\:ring-gray-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(209 213 219/var(--tw-ring-opacity))}.focus\:ring-green-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(201 225 171/var(--tw-ring-opacity))}.focus\:ring-green-300:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(175 211 130/var(--tw-ring-opacity))}.focus\:ring-green-700:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(73 109 28/var(--tw-ring-opacity))}.focus\:ring-primary-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}.focus\:ring-red-200:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 154 154/var(--tw-ring-opacity))}.group:hover .group-hover\:scale-110{--tw-scale-x:1.1;--tw-scale-y:1.1;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))}.group:hover .group-hover\:text-blue-500{--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:border-red-500{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:bg-red-50{--tw-bg-opacity:1;background-color:rgb(255 230 230/var(--tw-bg-opacity))}.group.has-error .group-\[\.has-error\]\:text-red-900{--tw-text-opacity:1;color:rgb(51 1 1/var(--tw-text-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:placeholder-red-700::placeholder{--tw-placeholder-opacity:1;color:rgb(153 2 2/var(--tw-placeholder-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:border-red-500:focus{--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error .group-\[\.has-error\]\:focus\:ring-red-500:focus{--tw-ring-opacity:1;--tw-ring-color:rgb(255 3 3/var(--tw-ring-opacity))}:is(.dark .dark\:block){display:block}:is(.dark .dark\:hidden){display:none}:is(.dark .dark\:divide-gray-600)>:not([hidden])~:not([hidden]){--tw-divide-opacity:1;border-color:rgb(75 85 99/var(--tw-divide-opacity))}:is(.dark .dark\:border-blue-500){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-500){--tw-border-opacity:1;border-color:rgb(107 114 128/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-600){--tw-border-opacity:1;border-color:rgb(75 85 99/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-700){--tw-border-opacity:1;border-color:rgb(55 65 81/var(--tw-border-opacity))}:is(.dark .dark\:border-gray-900){--tw-border-opacity:1;border-color:rgb(17 24 39/var(--tw-border-opacity))}:is(.dark .dark\:border-transparent){border-color:#0000}:is(.dark .dark\:bg-blue-600){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-700){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:bg-blue-900){--tw-bg-opacity:1;background-color:rgb(0 31 47/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-600){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:bg-gray-800\/50){background-color:#1f293780}:is(.dark .dark\:bg-gray-900){--tw-bg-opacity:1;background-color:rgb(17 24 39/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-600){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-700){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:bg-green-900){--tw-bg-opacity:1;background-color:rgb(24 36 9/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-700){--tw-bg-opacity:1;background-color:rgb(153 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:bg-red-900){--tw-bg-opacity:1;background-color:rgb(51 1 1/var(--tw-bg-opacity))}:is(.dark .dark\:bg-yellow-900){--tw-bg-opacity:1;background-color:rgb(99 49 18/var(--tw-bg-opacity))}:is(.dark .dark\:bg-opacity-80){--tw-bg-opacity:0.8}:is(.dark .dark\:text-blue-200){--tw-text-opacity:1;color:rgb(153 215 247/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-300){--tw-text-opacity:1;color:rgb(102 196 242/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-400){--tw-text-opacity:1;color:rgb(51 176 238/var(--tw-text-opacity))}:is(.dark .dark\:text-blue-500){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-100){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-200){--tw-text-opacity:1;color:rgb(229 231 235/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-300){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-400){--tw-text-opacity:1;color:rgb(156 163 175/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-50){--tw-text-opacity:1;color:rgb(249 250 251/var(--tw-text-opacity))}:is(.dark .dark\:text-gray-500){--tw-text-opacity:1;color:rgb(107 114 128/var(--tw-text-opacity))}:is(.dark .dark\:text-green-300){--tw-text-opacity:1;color:rgb(175 211 130/var(--tw-text-opacity))}:is(.dark .dark\:text-red-300){--tw-text-opacity:1;color:rgb(255 104 104/var(--tw-text-opacity))}:is(.dark .dark\:text-red-400){--tw-text-opacity:1;color:rgb(255 53 53/var(--tw-text-opacity))}:is(.dark .dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}:is(.dark .dark\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:text-yellow-300){--tw-text-opacity:1;color:rgb(250 202 21/var(--tw-text-opacity))}:is(.dark .dark\:placeholder-gray-400)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:placeholder-gray-400)::placeholder{--tw-placeholder-opacity:1;color:rgb(156 163 175/var(--tw-placeholder-opacity))}:is(.dark .dark\:ring-offset-gray-700){--tw-ring-offset-color:#374151}:is(.dark .dark\:ring-offset-gray-800){--tw-ring-offset-color:#1f2937}:is(.dark .dark\:hover\:bg-blue-600:hover){--tw-bg-opacity:1;background-color:rgb(0 125 187/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-blue-700:hover){--tw-bg-opacity:1;background-color:rgb(0 94 140/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-600:hover){--tw-bg-opacity:1;background-color:rgb(75 85 99/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-700:hover){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-gray-800:hover){--tw-bg-opacity:1;background-color:rgb(31 41 55/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-600:hover){--tw-bg-opacity:1;background-color:rgb(97 145 37/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-green-700:hover){--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:bg-red-600:hover){--tw-bg-opacity:1;background-color:rgb(204 2 2/var(--tw-bg-opacity))}:is(.dark .dark\:hover\:text-blue-500:hover){--tw-text-opacity:1;color:rgb(0 156 234/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-100:hover){--tw-text-opacity:1;color:rgb(243 244 246/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-gray-300:hover){--tw-text-opacity:1;color:rgb(209 213 219/var(--tw-text-opacity))}:is(.dark .dark\:hover\:text-white:hover){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:border-blue-500:focus){--tw-border-opacity:1;border-color:rgb(0 156 234/var(--tw-border-opacity))}:is(.dark .dark\:focus\:border-primary-500:focus){--tw-border-opacity:1;border-color:rgb(121 181 46/var(--tw-border-opacity))}:is(.dark .dark\:focus\:text-white:focus){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}:is(.dark .dark\:focus\:ring-blue-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 156 234/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 125 187/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-blue-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(0 62 94/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-gray-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(75 85 99/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-green-800:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(48 72 18/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-500:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(121 181 46/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-primary-600:focus){--tw-ring-opacity:1;--tw-ring-color:rgb(97 145 37/var(--tw-ring-opacity))}:is(.dark .dark\:focus\:ring-offset-gray-700:focus){--tw-ring-offset-color:#374151}:is(.dark .group:hover .dark\:group-hover\:text-white){--tw-text-opacity:1;color:rgb(255 255 255/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:border-red-500){--tw-border-opacity:1;border-color:rgb(255 3 3/var(--tw-border-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:bg-gray-700){--tw-bg-opacity:1;background-color:rgb(55 65 81/var(--tw-bg-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:text-red-500){--tw-text-opacity:1;color:rgb(255 3 3/var(--tw-text-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::-moz-placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}.group.has-error :is(.dark .group-\[\.has-error\]\:dark\:placeholder-red-500)::placeholder{--tw-placeholder-opacity:1;color:rgb(255 3 3/var(--tw-placeholder-opacity))}@media (min-width:640px){.sm\:block{display:block}.sm\:rounded-lg{border-radius:.5rem}.sm\:p-6{padding:1.5rem}.sm\:py-5{padding-top:1.25rem;padding-bottom:1.25rem}.sm\:text-sm{font-size:.875rem;line-height:1.25rem}}@media (min-width:768px){.md\:ml-2{margin-left:.5rem}.md\:mr-24{margin-right:6rem}.md\:table-cell{display:table-cell}.md\:flex-row{flex-direction:row}.md\:items-center{align-items:center}.md\:justify-center{justify-content:center}.md\:space-x-3>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(.75rem*var(--tw-space-x-reverse));margin-left:calc(.75rem*(1 - var(--tw-space-x-reverse)))}.md\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.md\:p-12{padding:3rem}}@media (min-width:1024px){.lg\:block{display:block}.lg\:table-cell{display:table-cell}.lg\:hidden{display:none}.lg\:w-96{width:24rem}.lg\:-translate-x-full{--tw-translate-x:-100%}.lg\:-translate-x-full,.lg\:translate-x-0{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))}.lg\:translate-x-0{--tw-translate-x:0px}.lg\:flex-row{flex-direction:row}.lg\:items-center{align-items:center}.lg\:justify-end{justify-content:flex-end}.lg\:justify-between{justify-content:space-between}.lg\:space-x-4>:not([hidden])~:not([hidden]){--tw-space-x-reverse:0;margin-right:calc(1rem*var(--tw-space-x-reverse));margin-left:calc(1rem*(1 - var(--tw-space-x-reverse)))}.lg\:space-y-0>:not([hidden])~:not([hidden]){--tw-space-y-reverse:0;margin-top:calc(0px*(1 - var(--tw-space-y-reverse)));margin-bottom:calc(0px*var(--tw-space-y-reverse))}.lg\:px-5{padding-left:1.25rem;padding-right:1.25rem}.lg\:pl-3{padding-left:.75rem}.lg\:pl-64{padding-left:16rem}}.\[\&\.active\]\:bg-primary-300.active{--tw-bg-opacity:1;background-color:rgb(175 211 130/var(--tw-bg-opacity))}.\[\&\.active\]\:bg-primary-500.active{--tw-bg-opacity:1;background-color:rgb(121 181 46/var(--tw-bg-opacity))}:is(.dark .\[\&\.active\]\:dark\:bg-primary-700).active{--tw-bg-opacity:1;background-color:rgb(73 109 28/var(--tw-bg-opacity))} \ No newline at end of file diff --git a/src/clj/auto_ap/cursor.clj b/src/clj/auto_ap/cursor.clj index 0ff1448d..4247b9f5 100644 --- a/src/clj/auto_ap/cursor.clj +++ b/src/clj/auto_ap/cursor.clj @@ -1,7 +1,6 @@ (ns auto-ap.cursor (:import (clojure.lang IDeref Atom ILookup Counted IFn AFn Indexed ISeq Seqable))) -; TODO not sure if these methods are needed at all; ICursor is used solely as a marker right now (defprotocol ICursor (path [cursor]) (state [cursor])) @@ -125,6 +124,15 @@ [] nil)) +(defn synthetic-cursor [v prefix] + (let [internal-cursor (cursor v)] + (reify ICursor + (path [this] + (into prefix (path internal-cursor))) + (state [this] + (state internal-cursor))))) + + (defn transact! [cursor f] "Changes value beneath cursor by passing it to a single-argument function f. Old value will be passed as function argument. Function diff --git a/src/clj/auto_ap/parse/templates.clj b/src/clj/auto_ap/parse/templates.clj index ab43ca87..181e6639 100644 --- a/src/clj/auto_ap/parse/templates.clj +++ b/src/clj/auto_ap/parse/templates.clj @@ -254,7 +254,7 @@ :total [:trim-commas-and-negate nil]}} ;; Young's Market Co new statement - {:vendor "Youngs Market" + {:vendor "RNDC" :keywords [#"(YOUNG'S MARKET COMPANY|Young.*Statement)"] :extract {:date #"([0-9]+/[0-9]+/[0-9]+)" :customer-identifier #"Customer Name +([\w ]+)" @@ -266,7 +266,7 @@ :multi-match? #"^[0-9]+.*\$?([0-9,]+\.[0-9]+).*\$?([0-9,]+\.[0-9]+)"} ;; Young's Market Co - INVOICE - {:vendor "Youngs Market" + {:vendor "RNDC" :keywords [#"P.O.Box 743564"] :extract {:date #"(?:INVOICE|CREDIT) DATE\n(?:.*?)(\S+)\n" #_#_:customer-identifier #"(?:INVOICE|CREDIT) DATE\n [0-9]+\s+(.*?)\s{2,}" diff --git a/src/clj/auto_ap/square/core3.clj b/src/clj/auto_ap/square/core3.clj index d9c1e916..55cdf61a 100644 --- a/src/clj/auto_ap/square/core3.clj +++ b/src/clj/auto_ap/square/core3.clj @@ -767,6 +767,14 @@ (dc/db conn) codes)))) +(defn get-square-client-and-location [code] + (let [[client] (get-square-clients code)] + (some->> client + :client/square-locations + (filter :square-location/client-location) + seq + (conj [client])))) + (defn upsert-locations ([] (apply de/zip diff --git a/src/clj/auto_ap/ssr/admin/accounts.clj b/src/clj/auto_ap/ssr/admin/accounts.clj index 895921b0..53061b12 100644 --- a/src/clj/auto_ap/ssr/admin/accounts.clj +++ b/src/clj/auto_ap/ssr/admin/accounts.clj @@ -7,6 +7,7 @@ audit-transact conn merge-query + pull-attr pull-many query2]] [auto-ap.query-params :as query-params] @@ -15,33 +16,38 @@ [auto-ap.solr :as solr] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [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 - forced-vector + field-validation-error + form-validation-error html-response - map->db-id-decoder + main-transformer + many-entity + modal-response ref->enum-schema ref->select-options + strip temp-id - validation-error - wrap-form-4xx + wrap-entity + wrap-form-4xx-2 wrap-schema-decode]] [bidi.bidi :as bidi] [clojure.string :as str] [datomic.api :as dc] - [hiccup2.core :as hiccup] [malli.core :as mc])) (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-account-table) - "hx-target" "#account-table" - "hx-indicator" "#account-table"} + "hx-target" "#entity-table" + "hx-indicator" "#entity-table"} [:fieldset.space-y-6 (com/field {:label "Name"} @@ -125,26 +131,22 @@ matching-count])) (def grid-page - (helper/build {:id "account-table" + (helper/build {:id "entity-table" :nav (com/admin-aside-nav) :page-specific-nav filters :fetch-page fetch-page :parse-query-params (comp (query-params/parse-key :code query-params/parse-long) (helper/default-parse-query-params grid-page)) - :action-buttons (fn [request] + :action-buttons (fn [_] [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-account-new-dialog)) - :hx-target "#modal-holder" - :hx-swap "outerHTML" :color :primary} "New Account")]) - :row-buttons (fn [request entity] + :row-buttons (fn [_ entity] [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-account-edit-dialog - :db/id (:db/id entity))) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} + :db/id (:db/id entity)))} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -177,11 +179,29 @@ (def table* (partial helper/table* grid-page)) (defn account-save [{:keys [form-params request-method] :as request}] - (let [entity (cond-> form-params - (= :post request-method) (assoc :db/id "new")) - _ (cond (= :post request-method) - (when-let [extant (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity)))] - (validation-error (format "The code %d is already in use." (:account/numeric-code entity))))) + (let [entity (cond-> form-params + (= :post request-method) (assoc :db/id "new")) + _ (cond (= :post request-method) + (when (seq (dc/q '[:find ?x :in $ ?nc :where [?x :account/numeric-code ?nc]] (dc/db conn) (:account/numeric-code entity))) + (field-validation-error (format "The code %d is already in use." (:account/numeric-code entity)) + [:account/numeric-code] + :form form-params))) + _ (some->> form-params + :account/client-overrides + (group-by :account-client-override/client) + (filter (fn [[_ overrides]] + (> (count overrides) 1))) + (map first) + seq + (#(form-validation-error (format "Client(s) %s have more than one override." + (str/join ", " + (map (fn [client] + (format "'%s'" (pull-attr (dc/db conn) + :client/name + (-> client))) + ) %))) + :form form-params)) ;; TODO shouldnt need to bubble this through. See if we can eliminate the passing of form and last-form. + ) {:keys [tempids]} (audit-transact [[:upsert-entity (cond-> entity (:account/numeric-code entity) (assoc :account/code (str (:account/numeric-code entity))))]] (:identity request)) @@ -207,140 +227,204 @@ "account_client_override_id" (:db/id o)}))) (html-response (row* identity updated-account {:flash? true}) - :headers {"hx-trigger" "closeModal" - "hx-retarget" (format "#account-table tr[data-id=\"%d\"]" (:db/id updated-account))}))) - + :headers (cond-> {"hx-trigger" "modalclose"} + (= :post request-method) (assoc "hx-retarget" "#entity-table tbody" + "hx-reswap" "afterbegin") + (= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-account))))))) (defn client-override* [override] - [:div.flex.gap-2.mb-2.client-override - [:div.w-96 - (com/typeahead {:name (format "account/client-overrides[%s][account-client-override/client]" (:db/id override)) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :id (str "account-client-override-" (:db/id override)) - :value [(:db/id (:account-client-override/client override)) - (:client/name (:account-client-override/client override))]})] - [:div.w-96 - (com/text-input {:name (format "account/client-overrides[%s][account-client-override/name]" (:db/id override)) - :class "w-full" - :value (:account-client-override/name override)})] - [:div (com/a-icon-button {"_" (hiccup/raw "on click halt the event then transition the closest <.client-override />'s opacity to 0 then remove closest <.client-override />") } svg/x)]]) + (com/data-grid-row (-> {:x-ref "p" + :data-key "show" + :x-data (hx/json {:show (boolean (doto (not (fc/field-value (:new? override))) + println))})} + hx/alpine-mount-then-appear) + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :account-client-override/client + (com/data-grid-cell {} + (com/validated-field {:errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :placeholder "Search..." + :class "w-96" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + :content-fn #(pull-attr (dc/db conn) :client/name %)})))) + (fc/with-field :account-client-override/name + (com/data-grid-cell + {} + (com/validated-field {:errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-96" + :value (fc/field-value)})))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) +(defn dialog* [{:keys [entity form-params form-errors]}] + (fc/start-form form-params form-errors + [:div {:x-data (hx/json {"accountName" (or (:account/name form-params) (:account/numeric-code entity)) + "accountCode" (or (:account/numeric-code form-params) (:account/numeric-code entity) )}) + :hx-target "this" + :class "w-full h-full"} + (com/modal + {} + [:form (-> {:hx-ext "response-targets" + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content" + :class "h-full"} + (assoc (if (:db/id entity) + :hx-put + :hx-post) + (str (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-edit-save)))) + [:fieldset {:class "hx-disable h-full"} + (com/modal-card + {} + [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 + [:span {:x-text "accountCode"}] + " - " + [:span {:x-text "accountName"}]]] + [:div.space-y-1 + (when-let [id (:db/id entity)] + (com/hidden {:name "db/id" + :value id})) -(defn dialog* [& {:keys [ account form-params]}] - (com/modal - {:modal-class "max-w-4xl"} - [:form#edit-form (merge {:hx-ext "response-targets" - :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content"} - form-params) - [:fieldset {:class "hx-disable"} - (com/modal-card - {} - [:div.flex [:div.p-2 "Account"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:account/numeric-code account) " - " (:account/name account)]] - [:div.space-y-6 - (when-let [id (:db/id account)] - (com/hidden {:name "db/id" - :value id})) - - (when (nil? account) - (com/field {:label "Numeric code"} - (com/text-input {:name "account/numeric-code" - :autofocus true - :class "w-32"}))) - (com/field {:label "Name"} - (com/text-input {:name "account/name" - :autofocus true - :class "w-32" - :value (:account/name account)})) - (com/field {:label "Account Type"} - (com/select {:name "account/type" - :class "w-36" - :id "type" - :value (some-> account :account/type name) - :options (ref->select-options "account-type")})) - (com/field {:label "Location"} - (com/text-input {:name "account/location" - :class "w-16" - :value (:account/location account)})) + (fc/with-field :account/numeric-code + (if (nil? (:db/id entity)) + (com/validated-field {:label "Numeric code" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :x-model "accountCode" + :autofocus true + :class "w-32"})) + (com/hidden {:name (fc/field-name) + :value (fc/field-value)}))) + (fc/with-field :account/name + (com/validated-field {:label "Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :x-model "accountName" - (com/field {:label "Invoice Allowance"} - (com/select {:name "account/invoice-allowance" - :value (some-> account :account/invoice-allowance name) - :class "w-36" - :options (ref->select-options "allowance")})) - (com/field {:label "Vendor Allowance"} - (com/select {:name "account/vendor-allowance" - :class "w-36" - :value (some-> account :account/vendor-allowance name) - :options (ref->select-options "allowance")})) - (com/field {:label "Applicability"} - (com/select {:name "account/applicability" - :class "w-36" - :value (some-> account :account/applicability name) - :options (ref->select-options "account-applicability")})) + :class "w-64" + :value (fc/field-value)}))) + (fc/with-field :account/type + (com/validated-field {:label "Account Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :id "type" + :value (some-> (fc/field-value) name) + :options (ref->select-options "account-type")}))) + (fc/with-field :account/location + (com/validated-field {:label "Location" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-16" + :value (fc/field-value)}))) - (com/field {:label "Client Overrides" :id "client-overrides"} - (for [override (:account/client-overrides account)] - (client-override* override))) - (com/a-button {:hx-get (bidi/path-for ssr-routes/only-routes - :admin-account-client-override-new) - :hx-target "#client-overrides" - :hx-swap "beforeend"} - "New override") - [:div#form-errors [:span.error-content]] - (com/button {:color :primary :form "edit-form" :type "submit"} - "Save")] - [:div])]])) + [:div.flex.flex-wrap.gap-4 + (fc/with-field :account/invoice-allowance + (com/validated-field {:label "Invoice Allowance" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :value (some-> (fc/field-value) name) + :class "w-36" + :options (ref->select-options "allowance")}))) + (fc/with-field :account/vendor-allowance + (com/validated-field {:label "Vendor Allowance" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :value (some-> (fc/field-value) name) + :options (ref->select-options "allowance")})))] + (fc/with-field :account/applicability + (com/validated-field {:label "Applicability" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :value (some-> (fc/field-value) name) + :options (ref->select-options "account-applicability")}))) -(defn new-client-override [_] + (fc/with-field :account/client-overrides + + (com/field {:label "Client Overrides" :id "client-overrides"} + + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Account name") + (com/data-grid-header {})] + :id "client-override-table"} + (fc/cursor-map + #(client-override* %)) + + (com/data-grid-new-row {:colspan 3 + :index (count (fc/field-value)) + :hx-get (bidi/path-for ssr-routes/only-routes + :admin-account-client-override-new)} + "New override"))))] + [:div + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors (seq form-errors)} + "Save account")])]])])) + +(defn new-client-override [{ {:keys [index]} :query-params}] (html-response - (client-override* {:db/id (str (java.util.UUID/randomUUID))}))) + (fc/start-form-with-prefix + [:account/client-overrides (or index 0)] + {:db/id (str (java.util.UUID/randomUUID)) + :new? true} + [] + (client-override* fc/*current*)))) -(defn account-edit-dialog [request] - (let [account (some-> request :route-params :db/id (#(dc/pull (dc/db conn) default-read %)))] - (html-response (dialog* :account account - :form-params {:hx-put (str (bidi/path-for ssr-routes/only-routes - :admin-account-edit-save))})))) - - -(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))}))) - -(def account-schema (mc/schema +(def form-schema (mc/schema [:map [:db/id {:optional true} [:maybe entity-id]] [:account/numeric-code {:optional true} [:maybe :int]] - [:account/name [:string {:min 1}]] - [:account/location [:maybe :string]] + [:account/name [:string {:min 1 :decode/string strip}]] + [:account/location {:optional true} [:maybe [:string {:decode/string strip}]]] [:account/type (ref->enum-schema "account-type")] - [:account/applicability (ref->enum-schema "account-applicability")] + [:account/applicability (ref->enum-schema "account-applicability")] ; [:account/invoice-allowance (ref->enum-schema "allowance")] [:account/vendor-allowance (ref->enum-schema "allowance")] - [:account/client-overrides {:decode/json map->db-id-decoder - :optional true} + [:account/client-overrides {:optional true} [:maybe - (forced-vector [:map - [:db/id [:or entity-id temp-id]] - [:account-client-override/client [:or entity-id :string]] - [:account-client-override/name :string]])]]])) + (many-entity {} + [:db/id [:or entity-id temp-id]] + [:account-client-override/client entity-id] + [:account-client-override/name [:string {:min 2 :decode/string strip}]])]]])) + +(defn account-dialog [{:keys [entity form-params form-errors]}] + (modal-response (dialog* {:entity entity + :form-params (or (when (seq form-params) + form-params) + (when entity + (mc/decode form-schema entity main-transformer)) + {}) + :form-errors form-errors}))) + + + (def key->handler (apply-middleware-to-all-handlers (->> {:admin-accounts (helper/page-route grid-page) :admin-account-table (helper/table-route grid-page) - :admin-account-client-override-new (-> new-client-override wrap-admin wrap-client-redirect-unauthenticated) + :admin-account-client-override-new (-> new-client-override + (wrap-schema-decode :query-schema [:map + [:index {:optional true + :default 0} [nat-int? {:default 0}]]]) + wrap-admin wrap-client-redirect-unauthenticated) :admin-account-save (-> account-save - (wrap-schema-decode :form-schema account-schema) + (wrap-entity [:form-params :db/id] default-read) + (wrap-schema-decode :form-schema form-schema) (wrap-nested-form-params) - (wrap-form-4xx)) - :admin-account-edit-dialog (-> account-edit-dialog + (wrap-form-4xx-2 (wrap-entity account-dialog [:form-params :db/id] default-read))) + :admin-account-edit-dialog (-> account-dialog + (wrap-entity [:route-params :db/id] default-read) (wrap-schema-decode :route-schema [:map [:db/id entity-id]])) - :admin-account-new-dialog account-new-dialog}) + :admin-account-new-dialog account-dialog}) (fn [h] (-> h (wrap-admin) diff --git a/src/clj/auto_ap/ssr/admin/background_jobs.clj b/src/clj/auto_ap/ssr/admin/background_jobs.clj index d468010f..f09879bd 100644 --- a/src/clj/auto_ap/ssr/admin/background_jobs.clj +++ b/src/clj/auto_ap/ssr/admin/background_jobs.clj @@ -2,24 +2,28 @@ (:require [amazonica.aws.ecs :as ecs] [auto-ap.logging :as alog] + [clojure.string :as str] [auto-ap.routes.utils :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.utils :refer [apply-middleware-to-all-handlers + entity-id + form-validation-error html-response - validation-error - wrap-form-4xx + modal-response + wrap-form-4xx-2 wrap-schema-decode]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [clj-time.coerce :as coerce] [clj-time.core :as time] - [clojure.string :as str] - [config.core :refer [env]]) + [malli.core :as mc] + [auto-ap.ssr.hx :as hx]) (:import (com.amazonaws.services.ecs.model AssignPublicIp))) @@ -57,8 +61,8 @@ false)) (defn ecs-task->job [task] - - {:status (condp = (:last-status task) + {:arn (:task-arn task) + :status (condp = (:last-status task) "RUNNING" :running "PENDING" :pending "PROVISIONING" :pending @@ -78,13 +82,11 @@ (def grid-page (helper/build {:id "job-table" + :id-fn :arn :nav (com/admin-aside-nav) :fetch-page fetch-page :action-buttons (fn [request] - [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes - :admin-job-start-dialog)) - :hx-target "#modal-holder" - :hx-swap "outerHTML" + [(com/button {:hx-get (str (bidi/path-for ssr-routes/only-routes :admin-job-start-dialog)) :color :primary} "Run job")]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes @@ -151,86 +153,119 @@ (str "_" (:dd-env env))) (dissoc form-params :name))] {:message (str "task " (str new-job) " started.")}) - (validation-error "This job is already running"))) + (form-validation-error "This job is already running" + :form form-params))) -(defn subform [{{:strs [name]} :query-params }] - (html-response (cond (= "bulk-journal-import" name) - [:div (com/field {:label "Url"} - [:div.flex.place-items-center.gap-2 - [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] - (com/text-input {:placeholder "ledger-data.csv" - :name "ledger-url"} )])] - (= "register-invoice-import" name) - [:div (com/field {:label "Url"} +(defn subform* [{:keys [name]}] + (into [:div {:class "fade-in-settle transition"}] + (cond (= "bulk-journal-import" name) + [(fc/with-field :ledger-url + (com/validated-field {:label "Url" + :errors (fc/field-errors)} + [:div.flex.place-items-center.gap-2 + [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] + (com/text-input {:placeholder "ledger-data.csv" + :name (fc/field-name) + :value (fc/field-value)} )]))] + (= "register-invoice-import" name) + [ + (fc/with-field :invoice-url + (com/validated-field {:label "Url" + :errors (fc/field-errors)} [:div.flex.place-items-center.gap-2 [:pre.text-xs.mr-1 "s3://data.prod.app.integreatconsult.com/bulk-import/"] (com/text-input {:placeholder "invoice-data.csv" - :name "invoice-url"} )])] - (= "load-historical-sales" name) - [:div - (com/field {:label "Client"} - (com/typeahead {:name "client" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :id (str "client-search")})) - (com/field {:label "Days to load"} - (com/text-input {:placeholder "60" - :name "days"} ))] - :else [:div])) + :name (fc/field-name) + :value (fc/field-value)} )]))] + (= "load-historical-sales" name) + [ + (fc/with-field :client + (com/validated-field {:label "Client" + :errors (fc/field-errors)} + (com/typeahead {:name (fc/field-name) + :value (fc/field-value) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search)}))) + (fc/with-field :days + (com/validated-field {:label "Days to load" + :errors (fc/field-errors)} + (com/text-input {:placeholder "60" + :name (fc/field-name) + :value (fc/field-value)} )))] + :else nil)) + ) -(defn job-start-dialog [_] - (html-response (com/modal - {:modal-class "max-w-4xl"} - [:form#edit-form {:hx-ext "response-targets" - :hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start - ) - :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content"} - [:fieldset {:class "hx-disable"} - (com/modal-card - {} - [:div.flex [:div.p-2 "New job"] ] - [:div.space-y-6 +(defn subform [{{:keys [name]} :query-params }] + (html-response + (fc/start-form {} nil + (subform* {:name name})))) - (com/field {:label "Job"} - (com/select {:name "name" - :class "w-64" - :options [["" ""] - ["yodlee2" "Yodlee Import"] - ["yodlee2-accounts" "Yodlee Account Import"] - ["intuit" "Intuit import"] - ["plaid" "Plaid import"] - ["bulk-journal-import" "Bulk Journal Import"] - ["square2-import-job" "Square2 Import"] - ["register-invoice-import" "Register Invoice Import "] - ["ezcater-upsert" "Upsert recent ezcater orders"] - ["load-historical-sales" "Load Historical Square Sales"] - ["export-backup" "Export Backup"]] - :hx-get (bidi/path-for ssr-routes/only-routes - :admin-job-subform) - :hx-target "#sub-form" - :hx-swap "innerHTML"})) +(defn job-start-dialog [{:keys [form-errors form-params] :as request}] + (fc/start-form (or form-params {}) form-errors + (modal-response + (com/modal ;; TODO we need a cleaner way to have forms that wrap the whole. In this cas + {} + [:form {:hx-post (bidi/path-for ssr-routes/only-routes :admin-job-start) + :class "h-full w-full" + "x-on:htmx:response-error" "unexpectedError=true" + "x-on:htmx:before-request" "unexpectedError=false" + :x-data (hx/json {:unexpectedError false})} + [:fieldset {:class "hx-disable h-full w-full"} + (com/modal-card {} + [:div.m-2 "New job"] + [:div.space-y-6 - [:div#sub-form] - [:div#form-errors [:span.error-content]] - (com/button {:color :primary :form "edit-form" :type "submit"} - "Run")] - [:div])]]))) + (fc/with-field :name + (com/validated-field {:label "Job" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :value (fc/field-value) + :class "w-64" + :options [["" ""] + ["yodlee2" "Yodlee Import"] + ["yodlee2-accounts" "Yodlee Account Import"] + ["intuit" "Intuit import"] + ["plaid" "Plaid import"] + ["bulk-journal-import" "Bulk Journal Import"] + ["square2-import-job" "Square2 Import"] + ["register-invoice-import" "Register Invoice Import "] + ["ezcater-upsert" "Upsert recent ezcater orders"] + ["load-historical-sales" "Load Historical Square Sales"] + ["export-backup" "Export Backup"]] + :hx-get (bidi/path-for ssr-routes/only-routes + :admin-job-subform) + :hx-target "#sub-form" + :hx-swap "innerHTML"}))) + + [:div#sub-form (subform* {:name (fc/with-field :name (fc/field-value))}) ]] + [:div + [:div#5xx-error.bg-red-100.mb-2.p-1 {:x-show "unexpectedError"} + "An unexpected error has occured."] + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors form-errors} "Run job")])]])))) + +(def form-schema (mc/schema [:map + [:name [:string {:min 1}]] + [:ledger-url {:optional true} [:string {:min 1}]] + [:invoice-url {:optional true} [:string {:min 1}]] + [:client {:optional true} entity-id] + [:days {:optional true} [:int {:min 1 :max 120}]] + ])) (def key->handler (apply-middleware-to-all-handlers (->> - {:admin-jobs (helper/page-route grid-page) - :admin-job-table (helper/table-route grid-page) - :admin-job-subform (-> subform (wrap-schema-decode :query-schema [:map [:name :string]])) - :admin-job-start (-> job-start - (wrap-schema-decode :form-schema [:map [:name :string]]) - (wrap-nested-form-params) - (wrap-form-4xx)) - :admin-job-start-dialog job-start-dialog}) + {:admin-jobs (helper/page-route grid-page) + :admin-job-table (helper/table-route grid-page) + :admin-job-subform (-> subform (wrap-schema-decode :query-schema [:map [:name {:optional true} [:maybe :string]]])) + :admin-job-start (-> job-start + (wrap-schema-decode :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 job-start-dialog)) + :admin-job-start-dialog job-start-dialog}) (fn [h] (-> h (wrap-admin) diff --git a/src/clj/auto_ap/ssr/admin/transaction_rules.clj b/src/clj/auto_ap/ssr/admin/transaction_rules.clj index 17b95695..c5a9c0bc 100644 --- a/src/clj/auto_ap/ssr/admin/transaction_rules.clj +++ b/src/clj/auto_ap/ssr/admin/transaction_rules.clj @@ -18,9 +18,9 @@ [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.company :refer [bank-account-typeahead*]] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper] [auto-ap.ssr.hx :as hx] - [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.utils @@ -29,53 +29,42 @@ field-validation-error form-validation-error html-response + main-transformer many-entity + modal-response money - path->name2 percentage ref->enum-schema ref->radio-options regex temp-id + wrap-entity wrap-form-4xx-2 wrap-schema-decode]] + [auto-ap.time :as atime] [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] - [auto-ap.cursor :as cursor])) - -;; 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? - + [malli.core :as mc])) (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"} + :admin-transaction-rule-table) + "hx-target" "#entity-table" + "hx-indicator" "#entity-table"} - [:fieldset.space-y-6 + [: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)))]})) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :vendor-search) + :id (str "vendor-search") + :value (:vendor (:parsed-query-params request)) + :value-fn :db/id + :content-fn :vendor/name})) (com/field {:label "Note"} (com/text-input {:name "note" :id "note" @@ -186,7 +175,7 @@ matching-count])) (def grid-page - (helper/build {:id "transaction-rule-table" + (helper/build {:id "entity-table" :nav (com/admin-aside-nav) :page-specific-nav filters :fetch-page fetch-page @@ -196,16 +185,12 @@ :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"} + :db/id (:db/id entity)))} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -224,7 +209,8 @@ {:key "bank-account" :name "Bank account" :sort-key "bank-account" - :render #(-> % :transaction-rule/bank-account :bank-account/name)} + :render #(-> % :transaction-rule/bank-account :bank-account/name) + :show-starting "lg"} {:key "description" :name "Description" :sort-key "description" @@ -237,7 +223,8 @@ (com/pill {:color :red} (format "more than $%.2f" amount-gte))) (when amount-lte - (com/pill {:color :primary} (format "less than $%.2f" amount-lte)))])} + (com/pill {:color :primary} (format "less than $%.2f" amount-lte)))]) + :show-starting "md"} {:key "note" :name "Note" :sort-key "note" @@ -272,42 +259,147 @@ (set)) bank-account-id)) +(defn validate-transaction-rule [form-params] + (doseq [[{:transaction-rule-account/keys [account location]} i] (map vector (:transaction-rule/accounts form-params) (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 form-params)) + + (let [total (reduce + 0.0 (map :transaction-rule-account/percentage (:transaction-rule/accounts form-params)))] + (when-not (dollars= 1.0 total) + (form-validation-error (format "Expense accounts total (%d%%) must add to 100%%" (int (* 100.0 total))) + :form form-params))) + + (when (and (:transaction-rule/bank-account form-params) + (not (bank-account-belongs-to-client? (:transaction-rule/bank-account form-params) + (:transaction-rule/client form-params)))) + (field-validation-error "does not belong to client" + [:transaction-rule/bank-account] + :form form-params))) + (defn transaction-rule-save [{:keys [form-params request-method identity] :as request}] + (validate-transaction-rule form-params) (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 form-params)) - - 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 form-params)) - - _ (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 form-params)) - - {:keys [tempids]} (audit-transact [[:upsert-entity entity]] (:identity request)) - updated-account (dc/pull (dc/db conn) + updated-rule (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))}))) + (row* identity updated-rule {:flash? true}) + :headers (cond-> {"hx-trigger" "modalclose"} + (= :post request-method) (assoc "hx-retarget" "#entity-table tbody" + "hx-reswap" "afterbegin") + (= :put request-method) (assoc "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" (:db/id updated-rule)) + "hx-reswap" "outerHTML"))))) + + + +(def transaction-read '[{:transaction/client [:client/name] + :transaction/bank-account [:bank-account/name]} + :transaction/description-original + [:transaction/date :xform clj-time.coerce/from-date]]) + +(defn transaction-rule-test [{:keys [form-params request-method identity] :as request + {:transaction-rule/keys [description client bank-account amount-lte amount-gte dom-lte dom-gte yodlee-merchant]} :form-params}] + (validate-transaction-rule form-params) + (let [valid-clients (extract-client-ids (:clients request) + client) + query (cond-> {:query {:find ['(pull ?e read)] + :in ['$ 'read] + :where []} + :args [(dc/db conn) transaction-read]} + description + (merge-query {:query {:in ['?descr] + :where ['[(iol-ion.query/->pattern ?descr) ?description-regex]]} + :args [description]}) + + valid-clients + (merge-query {:query {:in ['[?xx ...]] + :where ['[?e :transaction/client ?xx]]} + :args [(set valid-clients)]}) + + bank-account + (merge-query {:query {:in ['?bank-account-id] + :where ['[?e :transaction/bank-account ?bank-account-id]]} + :args [bank-account]}) + + description + (merge-query {:query {:where ['[?e :transaction/description-original ?do] + '[(re-find ?description-regex ?do)]]}}) + + amount-gte + (merge-query {:query {:in ['?amount-gte] + :where ['[?e :transaction/amount ?ta] + '[(>= ?ta ?amount-gte)]]} + :args [amount-gte]}) + + amount-lte + (merge-query {:query {:in ['?amount-lte] + :where ['[?e :transaction/amount ?ta] + '[(<= ?ta ?amount-lte)]]} + :args [amount-lte]}) + + dom-lte + (merge-query {:query {:in ['?dom-lte] + :where ['[?e :transaction/date ?transaction-date] + '[(iol-ion.query/dom ?transaction-date) ?dom] + '[(<= ?dom ?dom-lte)]]} + :args [dom-lte]}) + + dom-gte + (merge-query {:query {:in ['?dom-gte] + :where ['[?e :transaction/date ?transaction-date] + '[(iol-ion.query/dom ?transaction-date) ?dom] + '[(>= ?dom ?dom-gte)]]} + :args [dom-gte]}) + + client + (merge-query {:query {:in ['?client-id] + :where ['[?e :transaction/client ?client-id]]} + :args [client]}) + + + true + (merge-query {:query {:where ['[?e :transaction/id]]}})) + results (->> + (query2 query) + (map first))] + + (html-response + (com/modal-card {:class "fade-in transition duration-300"} + [:div.p-2.flex.space-x-4 [:div "Transaction Rule"] [:div ">"] [:div "Results"] [:div.ml-4.relative (com/badge {} (count results))]] + (com/data-grid + {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} "Bank") + (com/data-grid-header {} "Date") + (com/data-grid-header {} "Description")]} + (for [r (take 15 results)] + (com/data-grid-row + {} + (com/data-grid-cell {} (-> r :transaction/client :client/name)) + (com/data-grid-cell {} (-> r :transaction/bank-account :bank-account/name)) + (com/data-grid-cell {} (some-> r :transaction/date (atime/unparse-local atime/normal-date))) + (com/data-grid-cell {} (some-> r :transaction/description-original ))))) + [:div + (com/button (cond-> {:color :primary + :hx-vals (hx/json (:raw-form-params request)) + } + (:db/id form-params) (assoc :hx-put (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-save)) + (not (:db/id form-params)) (assoc :hx-post (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-save))) + "Save") + (com/button {:hx-vals (hx/json (:raw-form-params request)) + :hx-put (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-filled-account + )} "Back") + ]) + :headers (-> {} + (assoc "hx-retarget" ".modal-card") + (assoc "hx-reswap" "outerHTML"))))) @@ -315,334 +407,325 @@ [{:keys [ name account-location client-locations value]}] (com/select {:options (into [["" ""]] (cond account-location - [[account-location account-location]] + [[account-location account-location]] - (seq client-locations) - (into [["Shared" "Shared"]] - (for [cl client-locations] - [cl cl])) - :else - [["Shared" "Shared"]])) - :name name + (seq client-locations) + (into [["Shared" "Shared"]] + (for [cl client-locations] + [cl cl])) + :else + [["Shared" "Shared"]])) + :name name :value value :class "w-full"})) (defn- account-typeahead* - [{:keys [name value client-id]}] + [{:keys [name value client-id x-model]}] [: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)))})]) + :placeholder "Search..." + :url (str (bidi/path-for ssr-routes/only-routes :account-search) "?client-id=" client-id) + :id name + :x-model x-model + :value value + :content-fn (fn [value] + (:account/name (d-accounts/clientize (dc/pull (dc/db conn) d-accounts/default-read value) + client-id)))})]) (defn- transaction-rule-account-row* - [transaction-rule account] - (com/data-grid-row {} - (let [account-name (fc/field-name (:transaction-rule-account/account account))] - (list - (fc/with-field :db/id - (com/hidden {:name (fc/field-name) - :value (fc/field-value)})) - (fc/with-field :transaction-rule-account/account - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - [: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 (fc/field-value) - :client-id (:db/id (:transaction-rule/client transaction-rule)) - :name (fc/field-name)})]))) - (fc/with-field :transaction-rule-account/location - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - [:div [: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 (fc/field-name)}) - :hx-ext "rename-params" - :hx-rename-params-ex (hx/json {"transaction-rule/client" "client-id" - account-name "account-id" - "name" "name" - (fc/field-name) "value"}) - :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select) - :hx-swap "innerHTML"} - (location-select* {:name (fc/field-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 (fc/field-value)})]]))) - (fc/with-field :transaction-rule-account/percentage - (com/data-grid-cell - {} - (com/validated-field - {:errors (fc/field-errors)} - (com/money-input {:name (fc/field-name) - :class "w-16" - :value (some-> (fc/field-value) - (* 100 ) - (long ))})))))) - (com/data-grid-cell - (com/a-icon-button - {"_" (hiccup/raw "on click halt the event then transition the closest 's opacity to 0 then remove closest ") - :href "#"} - svg/x)))) - -(defn dialog* [& {:keys [ entity form-params form-errors]}] - (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"} + [account client-id client-locations] + (com/data-grid-row + (-> {:x-data (hx/json {:accountId (or (:db/id (fc/field-value (:transaction-rule-account/account account))) + (fc/field-value (:transaction-rule-account/account account))) + :location (fc/field-value (:transaction-rule-account/location account)) + :show (boolean (not (fc/field-value (:new? account))))}) + :data-key "show" + :x-ref "p"} + hx/alpine-mount-then-appear) + (let [account-name (fc/field-name (:transaction-rule-account/account account))] + (list - (fc/start-form entity form-errors - [:div.space-y-2 + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :transaction-rule-account/account + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + [:div {:hx-trigger "changed" + :hx-target "next div" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId, value: event.detail.accountId}" account-name) + :hx-get (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-account-typeahead)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data));"}] + (account-typeahead* {:value (fc/field-value) + :client-id client-id + :name (fc/field-name) + :x-model "accountId"})))) + (fc/with-field :transaction-rule-account/location + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors) + :x-data (hx/json {:location (fc/field-value)})} + [:div {:hx-trigger "changed" + :hx-target "next *" + :hx-swap "outerHTML" + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId || '', 'account-id': event.detail.accountId || '', value: event.detail.location}" (fc/field-name) ) + :hx-get (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-location-select) + :x-init "$watch('clientId', cid => $dispatch('changed', $data)); $watch('accountId', cid => $dispatch('changed', $data) )"}] + (location-select* {:name (fc/field-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 + :x-model "location" + :value (fc/field-value)})))) + (fc/with-field :transaction-rule-account/percentage + (com/data-grid-cell + {} + (com/validated-field + {:errors (fc/field-errors)} + (com/money-input {:name (fc/field-name) + :class "w-16" + :value (some-> (fc/field-value) + (* 100 ) + (long ))})))))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) + +(defn dialog* [{:keys [entity form-params form-errors]}] + (fc/start-form form-params form-errors + (com/modal + {:modal-class "max-w-2xl" + :hx-target "this"} + + [:form {:hx-ext "response-targets" + :hx-target-400 "#form-errors .error-content" + :x-trap "true" + :class "w-full h-full" + (if (:db/id entity) + :hx-put + :hx-post) (str (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-edit-save))} + (com/modal-card + {} + [:div.flex [:div.p-2 "Transaction Rule"]] + [:fieldset {:class "hx-disable" + :x-data (hx/json {:clientId (or (:db/id (:transaction-rule/client form-params)) + (:transaction-rule/client form-params) + (:db/id (:transaction-rule/client entity)))})} + + [:div.space-y-1 (when-let [id (:db/id entity)] (com/hidden {:name "db/id" :value id})) - - (fc/with-field :transaction-rule/client - - (com/validated-field - {:label "Client" - :errors (fc/field-errors)} - [:div.w-96 - (com/typeahead {:name (fc/field-name) - :error? (fc/error?) - :class "w-96" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :company-search) - :id (str "form-client-search") - :value (fc/field-value) - :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))})])) - - - (fc/with-field :transaction-rule/bank-account - (com/validated-field {:label "Bank Account" - :errors (fc/field-errors)} - [: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 (fc/field-name)}) - :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 (fc/field-name) - :value (fc/field-value)})])) - (fc/with-field :transaction-rule/description (com/validated-field {:label "Description" - :errors (fc/field-errors)} - (com/text-input {:name (fc/field-name) - :error? (fc/error?) - :placeholder "HOME DEPOT" - :class "w-96" - :value (fc/field-value)}))) + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :error? (fc/error?) + :x-init "$el.focus()" + :placeholder "HOME DEPOT" + :class "w-96" + :value (fc/field-value)}))) + [:div.filters {:x-data (hx/json {:clientFilter (boolean (fc/field-value (:transaction-rule/client fc/*current*))) + :bankAccountFilter (boolean (fc/field-value (:transaction-rule/bank-account fc/*current*))) + :amountFilter (boolean (or (fc/field-value (:transaction-rule/amount-gte fc/*current*)) + (fc/field-value (:transaction-rule/amount-lte fc/*current*)))) + :domFilter (boolean (or (fc/field-value (:transaction-rule/dom-gte fc/*current*)) + (fc/field-value (:transaction-rule/dom-lte fc/*current*))))})} - (com/field {:label "Amount"} - [:div.flex.gap-2 - (fc/with-field :transaction-rule/amount-gte - [:div.flex.flex-col - (com/money-input {:name (fc/field-name) + [:div.flex.gap-2.mb-2 + (com/a-button {"@click" "clientFilter=true" + "x-show" "!clientFilter"} "Filter client") + (com/a-button {"@click" "bankAccountFilter=true" + "x-show" "clientFilter && !bankAccountFilter"} "Filter bank account") + (com/a-button {"@click" "amountFilter=true" + "x-show" "!amountFilter"} "Filter amount") + (com/a-button {"@click" "domFilter=true" + "x-show" "!domFilter"} "Filter day of month")] + (fc/with-field :transaction-rule/client + + (com/validated-field + (-> {:label "Client" + :errors (fc/field-errors) + :x-show "clientFilter"} + (hx/alpine-appear)) + [:div.w-96 + (com/typeahead {:name (fc/field-name) + :error? (fc/error?) + :class "w-96" + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :company-search) + :x-model "clientId" + :value (fc/field-value) + :content-fn (fn [c] (pull-attr (dc/db conn) :client/name c))})])) + + (fc/with-field :transaction-rule/bank-account + (com/validated-field + (-> {:label "Bank Account" + :errors (fc/field-errors) + :x-show "bankAccountFilter"} + hx/alpine-appear) + [:div.w-96 + [:div#bank-account-changer {:hx-get (bidi/path-for ssr-routes/only-routes :bank-account-typeahead) + :hx-trigger "changed" + :hx-target "next *" + :hx-include "#bank-account-changer" + :hx-swap "innerHTML" + + :hx-vals (format "js:{name: '%s', 'client-id': event.detail.clientId}" (fc/field-name)) + :x-init "$watch('clientId', cid => $dispatch('changed', $data))"}] + + (bank-account-typeahead* {:client-id (:transaction-rule/client form-params) + :name (fc/field-name) + :value (fc/field-value)})])) + + (com/field (-> {:label "Amount" + :x-show "amountFilter"} + hx/alpine-appear) + [:div.flex.gap-2 + (fc/with-field :transaction-rule/amount-gte + [:div.flex.flex-col + (com/money-input {:name (fc/field-name) + :placeholder ">=" + :class "w-24" + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})]) + (fc/with-field :transaction-rule/amount-lte + [:div.flex.flex-col + (com/money-input {:name (fc/field-name) + :placeholder "<=" + :class "w-24" + :value (fc/field-value)}) + (com/errors {:errors (fc/field-errors)})])]) + + (com/field (-> {:label "Day of month" + :x-show "domFilter"} + hx/alpine-appear) + [:div.flex.gap-2 + (fc/with-field :transaction-rule/dom-gte + (com/validated-field + {:errors (fc/field-errors)} + (com/int-input {:name (fc/field-name) :placeholder ">=" :class "w-24" - :value (fc/field-value)}) - (com/errors {:errors (fc/field-errors)})]) - (fc/with-field :transaction-rule/amount-lte - [:div.flex.flex-col - (com/money-input {:name (fc/field-name) - :placeholder "<=" + :value (fc/field-value)}))) + (fc/with-field :transaction-rule/dom-lte + (com/validated-field + {:errors (fc/field-errors)} + (com/int-input {:name (fc/field-name) + :placeholder ">=" :class "w-24" - :value (fc/field-value)}) - (com/errors {:errors (fc/field-errors)})])]) - - (com/field {:label "Day of month"} - [:div.flex.gap-2 - (fc/with-field :transaction-rule/dom-gte - (com/validated-field - {:errors (fc/field-errors)} - (com/int-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)}))) - (fc/with-field :transaction-rule/dom-lte - (com/validated-field - {:errors (fc/field-errors)} - (com/int-input {:name (fc/field-name) - :placeholder ">=" - :class "w-24" - :value (fc/field-value)})))]) + :value (fc/field-value)})))])] [:h2.text-lg "Outcomes"] (fc/with-field :transaction-rule/vendor - (com/validated-field {:label "Assign Vendor" + (com/validated-field {:label "Assign Vendor" :errors (fc/field-errors)} [:div.w-96 (com/typeahead {:name (fc/field-name) - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes :vendor-search) - :id (str "form-vendor-search") - :value (fc/field-value) - :value-fn (some-fn :db/id identity) - :content-fn (some-fn :vendor/name #(pull-attr (dc/db conn) :vendor/name %))})])) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes :vendor-search) + :class "w-96" + :value (fc/field-value) + :content-fn #(pull-attr (dc/db conn) :vendor/name %)})])) (fc/with-field :transaction-rule/accounts - (list - (com/data-grid {:headers [(com/data-grid-header {} - "Account") - (com/data-grid-header {:class "w-32"} "Location") - (com/data-grid-header {:class "w-16"} "%") - (com/data-grid-header {:class "w-16"})] - :id "transaction-rule-account-table"} - (when @fc/*current* - (doall (for [tra fc/*current*] - (fc/with-cursor tra - (transaction-rule-account-row* entity tra)))))) - (com/errors {:errors (fc/field-errors)}))) - (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" - "index" "index"}) - :hx-vals (hiccup/raw "js:{index: countRows(\"#transaction-rule-account-table\")}") - :hx-target "#transaction-rule-account-table tbody" - :hx-swap "beforeend"} - "New account") + (com/validated-field + {:errors (fc/field-errors)} + (let [client-locations (some->> form-params :transaction-rule/client (pull-attr (dc/db conn) :client/locations))] + (com/data-grid {:headers [(com/data-grid-header {} "Account") + (com/data-grid-header {:class "w-32"} "Location") + (com/data-grid-header {:class "w-16"} "%") + (com/data-grid-header {:class "w-16"})]} + (fc/cursor-map #(transaction-rule-account-row* % (:transaction-rule/client form-params) client-locations)) + (com/data-grid-new-row {:colspan 4 + :hx-get (bidi/path-for ssr-routes/only-routes + :admin-transaction-rule-new-account) + :index (count (fc/field-value)) + :tr-params (hx/bind-alpine-vals {} {:client-id "clientId"})} + "New account"))))) + (fc/with-field :transaction-rule/transaction-approval-status - (com/validated-field {:label "Approval status" + (com/validated-field {:label "Approval status" :errors (fc/field-errors)} - (com/radio {:options (ref->radio-options "transaction-approval-status") - :value (fc/field-value) - :name (fc/field-name)}))) - - [:div#form-errors [:span.error-content - (com/errors {:errors (:errors fc/*form-errors*)})]] - (com/button {:color :primary :form "edit-form" :type "submit"} - "Save")])]] - [:div]))) + (com/radio {:options (ref->radio-options "transaction-approval-status") + :value (fc/field-value) + :name (fc/field-name) + :size :small + :orientation :horizontal}))) + + ]] + [:div + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors form-errors} "Save rule") + (com/validated-save-button {:errors form-errors :color :secondary + :hx-post (bidi/path-for ssr-routes/only-routes :admin-transaction-rule-test)} + + "Test rule")])]))) + (defn new-account [{{:keys [client-id index]} :query-params}] - (let [index (or index 0) ;; TODO schema decode is not working - transaction-rule {:transaction-rule/client (dc/pull (dc/db conn) '[:client/name :client/locations :db/id] - client-id) - :transaction-rule/accounts (conj (into [] (repeat (inc index) {} )) - {:db/id (str (java.util.UUID/randomUUID)) - :transaction-rule-account/location "Shared"})}] - (html-response - (fc/start-form transaction-rule [] - (fc/with-cursor (get-in fc/*current* [:transaction-rule/accounts index]) - (transaction-rule-account-row* - ;; TODO store a pointer to the "head " cursor for errors instead of nesting them - ;; makes it so you don't have to do this - transaction-rule - fc/*current*)))))) + (html-response + (fc/start-form-with-prefix + [:transaction-rule/accounts (or index 0)] + {:db/id (str (java.util.UUID/randomUUID)) + :transaction-rule-account/location "Shared" + :new? true} + [] + (transaction-rule-account-row* + fc/*current* + client-id + (some->> client-id (pull-attr (dc/db conn) :client/locations) client-id))))) -;; TODO check to see if it should be called "Shared" or "shared" for the value -;; TODO blank location is being allowed -;; TODO hydrate nested types more easily. make it easy to hydrate so you don't do weird sometimes pulls -;; TODO is it possible to make it easy to get a virtual cursor in the case of adding a new row? setting up -;; fake data doesn't feel right - maybe have a "prelude" that's dynamic (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)) + (pull-attr (dc/db conn) :account/location)) :client-locations (some->> client-id - (pull-attr (dc/db conn) :client/locations))}))) + (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})))) + (html-response (account-typeahead* {:name name + :value value + :client-id client-id + :x-model "accountId"}))) -(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"}))) +(def form-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 {:min 1 :error/message "required"}]] + [:transaction-rule-account/percentage percentage])]])) -(defn transaction-rule-error [request] - (let [entity (some-> request :last-form)] - (html-response (dialog* :entity entity - :form-errors (:form-errors request) - :form-params (if (:db/id entity) - {:hx-put (str (bidi/path-for ssr-routes/only-routes - :admin-transaction-rule-edit-save))} - {:hx-post (str (bidi/path-for ssr-routes/only-routes - :admin-transaction-rule-edit-save))})) - :headers {"hx-retarget" "#edit-form fieldset" - "hx-reselect" "#edit-form fieldset"}))) +(defn transaction-dialog [{:keys [entity form-params form-errors]}] + (clojure.pprint/pprint form-params) + (modal-response (dialog* {:entity entity + :form-params (or (when (seq form-params) + form-params) + (when entity + (mc/decode form-schema entity main-transformer)) + {}) + :form-errors form-errors}))) -(defn transaction-rule-new-dialog [_] - (html-response (dialog* :entity {} - :form-errors {} - :form-params {:hx-post (str (bidi/path-for ssr-routes/only-routes - :admin-transaction-rule-edit-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 {:min 1 :error/message "required"}]] - [:transaction-rule-account/percentage percentage])]])) (def key->handler (apply-middleware-to-all-handlers @@ -671,12 +754,28 @@ [: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-entity [:form-params :db/id] default-read) + (wrap-schema-decode :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 (-> transaction-dialog + (wrap-entity [:form-params :db/id] default-read)))) + + :admin-transaction-rule-test (-> transaction-rule-test + (wrap-entity [:form-params :db/id] default-read) + (wrap-schema-decode :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 (-> transaction-dialog + (wrap-entity [:form-params :db/id] default-read)))) + :admin-transaction-rule-filled-account (-> transaction-dialog + (wrap-entity [:form-params :db/id] default-read) + (wrap-schema-decode :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 (-> transaction-dialog + (wrap-entity [:form-params :db/id] default-read)))) + :admin-transaction-rule-edit-dialog (-> transaction-dialog + (wrap-entity [:route-params :db/id] default-read) (wrap-schema-decode :route-schema [:map [:db/id entity-id]])) - :admin-transaction-rule-new-dialog transaction-rule-new-dialog}) + :admin-transaction-rule-new-dialog transaction-dialog}) (fn [h] (-> h (wrap-admin) diff --git a/src/clj/auto_ap/ssr/company.clj b/src/clj/auto_ap/ssr/company.clj index 3891bb3a..aa1a19d2 100644 --- a/src/clj/auto_ap/ssr/company.clj +++ b/src/clj/auto_ap/ssr/company.clj @@ -95,7 +95,7 @@ selected-client-id) :client/bank-accounts (filter (fn [{:keys [bank-account/name]}] - (str/includes? (str/upper-case name) + (str/includes? (or (some-> name str/upper-case) "") (or (some-> query-params (get "q") str/upper-case) @@ -109,14 +109,13 @@ (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 %))}) + :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}]])) diff --git a/src/clj/auto_ap/ssr/company/company_1099.clj b/src/clj/auto_ap/ssr/company/company_1099.clj index f994d169..ecf21bf2 100644 --- a/src/clj/auto_ap/ssr/company/company_1099.clj +++ b/src/clj/auto_ap/ssr/company/company_1099.clj @@ -1,17 +1,32 @@ (ns auto-ap.ssr.company.company-1099 (:require - [auto-ap.datomic :refer [apply-pagination-raw conn remove-nils]] - [auto-ap.graphql.utils - :refer [assert-can-see-client extract-client-ids is-admin?]] + [auto-ap.datomic :refer [apply-pagination-raw conn]] + [auto-ap.graphql.utils :refer [assert-can-see-client]] + [auto-ap.routes.utils + :refer [wrap-client-redirect-unauthenticated wrap-secure]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [auto-ap.ssr.grid-page-helper :as helper] + [auto-ap.ssr.nested-form-params :refer [wrap-nested-form-params]] [auto-ap.ssr.svg :as svg] - [auto-ap.ssr.utils :refer [form-data->map html-response path->name]] + [auto-ap.ssr.utils + :refer [apply-middleware-to-all-handlers + entity-id + html-response + main-transformer + modal-response + ref->enum-schema + ref->select-options + strip + wrap-entity + wrap-form-4xx-2 + wrap-schema-decode]] [bidi.bidi :as bidi] - [cemerick.url :as url] [clojure.string :as str] - [datomic.api :as dc])) + [datomic.api :as dc] + [hiccup.util :refer [url]] + [malli.core :as mc])) (def vendor-read '[:db/id :vendor/name @@ -74,7 +89,7 @@ (def grid-page (helper/build - {:id "vendor-table" + {:id "entity-table" :nav (com/company-aside-nav) :id-fn (comp :db/id second) :fetch-page fetch-page @@ -89,14 +104,10 @@ :entity-name "Vendors" :route :company-1099-vendor-table :row-buttons (fn [request e] - [(com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes + [(com/icon-button {:hx-get (url (bidi/path-for ssr-routes/only-routes :company-1099-vendor-dialog :vendor-id (:db/id (second e))) - "?" - (url/map->query {:client-id (:db/id (first e))})) - :hx-ext "debug" - :hx-target "#modal-holder" - :hx-swap "outerHTML"} + {:client-id (:db/id (first e))})} svg/pencil)]) :headers [{:key "Client" :name "Client" @@ -161,117 +172,213 @@ (def table* (partial helper/table* grid-page)) (def row* (partial helper/row* grid-page)) -(defn vendor-save [{:keys [form-params identity route-params query-params] :as request}] - (let [client-id (Long/parseLong (get query-params "client-id")) - vendor-id (Long/parseLong (:vendor-id route-params))] - (assert-can-see-client identity client-id) - @(dc/transact conn [(remove-nils - (-> (form-data->map form-params) - (assoc :db/id (Long/parseLong (:vendor-id route-params))) - (update :vendor/legal-entity-1099-type #(some->> % not-empty (keyword "legal-entity-1099-type"))) - (update :vendor/legal-entity-tin-type #(some->> % not-empty (keyword "legal-entity-tin-type")))))]) - (html-response +(defn vendor-save [{:keys [form-params identity route-params query-params request-method] :as request + {:keys [vendor-id]} :route-params + {:keys [client-id]} :query-params}] - (row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id) - (dc/pull (dc/db conn) vendor-read vendor-id) - (sum-for-client-vendor client-id vendor-id) - ] {:flash? true}) - :headers {"hx-trigger" "closeModal"}))) -(defn vendor-dialog [request] - (let [vendor (dc/pull (dc/db conn) '[* {:vendor/legal-entity-1099-type [:db/ident] - :vendor/legal-entity-tin-type [:db/ident]}] (Long/parseLong (:vendor-id (:params request))))] ;; TODO perms - (html-response + (assert-can-see-client identity client-id) + + @(dc/transact conn [[:upsert-entity (-> form-params + (assoc :db/id (:vendor-id route-params)) + (update :vendor/address (fn [a] + (if (or (:address/street1 a) + (:address/street2 a) + (:address/city a) + (:address/state a) + (:address/zip a) + (:db/id a)) + a + nil)) ))]]) + (html-response + + (row* identity [(dc/pull (dc/db conn) [:db/id :client/code] client-id) + (dc/pull (dc/db conn) vendor-read vendor-id) + (sum-for-client-vendor client-id vendor-id) + ] {:flash? true}) + :headers {"hx-trigger" "modalclose" + "hx-retarget" (format "#entity-table tr[data-id=\"%d\"]" vendor-id)})) + +(def default-vendor-read '[* {[:vendor/legal-entity-1099-type :xform iol-ion.query/ident] [:db/ident] + [:vendor/legal-entity-tin-type :xform iol-ion.query/ident] [:db/ident]}]) + + +(def form-schema (mc/schema [:map + [:vendor/address {:default {}} + [:maybe + [:map + [:db/id {:optional true} [:maybe entity-id]] + [:address/street1 {:optional true} [:maybe [:string {:decode/string strip}]]] + [:address/street2 {:optional true} [:maybe [:string {:decode/string strip}]]] + [:address/city {:optional true} [:maybe [:string {:decode/string strip}]]] + [:address/state {:optional true} [:maybe [:string {:decode/string strip}]]] + [:address/zip {:optional true} [:maybe [:re { :error/message "invalid zip" + :decode/string strip} #"^(\d{5}|)$"]]]]]] + [:vendor/legal-entity-name {:optional true} [:maybe [:string {:decode/string strip}]]] + [:vendor/legal-entity-first-name {:optional true} [:maybe [:string {:decode/string strip}]]] + [:vendor/legal-entity-middle-name {:optional true} [:maybe [:string {:decode/string strip}]]] + [:vendor/legal-entity-last-name {:optional true} [:maybe [:string {:decode/string strip}]]] + [:vendor/legal-entity-tin {:optional true} [:maybe [:string {:decode/string strip}]]] + [:vendor/legal-entity-tin-type {:optional true} [:maybe (ref->enum-schema "legal-entity-tin-type")]] + [:vendor/legal-entity-1099-type {:optional true} [:maybe (ref->enum-schema "legal-entity-1099-type")]]])) + +(defn vendor-dialog [{{:keys [client-id]} :query-params {:keys [vendor-id]} :route-params :keys [entity form-params form-errors]}] + (fc/start-form (or (when (seq form-params) + form-params) + (when entity + (mc/decode form-schema entity main-transformer)) + {}) + form-errors + (modal-response (com/modal {} - [:form {:hx-post (str (bidi/path-for ssr-routes/only-routes - :company-1099-vendor-save - :request-method :post - :vendor-id (Long/parseLong (:vendor-id (:params request)))) - "?" - (url/map->query {:client-id (:client-id (:params request))})) - :hx-target (format "#vendor-table tr[data-id=\"%d\"]" (:db/id vendor)) - :hx-swap "outerHTML swap:300ms"} - [:fieldset {:class "hx-disable"} + [:form {:hx-post (url (bidi/path-for ssr-routes/only-routes + :company-1099-vendor-save + :request-method :post + :vendor-id vendor-id) + {:client-id client-id}) + :class "w-full h-full max-w-2xl" + :hx-swap "outerHTML swap:300ms"} + + [:fieldset {:class "hx-disable w-full h-full"} (com/modal-card {} - [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name vendor)]] - [:div.space-y-6 - [:div.grid.grid-cols-6.gap-4 - - [:h4.text-xl.border-b.col-span-6 "Address"] - [:div.col-span-6 - (com/field {:label "Street 1"} - (com/text-input {:name (path->name [:vendor/address :address/street1]) - :value (-> vendor :vendor/address :address/street1) - :placeholder "1700 Pennsylvania Ave" - :autofocus true}))] - [:div.col-span-6 - (com/field {:label "Street 2"} - (com/text-input {:name (path->name [:vendor/address :address/street2]) - :value (-> vendor :vendor/address :address/street2) - :placeholder "Suite 200"}))] - [:div.col-span-3 - (com/field {:label "City"} - (com/text-input {:name (path->name [:vendor/address :address/city]) - :value (-> vendor :vendor/address :address/city) - :placeholder "Cupertino"}))] - [:div.col-span-1 - (com/field {:label "State"} - (com/text-input {:name (path->name [:vendor/address :address/state]) - :value (-> vendor :vendor/address :address/state) - :placeholder "CA"}))] - [:div.col-span-2 - (com/field {:label "Zip"} - (com/text-input {:name (path->name [:vendor/address :address/zip]) - :value (-> vendor :vendor/address :address/zip) - :placeholder "98102"}))] - [:h4.text-xl.border-b.col-span-6 "Legal Entity"] - [:div.col-span-6 - (com/field {:label "Legal Entity Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-name]) - :value (-> vendor :vendor/legal-entity-name) - :placeholder "Good Restaurant LLC"}))] - [:div.col-span-6.text-center " - OR -"] - [:div.col-span-2 - (com/field {:label "First Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-first-name]) - :value (-> vendor :vendor/legal-entity-first-name) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "Middle Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-middle-name]) - :value (-> vendor :vendor/legal-entity-middle-name) - :placeholder "C."}))] - [:div.col-span-2 - (com/field {:label "Last Name"} - (com/text-input {:name (path->name [:vendor/legal-entity-last-name]) - :value (-> vendor :vendor/legal-entity-last-name) - :placeholder "Riley"}))] - [:div.col-span-2 - (com/field {:label "TIN"} - (com/text-input {:name (path->name [:vendor/legal-entity-tin]) - :value (-> vendor :vendor/legal-entity-tin) - :placeholder "John"}))] - [:div.col-span-2 - (com/field {:label "TIN Type"} - (com/select {:name (path->name [:vendor/legal-entity-tin-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-tin-type :db/ident name) - :options [["ein" "EIN"] - ["ssn" "SSN"]]}))] - [:div.col-span-2 - (com/field {:label "1099 Type"} - (com/select {:name (path->name [:vendor/legal-entity-1099-type]) - :allow-blank? true - :value (some-> vendor :vendor/legal-entity-1099-type :db/ident name) - :options [["none" "None"] - ["misc" "Misc"] - ["landlord" "Landlord"]]}))] - [:div.col-span-6 - (com/button {:color :primary} - "Save")]]] - [:div])]])))) + [:div.flex [:div.p-2 "Vendor 1099 Info"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:vendor/name entity)]] + [:div.grid.grid-cols-6.gap-x-4.gap-y-2 + + (fc/with-field-default :vendor/address {} + (println "ADDRESS" fc/*current*) + (list [:h4.text-xl.border-b.col-span-6 "Address"] + [:div.col-span-6 + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + + (fc/with-field :address/street1 + (com/validated-field {:label "Street 1" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "1700 Pennsylvania Ave" + :autofocus true})))] + [:div.col-span-6 + (fc/with-field :address/street2 + (com/validated-field {:label "Street 2" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "Suite 200"})))] + [:div.col-span-3 + (fc/with-field :address/city + (com/validated-field {:label "City" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "Cupertino"})))] + [:div.col-span-1 + (fc/with-field :address/state + (com/validated-field {:label "State" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "CA"})))] + [:div.col-span-2 + (fc/with-field :address/zip + (com/validated-field {:label "Zip" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "98102"})))])) + + [:h4.text-xl.border-b.col-span-6 "Legal Entity"] + [:div.col-span-6 + (fc/with-field :vendor/legal-entity-name + (com/validated-field {:label "Legal Entity Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :class "w-full" + :value (fc/field-value) + :placeholder "Good Restaurant LLC"})))] + [:div.col-span-6.text-center " - OR -"] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-first-name + (com/validated-field {:label "First Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "John"})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-middle-name + (com/validated-field {:label "Middle Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "C."})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-last-name + (com/validated-field {:label "Last Name" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "Riley"})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-tin + (com/validated-field {:label "TIN" + :errors (fc/field-errors)} + (com/text-input {:name (fc/field-name) + :value (fc/field-value) + :placeholder "John"})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-tin-type + (com/validated-field {:label "TIN Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :allow-blank? true + :value (some-> (fc/field-value) name) + :options [["ein" "EIN"] + ["ssn" "SSN"]]})))] + [:div.col-span-2 + (fc/with-field :vendor/legal-entity-1099-type + (com/validated-field {:label "1099 Type" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :allow-blank? true + :value (some-> (fc/field-value) name) + :options (ref->select-options "legal-entity-1099-type")})))]] + [:div + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors form-errors} "Save vendor")])]])))) (def vendor-table (helper/table-route grid-page)) (def page (helper/page-route grid-page)) + +(def key->handler + (apply-middleware-to-all-handlers + (->> + { + :company-1099 page + :company-1099-vendor-table vendor-table + :company-1099-vendor-dialog (-> vendor-dialog + (wrap-entity [:route-params :vendor-id] default-vendor-read) + (wrap-schema-decode :route-schema [:map [:vendor-id entity-id]] + :query-schema [:map [:client-id entity-id]])) + :company-1099-vendor-save (-> vendor-save + (wrap-entity [:form-params :db/id] default-vendor-read) + (wrap-schema-decode :form-schema form-schema + :route-schema [:map [:vendor-id entity-id]] + :query-schema [:map [:client-id entity-id]]) + (wrap-nested-form-params) + (wrap-form-4xx-2 (-> vendor-dialog + (wrap-entity [:form-params :db/id] default-vendor-read) + (wrap-entity [:route-params :vendor-id] default-vendor-read) + (wrap-schema-decode :route-schema [:map [:vendor-id entity-id]] + :query-schema [:map [:client-id entity-id]]))))}) + (fn [h] + (-> h + (wrap-secure) + (wrap-client-redirect-unauthenticated))))) diff --git a/src/clj/auto_ap/ssr/company/yodlee.clj b/src/clj/auto_ap/ssr/company/yodlee.clj index 699c40fa..7bae6fa2 100644 --- a/src/clj/auto_ap/ssr/company/yodlee.clj +++ b/src/clj/auto_ap/ssr/company/yodlee.clj @@ -208,7 +208,6 @@ fastlink.open({fastLinkURL: '%s', (def page (helper/page-route grid-page)) (def table (helper/table-route grid-page)) -;; TODO delete-after-settle (defn refresh-provider-account [{:keys [form-params identity]}] (let [provider-account (dc/pull (dc/db conn) default-read (some-> (get form-params "id") not-empty Long/parseLong))] (yodlee/refresh-provider-account (:client/code (:yodlee-provider-account/client provider-account)) diff --git a/src/clj/auto_ap/ssr/components.clj b/src/clj/auto_ap/ssr/components.clj index e7f6782d..7e01d7c7 100644 --- a/src/clj/auto_ap/ssr/components.clj +++ b/src/clj/auto_ap/ssr/components.clj @@ -15,6 +15,7 @@ (def breadcrumbs breadcrumbs/breadcrumbs-) (def button buttons/button-) +(def validated-save-button buttons/validated-save-button-) (def a-button buttons/a-button-) (def button-icon buttons/button-icon-) (def icon-button buttons/icon-button-) @@ -35,6 +36,7 @@ (def field inputs/field-) (def validated-field inputs/validated-field-) (def errors inputs/errors-) +(def form-errors inputs/form-errors-) (def left-aside aside/left-aside-) (def company-aside-nav aside/company-aside-nav-) @@ -57,6 +59,7 @@ (def data-grid-row data-grid/row-) (def data-grid-cell data-grid/cell-) (def data-grid-right-stack-cell data-grid/right-stack-cell-) +(def data-grid-new-row data-grid/new-row-) (defn link [params & children] (into [:a (update params :class str " font-medium text-blue-600 dark:text-blue-500 hover:underline ")] diff --git a/src/clj/auto_ap/ssr/components/aside.clj b/src/clj/auto_ap/ssr/components/aside.clj index 4410027e..075e7163 100644 --- a/src/clj/auto_ap/ssr/components/aside.clj +++ b/src/clj/auto_ap/ssr/components/aside.clj @@ -3,7 +3,9 @@ [hiccup2.core :as hiccup] [bidi.bidi :as bidi] [auto-ap.ssr-routes :as ssr-routes] - [auto-ap.client-routes :as client-routes])) + [auto-ap.client-routes :as client-routes] + [auto-ap.ssr.hx :as hx] + [auto-ap.ssr.hiccup-helper :as hh])) (defn menu-button- [params & children] [:div @@ -13,140 +15,47 @@ (update :class str " cursor-pointer flex items-center p-2 w-full text-xs text-gray-600 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700") (assoc :hx-indicator "find .htmx-indicator") (assoc :hx-boost "true") - (assoc :hx-select "#app-contents") - (assoc :hx-target "#app-contents") - (assoc :hx-swap "outerHTML")) + (assoc :hx-select "#app") + (assoc :hx-target "#app") + (assoc :hx-swap "innerHTML")) (when (:icon params) [:span {:class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 group-hover:text-blue-500 dark:text-gray-400 group-hover:scale-110 dark:group-hover:text-white mr-3"} (:icon params)]) (into [:span {:class "flex-1 text-left whitespace-nowrap text-gray-600 dark:text-white"}] children) - (when (:data-collapse-toggle params) + (when (get params "@click") [:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [:path {:fill-rule "evenodd", :d "M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z", :clip-rule "evenodd"}]]) [:div.htmx-indicator.flex.items-center (svg/spinner-primary {:class "inline w-4 h-4 text-white"})]]]) (defn sub-menu- [params & children] - [:ul {:id (:id params) :class "hidden py-2 space-y-1.5"} + [:ul (update params + :class (fnil hh/add-class "")"py-2 space-y-1.5") (for [c children] [:li (update-in c [1 1 :class ] str " flex items-center p-2 pl-11 w-full text-base font-normal text-gray-900 rounded-lg transition duration-75 group hover:bg-gray-100 dark:text-white dark:hover:bg-gray-700")])]) (defn left-aside- [{:keys [nav page-specific]} & children] - [:aside {:id "left-nav", :class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform -translate-x-full lg:translate-x-0", :aria-labelledby "left-nav" :aria-hidden "true" - "_" (hiccup/raw "init call initSidebarToggle()")} + [:aside {:id "left-nav", + :class "fixed top-0 left-0 pt-16 z-20 w-64 h-screen transition-transform -translate-x-full lg:translate-x-0", + "x-transition:enter" "transition duration-500" + "x-transition:enter-start" "lg:-translate-x-full" + "x-transition:enter-end" " lg:translate-x-0" + "x-transition:leave" "transition duration-500" + "x-transition:leave-start" "lg:translate-x-0" + "x-transition:leave-end" " lg:-translate-x-full" + + :aria-labelledby "left-nav" + :x-show "leftNavShow" + ":aria-hidden" "leftNavShow ? 'false' : 'true'"} [:div {:class "overflow-y-auto py-5 px-3 h-full bg-gray-50 border-r border-gray-200 dark:bg-gray-800 dark:border-gray-700"} nav - [:ul {:class "pt-5 mt-5 space-y-2 border-t border-gray-200 dark:border-gray-700"} - #_[:li - [:a {:href "#", :class "flex items-center p-2 text-base font-normal text-gray-900 rounded-lg transition duration-75 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white group"} - [:svg {:aria-hidden "true", :class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:d "M9 2a1 1 0 000 2h2a1 1 0 100-2H9z"}] - [:path {:fill-rule "evenodd", :d "M4 5a2 2 0 012-2 3 3 0 003 3h2a3 3 0 003-3 2 2 0 012 2v11a2 2 0 01-2 2H6a2 2 0 01-2-2V5zm3 4a1 1 0 000 2h.01a1 1 0 100-2H7zm3 0a1 1 0 000 2h3a1 1 0 100-2h-3zm-3 4a1 1 0 100 2h.01a1 1 0 100-2H7zm3 0a1 1 0 100 2h3a1 1 0 100-2h-3z", :clip-rule "evenodd"}]] - [:span {:class "ml-3"} "Docs"]]] - #_[:li - [:a {:href "#", :class "flex items-center p-2 text-base font-normal text-gray-900 rounded-lg transition duration-75 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white group"} - [:svg {:aria-hidden "true", :class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:d "M7 3a1 1 0 000 2h6a1 1 0 100-2H7zM4 7a1 1 0 011-1h10a1 1 0 110 2H5a1 1 0 01-1-1zM2 11a2 2 0 012-2h12a2 2 0 012 2v4a2 2 0 01-2 2H4a2 2 0 01-2-2v-4z"}]] - [:span {:class "ml-3"} "Components"]]] - #_[:li - [:a {:href "#", :class "flex items-center p-2 text-base font-normal text-gray-900 rounded-lg transition duration-75 hover:bg-gray-100 dark:hover:bg-gray-700 dark:text-white group"} - [:svg {:aria-hidden "true", :class "flex-shrink-0 w-6 h-6 text-gray-400 transition duration-75 dark:text-gray-400 group-hover:text-gray-900 dark:group-hover:text-white", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:fill-rule "evenodd", :d "M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-2 0c0 .993-.241 1.929-.668 2.754l-1.524-1.525a3.997 3.997 0 00.078-2.183l1.562-1.562C15.802 8.249 16 9.1 16 10zm-5.165 3.913l1.58 1.58A5.98 5.98 0 0110 16a5.976 5.976 0 01-2.516-.552l1.562-1.562a4.006 4.006 0 001.789.027zm-4.677-2.796a4.002 4.002 0 01-.041-2.08l-.08.08-1.53-1.533A5.98 5.98 0 004 10c0 .954.223 1.856.619 2.657l1.54-1.54zm1.088-6.45A5.974 5.974 0 0110 4c.954 0 1.856.223 2.657.619l-1.54 1.54a4.002 4.002 0 00-2.346.033L7.246 4.668zM12 10a2 2 0 11-4 0 2 2 0 014 0z", :clip-rule "evenodd"}]] - [:span {:class "ml-3"} "Help"]]]] - page-specific] - #_[:div {:class "hidden absolute bottom-0 left-0 justify-center p-4 space-x-4 w-full lg:flex bg-white dark:bg-gray-800 z-20 border-r border-gray-200 dark:border-gray-700"} - [:a {:href "#", :class "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:text-gray-400 hover:text-gray-900 dark:hover:text-white hover:bg-gray-100 dark:hover:bg-gray-600"} - [:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:d "M5 4a1 1 0 00-2 0v7.268a2 2 0 000 3.464V16a1 1 0 102 0v-1.268a2 2 0 000-3.464V4zM11 4a1 1 0 10-2 0v1.268a2 2 0 000 3.464V16a1 1 0 102 0V8.732a2 2 0 000-3.464V4zM16 3a1 1 0 011 1v7.268a2 2 0 010 3.464V16a1 1 0 11-2 0v-1.268a2 2 0 010-3.464V4a1 1 0 011-1z"}]]] - [:a {:href "#", :data-tooltip-target "tooltip-settings", :class "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:text-gray-400 dark:hover:text-white hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600"} - [:svg {:aria-hidden "true", :class "w-6 h-6", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} - [:path {:fill-rule "evenodd", :d "M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z", :clip-rule "evenodd"}]]] - [:div {:id "tooltip-settings", :role "tooltip", :class "inline-block absolute invisible z-10 py-2 px-3 text-sm font-medium text-white bg-gray-900 rounded-lg shadow-sm opacity-0 transition-opacity duration-300 tooltip"} "Settings page"] - [:button {:type "button", :data-dropdown-toggle "language-dropdown", :class "inline-flex justify-center p-2 text-gray-500 rounded cursor-pointer dark:hover:text-white dark:text-gray-400 hover:text-gray-900 hover:bg-gray-100 dark:hover:bg-gray-600"} - [:svg {:aria-hidden "true", :class "h-5 w-5 rounded-full mt-0.5", :xmlns "http://www.w3.org/2000/svg", :xmlns:xlink "http://www.w3.org/1999/xlink", :viewbox "0 0 3900 3900"} - [:path {:fill "#b22234", :d "M0 0h7410v3900H0z"}] - [:path {:d "M0 450h7410m0 600H0m0 600h7410m0 600H0m0 600h7410m0 600H0", :stroke "#fff", :stroke-width "300"}] - [:path {:fill "#3c3b6e", :d "M0 0h2964v2100H0z"}] - [:g {:fill "#fff"} - [:g {:id "d"} - [:g {:id "c"} - [:g {:id "e"} - [:g {:id "b"} - [:path {:id "a", :d "M247 90l70.534 217.082-184.66-134.164h228.253L176.466 307.082z"}] - [:use {:xlink:href "#a", :y "420"}] - [:use {:xlink:href "#a", :y "840"}] - [:use {:xlink:href "#a", :y "1260"}]] - [:use {:xlink:href "#a", :y "1680"}]] - [:use {:xlink:href "#b", :x "247", :y "210"}]] - [:use {:xlink:href "#c", :x "494"}]] - [:use {:xlink:href "#d", :x "988"}] - [:use {:xlink:href "#c", :x "1976"}] - [:use {:xlink:href "#e", :x "2470"}]]]] - [:div {:class "hidden z-50 my-4 text-base list-none bg-white rounded divide-y divide-gray-100 shadow dark:bg-gray-700", :id "language-dropdown"} - [:ul {:class "py-1", :role "none"} - [:li - [:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600", :role "menuitem"} - [:div {:class "inline-flex items-center"} - [:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :id "flag-icon-css-us", :viewbox "0 0 512 512"} - [:g {:fill-rule "evenodd"} - [:g {:stroke-width "1pt"} - [:path {:fill "#bd3d44", :d "M0 0h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0z", :transform "scale(3.9385)"}] - [:path {:fill "#fff", :d "M0 10h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0zm0 20h247v10H0z", :transform "scale(3.9385)"}]] - [:path {:fill "#192f5d", :d "M0 0h98.8v70H0z", :transform "scale(3.9385)"}] - [:path {:fill "#fff", :d "M8.2 3l1 2.8H12L9.7 7.5l.9 2.7-2.4-1.7L6 10.2l.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7L74 8.5l-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 7.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 24.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 21.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 38.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 35.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 52.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 49.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm-74.1 7l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7H65zm16.4 0l1 2.8H86l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm-74 7l.8 2.8h3l-2.4 1.7.9 2.7-2.4-1.7L6 66.2l.9-2.7-2.4-1.7h3zm16.4 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8H45l-2.4 1.7 1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9zm16.4 0l1 2.8h2.8l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h3zm16.5 0l.9 2.8h2.9l-2.3 1.7.9 2.7-2.4-1.7-2.3 1.7.9-2.7-2.4-1.7h2.9zm16.5 0l.9 2.8h2.9L92 63.5l1 2.7-2.4-1.7-2.4 1.7 1-2.7-2.4-1.7h2.9z", :transform "scale(3.9385)"}]]] " \n English (US)"]]] - [:li - [:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-600", :role "menuitem"} - [:div {:class "inline-flex items-center"} - [:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :id "flag-icon-css-de", :viewbox "0 0 512 512"} - [:path {:fill "#ffce00", :d "M0 341.3h512V512H0z"}] - [:path {:d "M0 0h512v170.7H0z"}] - [:path {:fill "#d00", :d "M0 170.7h512v170.6H0z"}]]]]] - [:li - [:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:text-gray-300 dark:hover:text-white dark:hover:bg-gray-600", :role "menuitem"} - [:div {:class "inline-flex items-center"} - [:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :id "flag-icon-css-it", :viewbox "0 0 512 512"} - [:g {:fill-rule "evenodd", :stroke-width "1pt"} - [:path {:fill "#fff", :d "M0 0h512v512H0z"}] - [:path {:fill "#009246", :d "M0 0h170.7v512H0z"}] - [:path {:fill "#ce2b37", :d "M341.3 0H512v512H341.3z"}]]]]]] - [:li - [:a {:href "#", :class "block py-2 px-4 text-sm text-gray-700 hover:bg-gray-100 dark:hover:text-white dark:text-gray-300 dark:hover:bg-gray-600", :role "menuitem"} - [:div {:class "inline-flex items-center"} - [:svg {:aria-hidden "true", :class "h-3.5 w-3.5 rounded-full mr-2", :xmlns "http://www.w3.org/2000/svg", :xmlns:xlink "http://www.w3.org/1999/xlink", :id "flag-icon-css-cn", :viewbox "0 0 512 512"} - [:defs - [:path {:id "a", :fill "#ffde00", :d "M1-.3L-.7.8 0-1 .6.8-1-.3z"}]] - [:path {:fill "#de2910", :d "M0 0h512v512H0z"}] - [:use {:width "30", :height "20", :transform "matrix(76.8 0 0 76.8 128 128)", :xlink:href "#a"}] - [:use {:width "30", :height "20", :transform "rotate(-121 142.6 -47) scale(25.5827)", :xlink:href "#a"}] - [:use {:width "30", :height "20", :transform "rotate(-98.1 198 -82) scale(25.6)", :xlink:href "#a"}] - [:use {:width "30", :height "20", :transform "rotate(-74 272.4 -114) scale(25.6137)", :xlink:href "#a"}] - [:use {:width "30", :height "20", :transform "matrix(16 -19.968 19.968 16 256 230.4)", :xlink:href "#a"}]] "中文 (繁體)"]]]]]] - [:script {:lang "text/javascript"} - (hiccup/raw " - function initSidebarToggle() { - var $targetEl = document.getElementById('left-nav'); - - var $triggerEl = document.getElementById('left-nav-toggle'); - - var options = { - onCollapse: () => { - document.getElementById('main-content').classList.remove('lg:pl-64') - }, - onExpand: () => { - document.getElementById('main-content').classList.add('lg:pl-64') - }, - onToggle: () => { - } - }; - - var collapse = new Collapse($targetEl, $triggerEl, options); - } -")]]) + + page-specific]]) (defn main-aside-nav- [] [:ul {:class "space-y-1"} @@ -155,12 +64,11 @@ (menu-button- {:icon svg/pie :href "/"} "Dashboard")] - [:li - (menu-button- {:aria-controls "dropdown-invoices", - :data-collapse-toggle "dropdown-invoices" + [:li {:x-data (hx/json {:open false})} + (menu-button- {"@click" "open = !open" :icon svg/accounting-invoice-mail} "Invoices") - (sub-menu- {:id "dropdown-invoices"} + (sub-menu- {:x-show "open"} (menu-button- {:href (bidi/path-for client-routes/routes :invoices)} "All") @@ -173,12 +81,11 @@ (menu-button- {:href (bidi/path-for client-routes/routes :voided-invoices)} "Voided"))] - [:li - (menu-button- {:aria-controls "dropdown-sales", - :data-collapse-toggle "dropdown-sales" - :icon svg/receipt-register-1} + [:li {:x-data (hx/json {:open false})} + (menu-button- {:icon svg/receipt-register-1 + "@click" "open = !open"} "Sales") - (sub-menu- {:id "dropdown-sales"} + (sub-menu- {:x-show "open"} (menu-button- {:href (str (bidi/path-for ssr-routes/only-routes :pos-sales) "?date-range=week")} "Sales") @@ -197,12 +104,11 @@ "?date-range=week")} "Cash drawer shifts") #_(menu-button- {:href "Sales"} "Cash Shifts") #_(menu-button- {:href "Sales"} "Tenders"))] - [:li - (menu-button- {:aria-controls "dropdown-payments" - :data-collapse-toggle "dropdown-payments" + [:li {:x-data (hx/json {:open false})} + (menu-button- {"@click" "open = !open" :icon svg/payments} "Payments") - (sub-menu- {:id "dropdown-payments"} + (sub-menu- {:x-show "open"} (menu-button- {:href (bidi/path-for client-routes/routes :payments)} "All") (menu-button- {:href (bidi/path-for client-routes/routes @@ -212,13 +118,12 @@ (menu-button- {:href (bidi/path-for client-routes/routes :payments)} "Voided"))] - [:li - (menu-button- {:aria-controls "dropdown-transactions" - :data-collapse-toggle "dropdown-transactions" + [:li {:x-data (hx/json {:open false})} + (menu-button- {"@click" "open = !open" :icon svg/bank} "Transactions") - (sub-menu- {:id "dropdown-transactions"} + (sub-menu- {:x-show "open"} (menu-button- {:href (bidi/path-for client-routes/routes :transactions)} "All") (menu-button- {:href (bidi/path-for client-routes/routes @@ -229,12 +134,11 @@ :approved-transactions)} "Approved") (menu-button- {:href (bidi/path-for ssr-routes/only-routes :transaction-insights)} "Insights"))] - [:li - (menu-button- {:aria-controls "dropdown-ledger" - :data-collapse-toggle "dropdown-ledger" + [:li {:x-data (hx/json {:open false})} + (menu-button- {"@click" "open = !open" :icon svg/receipt} "Ledger") - (sub-menu- {:id "dropdown-ledger"} + (sub-menu- {:x-show "open"} (menu-button- {:href (bidi/path-for client-routes/routes :ledger)} "Register") (menu-button- {:href (bidi/path-for client-routes/routes @@ -310,8 +214,7 @@ [:li (menu-button- {:icon svg/cog - :href (bidi/path-for client-routes/routes - :admin-rules)} + :href (bidi/path-for ssr-routes/only-routes :admin-transaction-rules)} "Rules")] [:li diff --git a/src/clj/auto_ap/ssr/components/buttons.clj b/src/clj/auto_ap/ssr/components/buttons.clj index 40574e20..ee276f37 100644 --- a/src/clj/auto_ap/ssr/components/buttons.clj +++ b/src/clj/auto_ap/ssr/components/buttons.clj @@ -1,5 +1,6 @@ (ns auto-ap.ssr.components.buttons - (:require [auto-ap.ssr.svg :as svg])) + (:require [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.hiccup-helper :as hh])) (defn button-icon- [_ i] [:div.h-4.w-4 i]) @@ -98,13 +99,16 @@ (into [:div.htmx-indicator-hidden.inline-flex.gap-2.items-center.justify-center] children)]) (defn a-button- [params & children] - [:a (update params - :class #(cond-> % - true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center") - (= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") - (= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ") - (nil? (:color params)) - (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700"))) + [:a (-> params + (update :class #(cond-> % + true (str " focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center hover:scale-105 transition duration-100 justify-center") + (= :secondary (:color params)) (str " text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700") + (= :primary (:color params)) (str " text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 ") + (nil? (:color params)) + (str " bg-white dark:bg-gray-600 border-gray-300 dark:border-gray-700 text-gray-500 hover:text-gray-800 dark:text-gray-400 dark:hover:text-gray-100 font-medium border border-gray-300 dark:border-gray-700") + )) + (assoc :tabindex 0) + (assoc :href (:href params "#"))) [:div.htmx-indicator.flex.items-center (svg/spinner {:class "inline w-4 h-4 text-white"}) [:div.ml-3 "Loading..."]] @@ -130,7 +134,9 @@ (defn a-icon-button- [params & children] (into - [:a (update params :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100") + [:a (-> params (update :class str " inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center p-3 text-sm font-medium border border-gray-300 dark:border-gray-700 text-center text-gray-500 hover:text-gray-800 rounded-lg dark:text-gray-400 dark:hover:text-gray-100" + ) + (update :href #(or % ""))) [:div.h-4.w-4 children]])) (defn save-button- [params & children] @@ -167,3 +173,15 @@ :hx-on:click "this.querySelector(\"input\").value = event.target.value; this.querySelector(\"input\").dispatchEvent(new Event('change', {bubbles: true}));"} [:input {:type "hidden" :name name}]] children))) + + +(defn validated-save-button- [{:keys [errors class] :as params} & children] + (button- (-> {:color (or (:color params) :primary) + :type "submit" :class (cond-> (or class "") + true (hh/add-class "w-32") + (seq errors) (hh/add-class "animate-shake"))} + (merge params) + (dissoc :errors)) + (if (seq children) + children + "Save"))) diff --git a/src/clj/auto_ap/ssr/components/data_grid.clj b/src/clj/auto_ap/ssr/components/data_grid.clj index caf91017..f62c7f12 100644 --- a/src/clj/auto_ap/ssr/components/data_grid.clj +++ b/src/clj/auto_ap/ssr/components/data_grid.clj @@ -3,8 +3,10 @@ [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components.card :refer [content-card-]] [auto-ap.ssr.components.paginator :refer [paginator-]] + [auto-ap.ssr.components.buttons :refer [a-button-]] [bidi.bidi :as bidi] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [auto-ap.ssr.hx :as hx])) (defn header- [params & rest] (into [:th.px-4.py-3 {:scope "col" :class (:class params) @@ -103,3 +105,23 @@ [:div {:class "htmx-indicator absolute -translate-x-1/2 -translate-y-1/2 top-2/4 left-1/2 overflow-hidden w-full h-full"} [:div {:class "flex items-center justify-center w-full h-full border border-gray-200 rounded-lg bg-gray-50 dark:bg-gray-800 dark:border-gray-700 bg-opacity-50" } [:div {:class "px-3 py-1 text-xs font-medium leading-none text-center text-blue-800 bg-blue-200 rounded-full animate-pulse dark:bg-blue-900 dark:text-blue-200"} "loading..."]]])]) + +(defn new-row- [{:keys [index colspan tr-params] :as params} & content] + (row- + (merge {:class "new-row" + :x-data (hx/json {:newRowIndex index}) + } + tr-params) + (cell- {:colspan colspan + :class "bg-gray-100"} + [:div.flex.justify-center + (a-button- (merge + (dissoc params :index :colspan) + { + "@click" "$dispatch('newRow', {index: newRowIndex++})" + :color :secondary + :hx-trigger "newRow" + :hx-vals (hiccup/raw "js:{index: event.detail.index}") + :hx-target "closest .new-row" + :hx-swap "beforebegin"}) + content)]))) diff --git a/src/clj/auto_ap/ssr/components/dialog.clj b/src/clj/auto_ap/ssr/components/dialog.clj index e6e2e99e..572a98d4 100644 --- a/src/clj/auto_ap/ssr/components/dialog.clj +++ b/src/clj/auto_ap/ssr/components/dialog.clj @@ -1,16 +1,24 @@ (ns auto-ap.ssr.components.dialog - (:require [hiccup2.core :as hiccup])) + (:require + [auto-ap.ssr.hiccup-helper :as hh])) (defn modal- [params & children] - [:div {:class (str "relative w-full max-h-full " (or (:modal-class params) " max-w-2xl "))} - (into [:div#modal-content] - children)] - ) + [:div (-> params + (assoc "@click.outside" "open=false") + (update :class (fnil hh/add-class "") "w-full h-full")) + children]) (defn modal-card- [params header content footer] - [:div#modal-card params - [: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 "p-6 space-y-6"} + [:div (update params + :class (fn [c] (-> c + (or "") + (hh/add-class "w-full p-4 h-full modal-card") + ))) + [:div {:class "bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content w-full flex flex-col h-full"} + [:div {:class "flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0"} header] + [:div {:class "px-6 space-y-6 overflow-y-scroll w-full shrink"} + #_[:div.bg-green-300.w-full.h-64 + "hello"] content] - [:div footer]]]) + (when footer [:div {:class "p-4 shrink-0"} footer])]]) + diff --git a/src/clj/auto_ap/ssr/components/inputs.clj b/src/clj/auto_ap/ssr/components/inputs.clj index 02678df7..8bfb07db 100644 --- a/src/clj/auto_ap/ssr/components/inputs.clj +++ b/src/clj/auto_ap/ssr/components/inputs.clj @@ -2,7 +2,9 @@ (:require [hiccup2.core :as hiccup] [auto-ap.ssr.hiccup-helper :as hh] - [clojure.string :as str])) + [clojure.string :as str] + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.hx :as hx])) (def default-input-classes @@ -30,43 +32,87 @@ (:allow-blank? params) (conj [:option {:value "" :selected (not (:value params))} ""]))] children)) + (defn typeahead- [params] - [:select (-> params - (dissoc :url) - (dissoc :value) - (dissoc :value-fn) - (dissoc :content-fn)) - (for [value (if (:multiple params) - (:value params) - [(:value params)]) - :when ((:value-fn params first) value)] - [:option {:value ((:value-fn params first) value) :selected true} ((:content-fn params second) value)]) - - [:script {:lang "javascript"} - (hiccup/raw (format " -(function () { -var element = document.getElementById('%s'); -var c = new Choices(element, {removeItems: true, removeItemButton:true, searchFloor: 3, searchPlaceholderValue: '%s'}); -let baseUrl = '%s'; + [:div {:x-data (hx/json {:open false + :baseUrl (if (str/includes? (:url params) "?") + (str (:url params) "&q=") + (str (:url params) "?q=")) + :value {:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))} + :search "" + :active -1 + :elements (if ((:value-fn params identity) (:value params)) + [{:value ((:value-fn params identity) (:value params)) :label ((:content-fn params identity) (:value params))}] + []) + :popper nil}) + :x-modelable "value.value" + :x-model (:x-model params) + :x-init "popper = Popper.createPopper($refs.input, $refs.dropdown, {placement: 'bottom-start', strategy: 'fixed', modifiers: {name: 'offset', options: {offset: [0, 10]}}})" + } + [:a {:class (-> (hh/add-class (or (:class params) "") default-input-classes) + (hh/add-class "cursor-pointer")) + "@click.prevent" "open = !open; popper.update()" + "@keydown.enter.prevent.stop" "open = !open; popper.update()" + "@keydown.down.prevent.stop" "open = true; popper.update()" + "@keydown.backspace" "value = {value: '', label: '' }" + :tabindex 0 + :x-init (:x-init params) + :x-ref "input" + } + [:input (-> params + (dissoc :class) + (dissoc :value-fn) + (dissoc :content-fn) -element.addEventListener('search', function (e) { - let fullUrl = baseUrl + (baseUrl.includes(\"?\") ? \"&\" : \"?\") + \"q=\" + e.detail.value; - let data = fetch(fullUrl) - .then(res => res.json()) - .then(data => { - c.setChoices(data, 'value', 'label', true) - }); -}); -element.addEventListener('choice', function (e) { -c.clearChoices(); - }) -})(); + (dissoc :placeholder) + (dissoc :x-model) + (assoc + "x-ref" "hidden" + :type "hidden" + ":value" "value.value" + :x-init (hiccup/raw (str "$watch('value', v => $dispatch('change')); "))))] + [:div.flex.w-full.justify-items-stretch + [:span.flex-grow.text-left {"x-text" "value.label"}] + [:div {:class "w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center"} + svg/drop-down]]] -" - (:id params) - (:placeholder params) - (:url params) - ))]]) + [:ul.dropdown-contents {:class "bg-gray-100 dark:bg-gray-600 rounded-lg shadow-2xl w-max z-50 ring-1" + "x-ref" "dropdown" + "@keydown.escape" "open = false; value = {value: '', label: '' }" + "x-transition:enter" "ease-[cubic-bezier(.3,2.3,.6,1)] duration-200" + "x-transition:enter-start" "!opacity-0" + "x-transition:enter-end" "!opacity-1" + "x-transition:leave" "ease-out duration-200" + "x-transition:leave-start" "!opacity-1" + "x-transition:leave-end" "!opacity-0" + "x-show " "open" + "x-trap" "open" + "@click.outside" "open=false;"} + + [:input {:type "text" + :class (-> (:class params) + (or "") + (hh/add-class default-input-classes) + (hh/replace-wildcard ["rounded" "border"] "border-bottom bg-gray-100 rounded-t-lg w-full")) + "x-model" "search" + "placeholder" (:placeholder params) + "@keydown.down.prevent" "active ++; active = active >= elements.length - 1 ? elements.length - 1 : active" + "@keydown.up.prevent" "active --; active = active < 0 ? 0 : active" + "@keydown.enter.prevent" "open = false; value = elements.length > 0 ? $data.elements[active] : {'value': '', label: ''};" + "x-init" "$watch('search', s => { if($el.value.length > 2) {fetch(baseUrl + s).then(data=>data.json()).then(data => {elements = data; active=-1}) }})"}] + [:div.dropdown-options {:class "rounded-b-lg overflow-hidden"} + [:template {:x-for "(element, index) in elements"} + [:li [:a {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-300 [&.active]:dark:bg-primary-700 text-gray-800 dark:text-gray-100" + :href "#" + ":class" "active == index ? 'active' : ''" + "@mouseover" "active = index" + "@mouseout" "active = -1" + "@click.prevent" "value = element; open=false; " + "x-html" "element.label"}]]] + [:template {:x-if "elements.length == 0"} + [:li {:class "px-4 py-2 flex gap-2 items-center outline-0 focus:bg-neutral-100 hover:bg-neutral-100 whitespace-nowrap [&.active]:bg-primary-500 text-gray-800 dark:text-gray-100 text-xs "} + "No results found"]]] + ]]) (defn use-size [size] @@ -80,6 +126,7 @@ c.clearChoices(); [:input (-> params (dissoc :error?) + (assoc :type "text") (update :class (fnil hh/add-class "") default-input-classes) (update :class #(str % (use-size size))))]) @@ -124,7 +171,8 @@ c.clearChoices(); [:p.mt-2.text-xs.text-red-600.dark:text-red-500.h-4 (str/join ", " errors)])) (defn field- [params & rest] - [:div {:id (:id params) :class (hh/add-class "group" (:class params))} + [:div (-> params + (update :class #(hh/add-class (or % "") "group" ))) (when (:label params) [:label {:class "block mb-2 text-sm font-medium text-gray-900 dark:text-white"} (:label params)]) rest @@ -137,6 +185,11 @@ c.clearChoices(); (when (sequential? errors) (str/join ", " (filter string? errors)))]) +(defn form-errors- [{:keys [errors]}] + [:div#form-errors (when errors + [:span.error-content + (errors- {:errors errors})])]) + (defn validated-field- [params & rest] (field- (cond-> params true (dissoc :errors) diff --git a/src/clj/auto_ap/ssr/components/navbar.clj b/src/clj/auto_ap/ssr/components/navbar.clj index e08d9c73..3759e741 100644 --- a/src/clj/auto_ap/ssr/components/navbar.clj +++ b/src/clj/auto_ap/ssr/components/navbar.clj @@ -13,7 +13,8 @@ [:div {:class "px-3 py-3 lg:px-5 lg:pl-3"} [:div {:class "flex items-center justify-between"} [:div {:class "flex items-center justify-start"} - [:button {:aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600"} + [:button {:aria-controls "left-nav", :id "left-nav-toggle" :type "button", :class "inline-flex items-center p-2 mt-2 ml-2 mr-2 text-sm text-gray-500 rounded-lg hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-gray-200 dark:text-gray-400 dark:hover:bg-gray-700 dark:focus:ring-gray-600" + "@click" "leftNavShow = !leftNavShow"} [:span {:class "sr-only"} "Open sidebar"] [:svg {:class "w-6 h-6", :aria-hidden "true", :fill "currentColor", :viewbox "0 0 20 20", :xmlns "http://www.w3.org/2000/svg"} [:path {:clip-rule "evenodd", :fill-rule "evenodd", :d "M2 4.75A.75.75 0 012.75 4h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 4.75zm0 10.5a.75.75 0 01.75-.75h7.5a.75.75 0 010 1.5h-7.5a.75.75 0 01-.75-.75zM2 10a.75.75 0 01.75-.75h14.5a.75.75 0 010 1.5H2.75A.75.75 0 012 10z"}]]] @@ -24,10 +25,7 @@ (when (is-admin? identity) [:button.mt-1.lg:w-96.relative.hidden.lg:block {:class "bg-gray-50 hover:bg-gray-200 dark:hover:bg-gray-700 border border-gray-300 text-gray-900 sm:text-sm rounded-lg focus:ring-primary-500 focus:border-primary-500 w-full pl-10 py-4 pr-2.5 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-primary-500 dark:focus:border-primary-500 gap-4 " - :hx-get (bidi/path-for ssr-routes/only-routes - :search) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} + :hx-get (bidi/path-for ssr-routes/only-routes :search)} [:div {:class "absolute inset-y-0 left-0 flex items-center pl-3 pointer-events-none text-gray-500"} [:div.w-4.h-4 svg/search] [:span.ml-2 "Search"]]]) diff --git a/src/clj/auto_ap/ssr/components/page.clj b/src/clj/auto_ap/ssr/components/page.clj index 7afa7c3a..0fbcdaef 100644 --- a/src/clj/auto_ap/ssr/components/page.clj +++ b/src/clj/auto_ap/ssr/components/page.clj @@ -3,20 +3,26 @@ [auto-ap.ssr.components.aside :refer [left-aside-]] [auto-ap.ssr.components.navbar :refer [navbar-]] [hiccup2.core :as hiccup] - [auto-ap.ssr.svg :as svg])) + [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.hx :as hx])) (defn page- [{:keys [nav page-specific client client-selection identity app-params] :or {app-params {}}} & children] [:div#app {"_" (hiccup/raw " on notification put event.detail.value into #notification-details then add .htmx-added to #notification-holder then remove .hidden from #notification-holder then wait 30ms then remove .htmx-added from #notification-holder on htmx:responseError put event.detail.xhr.response into #error-details then add .htmx-added to #error-holder then remove .hidden from #error-holder then wait 30ms then remove .htmx-added from #error-holder" - )} + ) + :x-data (hx/json {:leftNavShow true})} (navbar- {:client-selection client-selection - :client client - :identity identity}) - [:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params :hx-disinherit "*") - (left-aside- {:nav nav + :client client + :identity identity}) + [:div#app-contents.flex.pt-16.overflow-hidden (assoc app-params + :hx-disinherit "*" + :x-init "leftNavShow = true") + (left-aside- {:nav nav :page-specific page-specific}) - [:div#main-content {:class "relative w-full h-full lg:pl-64 overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content " + [:div#main-content {:class "relative w-full h-full overflow-y-auto px-4 bg-gray-100 dark:bg-gray-900 min-h-content lg:pl-64" + ":class" "leftNavShow ? 'lg:pl-64' : ''" + :x-effect "leftNavShow ? $el.classList.add('lg:pl-64') : $el.classList.remove('lg:pl-64')" } [:div#notification-holder.hidden [:div.fixed.top-0.right-0.left-0.z-30.mx-auto.max-w-screen-lg.w-screen-lg.my-0.pt-8.rounded-lg @@ -49,4 +55,5 @@ (into [:div.p-4] children)]] + ]) diff --git a/src/clj/auto_ap/ssr/components/radio.clj b/src/clj/auto_ap/ssr/components/radio.clj index a793a901..e02dc4d5 100644 --- a/src/clj/auto_ap/ssr/components/radio.clj +++ b/src/clj/auto_ap/ssr/components/radio.clj @@ -1,11 +1,18 @@ -(ns auto-ap.ssr.components.radio) +(ns auto-ap.ssr.components.radio + (:require [auto-ap.ssr.hiccup-helper :as hh] + [auto-ap.ssr.hx :as hx])) -(defn radio- [{:keys [options name title size] :or {size :medium} selected-value :value}] +(defn radio- [{:keys [options name title size orientation] :or {size :medium} selected-value :value}] [: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 (cond-> "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" + (= orientation :horizontal) (-> (hh/add-class "flex gap-2 flex-wrap") + (hh/remove-wildcard ["w-" "rounded-lg" "border" "bg-"])))} (for [{:keys [value content]} options] - [:li {:class "w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600"} - [:div {:class "flex items-center pl-3"} + [:li {:class (cond-> "w-full border-b border-gray-200 rounded-t-lg dark:border-gray-600" + (= orientation :horizontal) (-> (hh/remove-wildcard ["w-full" "rounded-"]) + (hh/add-class "w-auto shrink-0 block rounded-lg border border-gray-200 dark:border-gray-600 px-3")))} + [:div {:class (cond-> "flex items-center" + (not= orientation :horizontal) (hh/add-class "pl-3"))} [:input (cond-> {:id (str "list-" name "-" value) :type "radio", :value value @@ -25,4 +32,7 @@ (str " " "text-xs py-2") (= size :medium) - (str " " "text-sm py-3"))} content]]])]) + (str " " "text-sm py-3") + + (= orientation :horizontal) + (hh/remove-class "w-full"))} content]]])]) diff --git a/src/clj/auto_ap/ssr/core.clj b/src/clj/auto_ap/ssr/core.clj index 0935bbd0..d4eeaa9c 100644 --- a/src/clj/auto_ap/ssr/core.clj +++ b/src/clj/auto_ap/ssr/core.clj @@ -46,10 +46,6 @@ :bank-account-typeahead (wrap-client-redirect-unauthenticated (wrap-secure company/bank-account-typeahead)) :company (wrap-client-redirect-unauthenticated (wrap-secure company/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-dialog (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-dialog)) - :company-1099-vendor-save (wrap-client-redirect-unauthenticated (wrap-secure company-1099/vendor-save)) :company-plaid (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/page)) :company-plaid-table (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/table)) :company-plaid-link (wrap-client-redirect-unauthenticated (wrap-secure company-plaid/link)) @@ -76,6 +72,7 @@ :transaction-insight-explain (wrap-client-redirect-unauthenticated (wrap-admin insights/explain)) :admin-ezcater-xls (wrap-client-redirect-unauthenticated (wrap-admin ezcater-xls/page)) :search (wrap-client-redirect-unauthenticated (wrap-secure search/dialog-contents))} + (into company-1099/key->handler) (into pos-sales/key->handler) (into pos-expected-deposits/key->handler) (into pos-tenders/key->handler) diff --git a/src/clj/auto_ap/ssr/form_cursor.clj b/src/clj/auto_ap/ssr/form_cursor.clj index 1170afc0..b1617452 100644 --- a/src/clj/auto_ap/ssr/form_cursor.clj +++ b/src/clj/auto_ap/ssr/form_cursor.clj @@ -2,17 +2,26 @@ (:require [auto-ap.ssr.utils :refer [path->name2]] [auto-ap.cursor :as cursor])) +(def ^:dynamic *prefix* []) (def ^:dynamic *form-data*) (def ^:dynamic *form-errors*) (def ^:dynamic *prev-cursor* nil) (def ^:dynamic *current* nil) + + (defmacro start-form [form-data errors & rest] `(binding [*form-data* ~form-data *form-errors* (or ~errors {})] - (binding [*current* (cursor/cursor *form-data*)] + (binding [*current* (if (cursor/cursor? *form-data*) + *form-data* + (cursor/cursor *form-data*))] ~@rest))) +(defmacro start-form-with-prefix [prefix form-data errors & rest] + `(binding [*prefix* ~prefix] + (start-form ~form-data ~errors ~@rest))) + (defmacro with-cursor [cursor & rest] `(binding [*current* ~cursor] ~@rest)) @@ -21,13 +30,19 @@ `(with-cursor (get *current* ~field ) ~@rest)) +(defmacro with-field-default [field default & rest] + `(with-cursor (get *current* ~field ~default) + ~@rest)) + + (defn field-name ([] (field-name *current*)) ([cursor] - (apply path->name2 (cursor/path cursor)))) + (apply path->name2 (into (or *prefix* []) (cursor/path cursor))))) -(defn field-value [] - @*current*) +(defn field-value + ([] (field-value *current*)) + ([cursor] @cursor)) (defn field-errors ([] @@ -44,3 +59,13 @@ (every? string? errors))))) +(defn cursor-map + ([f] (cursor-map *current* f)) + ([cursor f] + (when (field-value) + (doall + (for [n cursor] + (with-cursor n + (f n))))))) + + diff --git a/src/clj/auto_ap/ssr/hx.clj b/src/clj/auto_ap/ssr/hx.clj index d5c59d85..370e27b3 100644 --- a/src/clj/auto_ap/ssr/hx.clj +++ b/src/clj/auto_ap/ssr/hx.clj @@ -18,3 +18,35 @@ (defn triggers [& triggers] (str/join ", " triggers)) + +(defn alpine-appear [m] + (assoc m + "x-transition:enter" "transition duration-500" + "x-transition:enter-start" "opacity-0" + "x-transition:enter-end" "opacity-100")) + +(defn alpine-disappear [m] + (assoc m + "x-transition:leave" "transition duration-500" + "x-transition:leave-start" "opacity-100" + "x-transition:leave-end" "opacity-0")) + +(defn alpine-mount-then-appear [{:keys [data-key] :as params}] + (merge (-> {:x-data (json {data-key false}) + :x-init (format "$nextTick(() => %s=true)" (name data-key)) + :x-show (name data-key)} + alpine-appear + alpine-disappear) + (dissoc params :data-key))) + +(defn bind-alpine-vals [m field->alpine-field] + (assoc m "x-bind:hx-vals" + + (format "JSON.stringify({%s})" + (str/join ", " + (map + (fn [[field alpine-field]] + (format "\"%s\": $data.%s" field alpine-field)) + + field->alpine-field))))) + diff --git a/src/clj/auto_ap/ssr/nested_form_params.clj b/src/clj/auto_ap/ssr/nested_form_params.clj index 1fa4648e..83286aa5 100644 --- a/src/clj/auto_ap/ssr/nested_form_params.clj +++ b/src/clj/auto_ap/ssr/nested_form_params.clj @@ -64,7 +64,9 @@ (nested-params-request request {})) ([request options] (let [parse (:key-parser options parse-nested-keys)] - (update-in request [:form-params] nest-params parse)))) + (-> request + (assoc :raw-form-params (:form-params request)) + (update-in [:form-params] nest-params parse))))) (defn wrap-nested-form-params "Middleware to converts a flat map of parameters into a nested map. diff --git a/src/clj/auto_ap/ssr/pos/common.clj b/src/clj/auto_ap/ssr/pos/common.clj index ef37e0af..97022a42 100644 --- a/src/clj/auto_ap/ssr/pos/common.clj +++ b/src/clj/auto_ap/ssr/pos/common.clj @@ -3,9 +3,6 @@ [auto-ap.time :as atime] [auto-ap.ssr.svg :as svg])) -;; TODO make date-input take clj date -;; TODO make total fields take decimals - (defn date-range-field* [request] [:div#date-range {} (com/field {:label "Date Range"} diff --git a/src/clj/auto_ap/ssr/pos/refunds.clj b/src/clj/auto_ap/ssr/pos/refunds.clj index 49f741b1..2651307e 100644 --- a/src/clj/auto_ap/ssr/pos/refunds.clj +++ b/src/clj/auto_ap/ssr/pos/refunds.clj @@ -8,25 +8,15 @@ merge-query pull-many query2]] - [auto-ap.graphql.utils :refer [extract-client-ids]] - [auto-ap.routes.utils - :refer [wrap-client-redirect-unauthenticated wrap-secure]] - [auto-ap.ssr.pos.common :refer [date-range-field* processor-field* total-field*]] + [auto-ap.query-params :as query-params] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.grid-page-helper :as helper] - [auto-ap.ssr.svg :as svg] + [auto-ap.ssr.pos.common :refer [date-range-field* total-field*]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [clj-time.coerce :as c] - [datomic.api :as dc] - [clojure.set :as set] - [auto-ap.query-params :as query-params] - [malli.core :as m])) - -;; TODO refunds -;; always should be fast -;; make params parsing composable + [datomic.api :as dc])) (defn filters [request] [:form {"hx-trigger" "change delay:500ms, keyup changed from:.hot-filter delay:1000ms" diff --git a/src/clj/auto_ap/ssr/search.clj b/src/clj/auto_ap/ssr/search.clj index 45cbee37..045e68cf 100644 --- a/src/clj/auto_ap/ssr/search.clj +++ b/src/clj/auto_ap/ssr/search.clj @@ -2,7 +2,7 @@ (:require [auto-ap.graphql.utils :refer [can-see-client?]] [auto-ap.solr :as solr] - [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.ssr.utils :refer [html-response modal-response]] [auto-ap.time :as atime] [clojure.string :as str] [com.brunobonacci.mulog :as mu] @@ -130,11 +130,11 @@ :form (:form-params request)) (if-let [q (get (:form-params request) "q")] (html-response (search-results* q (:identity request))) - (html-response + (modal-response (com/modal {} - (com/modal-card {} + (com/modal-card {:class "w-full h-full"} [:div.p-2 "Search"] - [:div#search.overflow-auto.space-y-6.p-2.h-96 + [:div#search.overflow-auto.space-y-6.p-2.w-full (com/text-input {:id "search-input" :type "search" diff --git a/src/clj/auto_ap/ssr/svg.clj b/src/clj/auto_ap/ssr/svg.clj index 385bbc1b..077f0988 100644 --- a/src/clj/auto_ap/ssr/svg.clj +++ b/src/clj/auto_ap/ssr/svg.clj @@ -200,7 +200,7 @@ :stroke-linejoin "round"}]]) (def drop-down - [:svg {:class "w-4 h-4 ml-2", :aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} + [:svg {:aria-hidden "true", :fill "none", :stroke "currentColor", :viewbox "0 0 24 24", :xmlns "http://www.w3.org/2000/svg"} [:path {:stroke-linecap "round", :stroke-linejoin "round", :stroke-width "2", :d "M19 9l-7 7-7-7"}]]) (def download diff --git a/src/clj/auto_ap/ssr/transaction/insights.clj b/src/clj/auto_ap/ssr/transaction/insights.clj index 78eb6ef9..3515306f 100644 --- a/src/clj/auto_ap/ssr/transaction/insights.clj +++ b/src/clj/auto_ap/ssr/transaction/insights.clj @@ -1,21 +1,21 @@ (ns auto-ap.ssr.transaction.insights (:require [auto-ap.client-routes :as client-routes] - [auto-ap.datomic :refer [conn pull-attr visible-clients]] + [auto-ap.datomic :refer [conn visible-clients]] [auto-ap.rule-matching :refer [spread-cents]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] [auto-ap.ssr.svg :as svg] [auto-ap.ssr.ui :refer [base-page]] - [auto-ap.ssr.utils :refer [html-response]] + [auto-ap.ssr.utils :refer [html-response modal-response]] [auto-ap.time :as atime] [bidi.bidi :as bidi] [cemerick.url :as url] [clj-http.client :as http] [clj-time.coerce :as coerce] [datomic.api :as dc] - [iol-ion.tx :refer [random-tempid]] - [hiccup2.core :as hiccup])) + [hiccup2.core :as hiccup] + [iol-ion.tx :refer [random-tempid]])) (def pull-expr [:transaction/description-original :db/id @@ -55,7 +55,7 @@ [(>= ?d ?starting)]] :args [(dc/db conn) - (iol-ion.query/recent-date 120) + (iol-ion.query/recent-date 300) (map :db/id clients) pull-expr]}) @@ -234,43 +234,49 @@ :hide-actions? true "_" (hiccup/raw "init transition opacity to 0 over 500ms then remove me"))))) (defn explain [{:keys [identity session] {:keys [transaction-id]} :route-params}] - (let [r (dc/pull (dc/db conn) + (let [r (dc/pull (dc/db conn) pull-expr (Long/parseLong transaction-id)) - similar (pinecone-similarity-list transaction-id)] - (html-response - (com/modal {} - (com/modal-card {:style {:width "900px"}} - [:div.flex [:div.p-2 "Similar Transactions"]] - [:table.w-full - [:thead - [:tr - [:td "Date"] - [:td "Description"] - [:td "Amount"] - [:td "Vendor"] - [:td "Account"] - [:td "Score"]]] - [:tbody - [:tr - [:th.text-left (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))] - [:th.text-left (-> r :transaction/description-original)] - [:th.text-left (if (> (-> r :transaction/amount) 0.0) - [:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))] - [:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])] - [:th] - [:th] - [:th.text-left]] - (take 10 - (for [{:keys [amount date description vendor-name numeric-code score]} similar] - [:tr - [:td (subs date 0 10)] - [:td description] - [:td (some->> amount double (format "$%.2f"))] - [:td vendor-name] - [:td numeric-code] - [:td (format "%.1f%%" (* 100 (double score)))]]))]] - [:div]))))) + similar (pinecone-similarity-list transaction-id)] + (modal-response + (com/modal {} + (com/modal-card {:style {:width "900px"}} + [:div.flex [:div.p-2 "Similar Transactions"]] + (com/data-grid {:headers [(com/data-grid-header {:name "Date" + :key "date"}) + (com/data-grid-header {:name "Description" + :key "description"}) + (com/data-grid-header {:name "Amount" + :key "amount"}) + (com/data-grid-header {:name "Vendor" + :key "vendor"}) + (com/data-grid-header {:name "Account" + :key "account"}) + (com/data-grid-header {:name "Score" + :key "score"})]} + + (com/data-grid-row {:class "bg-primary-200"} + (com/data-grid-cell {:class "text-left font-bold"} (some-> r :transaction/date coerce/to-date-time (atime/unparse-local atime/normal-date))) + (com/data-grid-cell {:class "text-left font-bold"} (-> r :transaction/description-original) ) + (com/data-grid-cell {:class "font-bold"} (if (> (-> r :transaction/amount) 0.0) + [:div.tag.is-success.is-light (str "$" (Math/round (:transaction/amount r)))] + [:div.tag.is-danger.is-light (str "$" (Math/round (:transaction/amount r)))])) + (com/data-grid-cell {}) + (com/data-grid-cell {}) + (com/data-grid-cell {})) + + (com/data-grid-row {} + (take 10 + (for [{:keys [amount date description vendor-name numeric-code score]} similar] + (com/data-grid-row + {} + (com/data-grid-cell {:class "text-left"} (subs date 0 10)) + (com/data-grid-cell {:class "text-left"} description ) + (com/data-grid-cell {} (some->> amount double (format "$%.2f"))) + (com/data-grid-cell {} vendor-name) + (com/data-grid-cell {} numeric-code) + (com/data-grid-cell {} (format "%.1f%%" (* 100 (double score))))))))) + [:div]))))) (defn transaction-rows* [{:keys [clients identity after]}] (let [recommendations (transaction-recommendations identity clients :after after)] diff --git a/src/clj/auto_ap/ssr/ui.clj b/src/clj/auto_ap/ssr/ui.clj index 41ba3fe2..f0fe6ed0 100644 --- a/src/clj/auto_ap/ssr/ui.clj +++ b/src/clj/auto_ap/ssr/ui.clj @@ -1,5 +1,7 @@ (ns auto-ap.ssr.ui (:require + [auto-ap.ssr.hx :as hx] + [config.core :refer [env]] [hiccup2.core :as hiccup])) (defn html-page [hiccup] @@ -26,8 +28,14 @@ [:script {:src "https://unpkg.com/hyperscript.org@0.9.7/dist/_hyperscript.min.js"}] [:script {:src "https://unpkg.com/@popperjs/core@2.11.8/dist/umd/popper.min.js"}] [:script {:src "https://cdn.plaid.com/link/v2/stable/link-initialize.js"}] - [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js" - :crossorigin= "anonymous"}] + (if (= "dev" (:dd-env env)) + [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.js" + :crossorigin= "anonymous"}] + [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/htmx.min.js" + :crossorigin= "anonymous"}]) + + [:script {:src "https://unpkg.com/htmx.org@1.9.6/dist/ext/class-tools.js" :crossorigin= "anonymous"}] + [:script {:src "https://unpkg.com/htmx.org/dist/ext/debug.js"}] [:script {:src "/js/htmx-disable.js"}] [:script {:type "text/javascript", :src "https://cdn.yodlee.com/fastlink/v4/initialize.js", :async "async"}]] @@ -35,14 +43,14 @@ [:script {:type "text/javascript" :src "https://cdn.jsdelivr.net/npm/vanillajs-datepicker@1.1.4/dist/js/datepicker-full.min.js"}] - [:link {:rel "stylesheet" :href "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/styles/choices.min.css"}] [:script {:src "https://cdn.jsdelivr.net/npm/choices.js@9.0.1/public/assets/scripts/choices.min.js"}] [:script {:src "https://unpkg.com/htmx.org/dist/ext/response-targets.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"}] - #_[:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}] + [:script {:defer true :src "https://cdn.jsdelivr.net/npm/@alpinejs/focus@3.x.x/dist/cdn.min.js"}] + [:script {:defer true :src "https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"}] [:style " input::-webkit-outer-spin-button, @@ -53,38 +61,43 @@ input::-webkit-inner-spin-button { } input[type=number] { -moz-appearance:textfield; /* Firefox */ -} +} "] - " - ] - [:body {:hx-ext "disable-submit"} + [:body {:hx-ext "disable-submit, class-tools"} contents [: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 call curModal.show() - on \"modalClosing\" from 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); -") + [:div#modal-holder + {:tabindex "-1", :class "fixed top-0 left-0 z-[99] flex items-center justify-center w-screen h-screen" + "x-show" "open" + ":aria-hidden" "!open" + "x-data" (hx/json {"open" false}) + "@modalopen.document" "open=true" + "@modalclose.document" "open=false"} - ]]]])) + [:div {:class "bg-gray-900 bg-opacity-50 dark:bg-opacity-80 fixed inset-0 z-40 md:p-12" + "x-show" "open" + ":aria-hidden" "!open" + "x-transition:enter" "duration-300" + "x-transition:enter-start" "!bg-opacity-0" + "x-transition:enter-end" "!bg-opacity-50" + "x-transition:leave" "duration-300" + "x-transition:leave-start" "!bg-opacity-50" + "x-transition:leave-end" "!bg-opacity-0"} + + [:div { + :class "flex h-full w-full justify-stretch md:justify-center items-stretch md:items-center " + "x-trap.inert.noscroll" "open" + "x-trap.inert" "open" + "x-show" "open" + "x-transition:enter" "ease-out duration-300" + "x-transition:enter-start" "!bg-opacity-0 !translate-y-32" + "x-transition:enter-end" "!bg-opacity-100 !translate-y-0" + "x-transition:leave" "duration-300" + "x-transition:leave-start" "!opacity-100 !translate-y-0" + "x-transition:leave-end" "!opacity-0 !translate-y-32"} + [:div.flex.items-center.justify-center.max-w-6xl {:class "min-w-[700px] max-h-full "} + [:div#modal-content.flex.flex-col.self-stretch {:class "min-w-[700px] md:p-12"} ;;.overflow-scroll + ] + ]]]]]])) diff --git a/src/clj/auto_ap/ssr/users.clj b/src/clj/auto_ap/ssr/users.clj index d1ba7b58..7d481140 100644 --- a/src/clj/auto_ap/ssr/users.clj +++ b/src/clj/auto_ap/ssr/users.clj @@ -6,6 +6,7 @@ apply-sort-3 conn merge-query + pull-attr pull-many query2]] [auto-ap.query-params :as query-params] @@ -14,14 +15,22 @@ :refer [wrap-admin wrap-client-redirect-unauthenticated]] [auto-ap.ssr-routes :as ssr-routes] [auto-ap.ssr.components :as com] + [auto-ap.ssr.form-cursor :as fc] [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 - forced-vector html-response + main-transformer + many-entity + modal-response ref->enum-schema + ref->select-options + wrap-entity + wrap-form-4xx-2 wrap-schema-decode]] [auto-ap.time :as atime] [bidi.bidi :as bidi] @@ -72,12 +81,12 @@ :content "None"}]})) (com/field {:label "Client"} (com/typeahead {:name "client" - :placeholder "Search..." - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :id (str "client-search") - :value [(:db/id (:client (:parsed-query-params request))) - (:client/name (:client (:parsed-query-params request)))]}))]]) + :placeholder "Search..." + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :id (str "client-search") + :value [(:db/id (:client (:parsed-query-params request))) + (:client/name (:client (:parsed-query-params request)))]}))]]) (def default-read '[:db/id :user/name @@ -205,9 +214,7 @@ :hx-vals (format "{\"db/id\": \"%s\"}" (:db/id entity))} "Impersonate") (com/icon-button {:hx-get (str (bidi/path-for ssr-routes/only-routes :user-edit-dialog - :db/id (:db/id entity))) - :hx-target "#modal-holder" - :hx-swap "outerHTML"} + :db/id (:db/id entity)))} svg/pencil)]) :breadcrumbs [[:a {:href (bidi/path-for ssr-routes/only-routes :admin)} @@ -250,94 +257,137 @@ (def table* (partial helper/table* grid-page)) (defn impersonate [request] - (let [user (some-> request :params :db/id (#(dc/pull (dc/db conn) default-read %))) ] + (if (:entity request) {:status 200 - :headers {"hx-redirect" (str "/?jwt=" (jwt/sign (auth/user->jwt user "FAKE_TOKEN") - (:jwt-secret env) - {:alg :hs512})) - } - :session {:identity (dissoc (auth/user->jwt user "FAKE_TOKEN") - :exp)}})) + :headers {"hx-redirect" (str "/?jwt=" (jwt/sign (auth/user->jwt (:entity request) "FAKE_TOKEN") + (:jwt-secret env) + {:alg :hs512}))} + :session {:identity (dissoc (auth/user->jwt (:entity request) "FAKE_TOKEN") + :exp)}} + {:status 404})) + +(defn client-row* [client] + (com/data-grid-row (-> {:x-ref "p" + :data-key "show" + :x-data (hx/json {:show (boolean (not (fc/field-value (:new? client))))})} + hx/alpine-mount-then-appear) + (com/data-grid-cell {} + (com/validated-field {:errors (fc/field-errors (:db/id fc/*current*))} + (com/typeahead {:name (fc/field-name (:db/id fc/*current*)) + :class "w-full" + :url (bidi/path-for ssr-routes/only-routes + :company-search) + :value (fc/field-value) + :value-fn :db/id + + + :content-fn #(pull-attr (dc/db conn) :client/name (:db/id %)) + :size :small}))) + (com/data-grid-cell {:class "align-top"} + (com/a-icon-button {"@click.prevent.stop" "show=false; setTimeout(() => $refs.p.remove(), 500)"} svg/x)))) + + +(defn dialog* [{:keys [form-params form-errors entity]}] + (fc/start-form + form-params form-errors + (com/modal + {:hx-target "this"} + [:form {:hx-ext "response-targets" + :hx-put (str (bidi/path-for ssr-routes/only-routes + :user-edit-save + :request-method :put)) + :hx-swap "outerHTML swap:300ms" + :hx-target-400 "#form-errors .error-content" + :class "w-full h-full"} + [:fieldset {:class "hx-disable h-full"} + (com/modal-card + {} + [:div.flex [:div.p-2 "User"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:user/name entity)]] + [:div.space-y-6 + (fc/with-field :db/id + (com/hidden {:name (fc/field-name) + :value (fc/field-value)})) + (fc/with-field :user/role + (com/validated-field {:label "Role" + :errors (fc/field-errors)} + (com/select {:name (fc/field-name) + :class "w-36" + :autofocus true + :value (some->> (fc/field-value) name) + :options (ref->select-options "user-role")}))) + (fc/with-field :user/clients + (com/validated-field {:label "Clients"} + (com/data-grid {:headers [(com/data-grid-header {} "Client") + (com/data-grid-header {} )] + :id "client-table"} + (fc/cursor-map #(client-row* %)) + (com/data-grid-new-row {:colspan 2 + :index (count (fc/field-value)) + :hx-get (bidi/path-for ssr-routes/only-routes + :user-client-new)} + "Assign new client"))))] + [:div + (com/form-errors {:errors (:errors fc/*form-errors*)}) + (com/validated-save-button {:errors (seq form-errors)} + "Save user")])]]))) (defn user-edit-save [{:keys [form-params identity] :as request}] - (let [_ @(dc/transact conn [[:upsert-entity form-params]]) + (let [_ @(dc/transact conn [[:upsert-entity form-params]]) user (some-> form-params :db/id (#(dc/pull (dc/db conn) default-read %)))] (html-response (row* identity user {:flash? true}) - :headers {"hx-trigger" "closeModal" + :headers {"hx-trigger" "modalclose" "hx-retarget" (format "#user-table tr[data-id=\"%d\"]" (:db/id user))}))) -(defn user-edit-dialog [request] - (let [user (some-> request - :route-params - :db/id - (#(dc/pull (dc/db conn) default-read %)))] - (html-response - (com/modal - {} - [:form {:hx-ext "response-targets" - :hx-put (str (bidi/path-for ssr-routes/only-routes - :user-edit-save - :request-method :put)) - :hx-swap "outerHTML swap:300ms" - :hx-target-400 "#form-errors .error-content"} - [:fieldset {:class "hx-disable"} - (com/modal-card - {} - [:div.flex [:div.p-2 "User"] [:p.ml-2.rounded.bg-gray-200.p-2.dark:bg-gray-600 (:user/name user)]] - [:div.space-y-6 - (com/hidden {:name "db/id" - :value (:db/id user)}) - (com/field {:label "Role"} - (com/select {:name "user/role" - :class "w-36" - :autofocus true - :id "role" - :value (name (:user/role user)) - :options [["none" "None"] - ["power-user" "Power user"] - ["manager" "Manager"] - ["admin" "Admin"] - ["user" "User"]]})) - (com/field {:label "Clients"} - (com/typeahead {:name "user/clients" - :class "w-full" - :multiple "multiple" - :url (bidi/path-for ssr-routes/only-routes - :company-search) - :id "clients" - :value (map - (fn [client] - [(:db/id client) (:client/name client)]) - (:user/clients user)) - :size :small})) - [:div#form-errors [:span.error-content]] - (com/button {:color :primary :type "submit"} - "Save")] - [:div])]])))) + +(def form-schema + (mc/schema + [:map + [:db/id entity-id] + [:user/clients {:optional true} + [:maybe + (many-entity {} [:db/id entity-id])]] + [:user/role (ref->enum-schema "user-role")]])) + +(defn user-dialog [{:keys [form-params entity form-errors]}] + (modal-response + (dialog* {:form-params (or (when (seq form-params) + form-params) + (when entity + (mc/decode form-schema entity main-transformer)) + {}) + :entity entity + :form-errors form-errors}))) + +(defn new-client [{ {:keys [index]} :query-params}] + (html-response + (fc/start-form-with-prefix [:user/clients (or index 0)] {:db/id nil + :new? true} [] + (client-row* fc/*current*)))) (def key->handler (apply-middleware-to-all-handlers {:users (helper/page-route grid-page) :user-table (helper/table-route grid-page) :user-edit-save (-> user-edit-save - (wrap-schema-decode - :form-schema (mc/schema - [:map - [:db/id entity-id] - [:user/clients {:optional true} - [:maybe - (forced-vector entity-id)]] - [:user/role (ref->enum-schema "user-role")]]))) - :user-edit-dialog (-> user-edit-dialog + (wrap-entity [:form-params :db/id] default-read) + (wrap-schema-decode :form-schema form-schema) + (wrap-nested-form-params) + (wrap-form-4xx-2 (wrap-entity user-dialog [:form-params :db/id] default-read))) + :user-client-new (-> new-client + (wrap-schema-decode :query-schema [:map + [:index {:optional true + :default 0} [nat-int? {:default 0}]]])) + :user-edit-dialog (-> user-dialog + (wrap-entity [:route-params :db/id] default-read) (wrap-schema-decode :route-schema (mc/schema [:map [:db/id entity-id]]))) :user-impersonate (-> impersonate + (wrap-entity [:params :db/id] default-read) (wrap-schema-decode :params-schema (mc/schema [:map [:db/id entity-id]])))} (fn [h] (-> h (wrap-admin) (wrap-client-redirect-unauthenticated))))) - diff --git a/src/clj/auto_ap/ssr/utils.clj b/src/clj/auto_ap/ssr/utils.clj index 6529aebb..13b53c0d 100644 --- a/src/clj/auto_ap/ssr/utils.clj +++ b/src/clj/auto_ap/ssr/utils.clj @@ -1,15 +1,15 @@ (ns auto-ap.ssr.utils (:require - [auto-ap.datomic :refer [all-schema]] + [auto-ap.datomic :refer [all-schema conn]] [auto-ap.logging :as alog] [clojure.string :as str] [config.core :refer [env]] + [datomic.api :as dc] [hiccup2.core :as hiccup] [malli.core :as mc] [malli.error :as me] [malli.transform :as mt2] - [slingshot.slingshot :refer [throw+ try+]] - [manifold.time :as mt])) + [slingshot.slingshot :refer [throw+ try+]])) (defn html-response [hiccup & {:keys [status headers oob] :or {status 200 headers {} oob []}}] {:status status @@ -27,6 +27,25 @@ o)) oob)))}) +(defn modal-response [hiccup & {:as opts}] + (apply html-response + (into + [hiccup] + (mapcat identity + (-> opts + (assoc-in [:headers "hx-trigger"] "modalopen") + (assoc-in [:headers "hx-retarget"] "#modal-content") + (assoc-in [:headers "hx-reswap"] "innerHTML")))))) + +(defn next-step-modal-response [hiccup & {:as opts}] + (apply html-response + (into + [hiccup] + (mapcat identity + (-> opts + (assoc-in [:headers "hx-retarget"] "#modal-content") + (assoc-in [:headers "hx-reswap"] "innerHTML")))))) + (defn wrap-error-response [handler] (fn [request] (try @@ -92,17 +111,25 @@ (defn parse-empty-as-nil [] (mt2/transformer {:decoders - {:double empty->nil + {:string empty->nil + :double empty->nil :int empty->nil :long empty->nil 'nat-int? empty->nil}})) -(def entity-id (mc/schema [nat-int? {:error/message "required"} ])) +(def entity-id (mc/schema [nat-int? {:error/message "required" + :decode/arbitrary (fn [e] + (if (and (map? e) (:db/id e)) + (:db/id e) + e))} ])) -(def temp-id (mc/schema :string)) +(def temp-id (mc/schema [:string {:min 1}])) (def money (mc/schema [:double])) -(def percentage (mc/schema [:double {:decode/arbitrary (fn [x] (some-> x (* 0.01))) - :max 1.0 +(def percentage (mc/schema [:double {:decode/string {:enter (fn [x] + (if (and (string? x) (re-find #"^\d+(\.\d+)?$" x)) + (-> x (Double/parseDouble) (* 0.01)) + x))} + :max 1.0 :error/message "1-100"}])) (def regex (mc/schema [:fn {:error/message "not a regex"} @@ -116,15 +143,11 @@ (def map->db-id-decoder {:enter (fn [x] - (into [] - (for [[k v] (sort-by (comp #(Long/parseLong %) name first) x)] - v - #_(assoc v :db/id (cond (and (string? k) (re-find #"^\d+$" k)) - (Long/parseLong k) - (keyword? k) - (name k) - :else - k)))))}) + (if (sequential? x) + x + (into [] + (for [[k v] (sort-by (comp #(Long/parseLong %) name first) x)] + v))))}) (defn many-entity [params & keys] (mc/schema @@ -146,9 +169,8 @@ (defn keyword->str [k] (subs (str k) 1)) -(defn validation-error [m & {:as data}] - (throw+ (ex-info m (merge data {:type :validation})))) +;; TODO make this bubble the form data automatically (defn field-validation-error [m path & {:as data}] (throw+ (ex-info m (merge data {:type :field-validation :form-errors (assoc-in {} path [m])})))) @@ -166,6 +188,16 @@ (mt2/transformer {:name :arbitrary}) mt2/default-value-transformer)) +(defn strip [s] + (cond (and (string? s) (str/blank? s)) + nil + + (string? s) + (str/trim s) + + :else + s)) + (defn wrap-schema-decode [handler & {:keys [form-schema query-schema route-schema params-schema]}] (fn [{:keys [form-params query-params params] :as request}] (let [request (try @@ -199,7 +231,6 @@ main-transformer))) (catch Exception e (alog/warn ::validation-error :error e) - (throw (ex-info (->> (-> e (ex-data ) :data @@ -217,7 +248,11 @@ (handler request)))) (defn ref->enum-schema [n] - (into [:enum {:decode/string #(keyword n %)}] + (into [:enum {:decode/string #(if (keyword? %) + % + (when (not-empty %) + (keyword n %)) + )}] (for [{:db/keys [ident]} (all-schema) :when (= n (namespace ident))] ident))) @@ -239,42 +274,6 @@ {:value (name ident) :content (str/replace (str/capitalize (name ident)) "-" " ")}))) - -#_(defn namespaceize-decoder [n] - {:exit (fn [m] - (when m - (reduce - (fn [m [k v]] - (if (= k "id") - (assoc m :db/id v) - (assoc m (keyword n (name k)) v))) - m - m)))}) - - -(defn wrap-form-4xx [handler] - (fn [request] - (try+ - (handler request) - - - (catch [:type :validation] e - (alog/warn ::form-4xx :error e) - (html-response [:span.error-content.text-red-500 (:message &throw-context)] - :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+ @@ -290,18 +289,18 @@ (:errors (:explain (:error e))))] (alog/warn ::form-4xx :errors errors) (form-handler (assoc request - :last-form (:decoded e) + :form-params (:decoded e) :field-validation-errors errors :form-errors humanized))) #_(html-response [:span.error-content.text-red-500 (:message &throw-context)] :status 400)) (catch [:type :field-validation] e (form-handler (assoc request - :last-form (:form e) + :form-params (:form e) :form-errors (:form-errors e)))) (catch [:type :form-validation] e (form-handler (assoc request - :last-form (:form e) + :form-params (:form e) :form-validation-errors (:form-validation-errors e) :form-errors {:errors (:form-validation-errors e)})))))) @@ -317,10 +316,24 @@ (defn path->name2 [k & rest] (let [k->n (fn [k] (if (keyword? k) - (str (namespace k) "/" (name k)) + (str (when (namespace k) + (str (namespace k) "/")) + (name k)) k))] (str (k->n k) (str/join "" (map (fn [k] (str "[" (k->n k) "]")) rest))))) + + +(defn wrap-entity [handler path read] + (fn wrap-entity-request [request] + (let [entity (some->> + (get-in request path) + (#(if (string? %) (Long/parseLong %) %)) + (dc/pull (dc/db conn) read ))] + (handler (if entity + (assoc request + :entity entity) + request))))) diff --git a/src/cljc/auto_ap/extract-line-items-from-invoice.repl b/src/cljc/auto_ap/extract-line-items-from-invoice.repl new file mode 100644 index 00000000..a4c2cbd7 --- /dev/null +++ b/src/cljc/auto_ap/extract-line-items-from-invoice.repl @@ -0,0 +1,31 @@ +;; This buffer is for Clojure experiments and evaluation. + +;; Press C-j to evaluate the last expression. + +;; You can also press C-u C-j to evaluate the expression and pretty-print its result. + +(user/init-repl) + +(dc/q '[:find ?nc + :in $ ?c + :where + [?je :journal-entry/client ?c] + ] + [:client/code "NGOP"]) + + + + + +(->> + (:line-items (first (:line-item-groups (first p)))) + (map (fn [li] + (->> (:line-item-expense-fields li) + (map + (fn [field] + [(:text (:label-detection field)) (:text (:value-detection field))]) + ) + (filter first) + (into {})))) + (clojure.data.json/pprint)) + diff --git a/src/cljc/auto_ap/ssr_routes.cljc b/src/cljc/auto_ap/ssr_routes.cljc index cad4d193..bddbcebc 100644 --- a/src/cljc/auto_ap/ssr_routes.cljc +++ b/src/cljc/auto_ap/ssr_routes.cljc @@ -16,6 +16,7 @@ ["/inspect/" [#"\d+" :entity-id] #"/?"] :admin-history-inspect} "/user" {"" {:get :users :put :user-edit-save} + "/client/new" :user-client-new "/table" :user-table "/impersonate" :user-impersonate [[#"\d+" :db/id] "/edit"] {:get :user-edit-dialog}} @@ -36,9 +37,11 @@ :put :admin-transaction-rule-save :post :admin-transaction-rule-save} "/table" :admin-transaction-rule-table + "/account/filled" :admin-transaction-rule-filled-account "/account/new" :admin-transaction-rule-new-account "/account/location-select" :admin-transaction-rule-location-select "/account/typeahead" :admin-transaction-rule-account-typeahead + "/test" :admin-transaction-rule-test "/new" {:get :admin-transaction-rule-new-dialog} [[#"\d+" :db/id] "/edit"] :admin-transaction-rule-edit-dialog }} diff --git a/tailwind.config.js b/tailwind.config.js index b2056c91..dc5cc907 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -5,6 +5,22 @@ module.exports = { "./node_modules/flowbite/**/*.js"], theme: { extend: { + "keyframes": { + shake: { + '0%': { transform: 'translateX(0px)' }, + '12.5%': { transform: 'translateX(-5px)' }, + '25%': { transform: 'translateX(0px)' }, + '37.5%': { transform: 'translateX(5px)' }, + '50%': { transform: 'translateX(0px)' }, + '62.5%': { transform: 'translateX(-5px)' }, + '75%': { transform: 'translateX(5px)' }, + '87.5%': { transform: 'translateX(5px)' }, + '100%': { transform: 'translateX(0px)' }, + }, + }, + animation: { + 'shake': 'shake 0.5s ease-out 1', + }, "fontFamily": { "sans": ["Calibri", "ui-sans-serif", "system-ui", "-apple-system", "BlinkMacSystemFont", "Segoe UI", "Roboto", "Helvetica Neue", "Arial", "Noto Sans", "sans-serif", "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"]