refactor(ssr): revert hiccup→Selmer migration; render forms in Hiccup again

Abandons the Selmer-templating step of the SSR re-authoring and moves the four
migrated form/wizard modals back to Hiccup (com/* components), keeping the
whole-form HTMX swap doctrine, top-rooted render functions, and the
session-backed wizard engine unchanged.

- transaction/edit, transaction/bulk_code, invoices (bulk-edit group), and
  pos/sales_summaries render via com/* again; every hx-* swap (whole-form +
  targeted location-cell / totals-tbody / inline account-cell swaps) is
  preserved exactly.
- add com/single-modal-card to centralize the md:w-[950px] md:h-[650px] modal
  chrome that previously lived only in the Selmer modal-card templates.
- delete auto-ap.ssr.selmer, auto-ap.ssr.components.selmer, selmer_test, the
  whole resources/templates tree (55 files), the selmer dependency, and the
  tailwind resources/templates content glob.
- strip Selmer guidance from the ssr-form-migration skill + modernization plan.

Verified: all four namespaces compile and render with no stringified-hiccup
leaks; output.css rebuilds byte-identically (no Tailwind class loss); 60 e2e
specs pass — the four reverted modals (incl. whole-form-swap focus/caret tests)
plus the untouched wizard/pay/new/rule modals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-30 00:37:21 -07:00
parent e1a2f7b638
commit 8b43017d6e
71 changed files with 746 additions and 1793 deletions

View File

@@ -1,9 +0,0 @@
<a class="{{ extra }} 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 {% if color = "secondary" %}text-white bg-blue-500 hover:bg-blue-600 focus:ring-blue-300 dark:bg-blue-600 dark:hover:bg-blue-700 focus:ring-blue-250{% elif color = "primary" %}text-white bg-green-500 hover:bg-green-600 focus:ring-green-300 dark:bg-green-600 dark:hover:bg-green-700 focus:ring-green-250{% elif color = "red" %}text-white bg-red-500 hover:bg-red-600 focus:ring-red-250 dark:bg-red-600 dark:hover:bg-red-700{% else %}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{% endif %}"{{ attrs|safe }}>
{% if indicator %}
<div class="htmx-indicator flex items-center">
{% include "templates/components/spinner.html" %}
<div class="ml-3">Loading...</div>
</div>
{% endif %}
<div class="inline-flex gap-2 items-center justify-center{% if indicator %} htmx-indicator-hidden{% endif %}">{{ body|safe }}</div>
</a>

View File

@@ -1,3 +0,0 @@
<a class="{{ extra }} inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center 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"{{ attrs|safe }}>
<div class="h-4 w-4">{{ body|safe }}</div>
</a>

View File

@@ -1,2 +0,0 @@
<div class="{{ classes }}"{{ attrs|safe }}>{{ body|safe }}
</div>

View File

@@ -1,2 +0,0 @@
<button class="{{ classes }}"{{ attrs|safe }}>{{ body|safe }}
</button>

View File

@@ -1,6 +0,0 @@
<div class="inline-flex rounded-md shadow-sm"
role="group"
hx-on:click="this.querySelector(&quot;input&quot;).value = event.target.value; this.querySelector(&quot;input&quot;).dispatchEvent(new Event('change', {bubbles: true}));">
<input type="hidden" name="{{ name }}">
{{ body|safe }}
</div>

View File

@@ -1,7 +0,0 @@
<button class="{{ extra }} focus:ring-4 font-bold rounded-lg text-xs p-3 text-center mr-2 inline-flex items-center relative justify-center disabled:opacity-50 hover:scale-105 transition duration-100 {% if color = "primary" %}bg-green-500 hover:bg-green-600 focus:ring-green-250 dark:bg-green-600 dark:hover:bg-green-700 text-white{% elif color = "red" %}bg-red-500 hover:bg-red-600 focus:ring-red-250 dark:bg-red-600 dark:hover:bg-red-700 text-white{% else %}bg-white-500 hover:bg-white-600 focus:ring-white-250 dark:bg-white-600 dark:hover:bg-white-700 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{% endif %}"{{ attrs|safe }}>
<div class="htmx-indicator flex items-center absolute inset-0 justify-center">
{% include "templates/components/spinner.html" %}
{% if loading_label %}<div class="ml-3">Loading...</div>{% endif %}
</div>
<div class="htmx-indicator-invisible inline-flex gap-2 items-center justify-center">{{ body|safe }}</div>
</button>

View File

@@ -1,3 +0,0 @@
<td class="px-4 py-2{% if klass %} {{ klass }}{% endif %}"
{{ attrs|safe }}>{{ body|safe }}
</td>

View File

@@ -1,10 +0,0 @@
<th class="px-4 py-3{% if klass %} {{ klass }}{% endif %}"
scope="col"
@click="{{ click|safe }}"
{{ attrs|safe }}>
{% if sort_key %}
<a href="#">{{ body|safe }}</a>
{% else %}
{{ body|safe }}
{% endif %}
</th>

View File

@@ -1,2 +0,0 @@
<tr class="{{ classes }}"{{ attrs|safe }}>{{ body|safe }}
</tr>

View File

@@ -1,11 +0,0 @@
<div class="shrink overflow-y-scroll">
<table class="{{ table_class }}"{{ table_attrs|safe }}>
<thead class="{{ thead_class }}">
<tr>{{ headers|safe }}</tr>
</thead>
<tbody>
{{ rows|safe }}
</tbody>
{{ footer_tbody|safe }}
</table>
</div>

View File

@@ -1,3 +0,0 @@
{# Hidden input. The Clojure wrapper (sc/hidden) serializes the full attribute map
(name, value, optional id/form/class/Alpine :value bind) into `attrs`. #}
<input type="hidden"{{ attrs|safe }}>

View File

@@ -1 +0,0 @@
<a class="{{ classes }}"{{ attrs|safe }}>{{ body|safe }}</a>

View File

@@ -1,8 +0,0 @@
{# Location <select> for a transaction account row. Plain-HTML attributes -- the Selmer
migration target (no Hiccup keyword/string attribute ambiguity). Rendered into the
surrounding Hiccup row via the auto-ap.ssr.selmer interop bridge. #}
<select name="{{ name }}" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 {{ variant }}">
{% for opt in options %}
<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>
{% endfor %}
</select>

View File

@@ -1,15 +0,0 @@
{# Base modal-card chrome (single-step: header / optional side panel / body / footer).
A child template extends this and fills the head / side_panel / body / footer blocks,
so the whole card renders from one shared context in a single render call. Enter
triggers the footer save button via $refs.next. Mirrors transaction-edit/edit-modal
(the string-slot version still used by Transaction Edit). #}
<div class="modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen md:w-[950px] md:h-[650px] w-full h-full last-modal-step transition duration-150"
@keydown.enter.prevent.stop="if ($refs.next) {$refs.next.click()}"
x-data="">
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0">{% block head %}{% endblock %}</div>
<div class="flex shrink overflow-auto grow">
{% block side_panel %}{% endblock %}
<div class="px-6 py-2 space-y-6 overflow-y-scroll w-full shrink grow">{% block body %}{% endblock %}</div>
</div>
<div class="p-4 border-t">{% block footer %}{% endblock %}</div>
</div>

View File

@@ -1,2 +0,0 @@
<div class="{{ classes }}" @click.outside="open=false"{{ attrs|safe }}>{{ body|safe }}
</div>

View File

@@ -1,4 +0,0 @@
{# Money input (number, step=0.01, right-aligned). Owns its class base; callers pass the
non-class attributes via attrs + a variant (width/size) class. Class set =
inputs/default-input-classes + appearance-none/text-right + the variant. #}
<input type="number" step="0.01" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 appearance-none text-right {{ variant }}"{{ attrs|safe }}>

View File

@@ -1,16 +0,0 @@
<ul class="{{ ul_class }}">
{% for opt in options %}
<li class="{{ li_class }}">
<div class="{{ div_class }}">
<input id="{{ opt.id }}"
type="radio"
value="{{ opt.value }}"
name="{{ name }}"
class="{{ input_class }}"
{{ input_attrs|safe }}
{% if opt.checked %}checked{% endif %}>
<label for="{{ opt.id }}" class="{{ label_class }}">{{ opt.content|safe }}</label>
</div>
</li>
{% endfor %}
</ul>

View File

@@ -1,8 +0,0 @@
{# Generic <select>. options = [{value, label, selected}]. Plain-HTML attributes -- the
Selmer migration target (no Hiccup keyword/string attribute ambiguity). Extra attrs
(hx-*, x-*) ride through {{ attrs|safe }}. #}
<select name="{{ name }}" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 {{ variant }}"{{ attrs|safe }}>
{% for opt in options %}
<option value="{{ opt.value }}"{% if opt.selected %} selected{% endif %}>{{ opt.label }}</option>
{% endfor %}
</select>

View File

@@ -1,11 +0,0 @@
<svg aria-hidden="true"
class="animate-spin inline w-4 h-4 text-white"
fill="none"
role="status"
viewbox="0 0 100 101"
xmlns="http://www.w3.org/2000/svg">
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="#E5E7EB">
</path>
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentColor">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1,7 +0,0 @@
<svg aria-hidden="true"
fill="none"
stroke="currentColor"
viewbox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg">
<path d="M19 9l-7 7-7-7" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"></path>
</svg>

Before

Width:  |  Height:  |  Size: 240 B

View File

@@ -1,8 +0,0 @@
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<defs></defs><title>navigation-next</title>
<path d="M23,9.5H12.387a4,4,0,0,0-4,4v2" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor">
</path>
<polyline fill="none" points="19 13.498 23 9.498 19 5.498" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor"></polyline>
<path d="M14.387,13v5.5a1,1,0,0,1-1,1h-12a1,1,0,0,1-1-1V6.5a1,1,0,0,1,1-1h12a1,1,0,0,1,1,1V7" fill="none" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor">
</path>
</svg>

Before

Width:  |  Height:  |  Size: 579 B

View File

@@ -1,3 +0,0 @@
<svg viewbox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<defs></defs><title>delete-2</title><circle cx="12" cy="12" fill="none" r="11.5" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor"></circle><line fill="none" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor" x1="7" x2="17" y1="7" y2="17"></line><line fill="none" stroke-linecap="round" stroke-linejoin="round" stroke="currentColor" x1="17" x2="7" y1="7" y2="17"></line>
</svg>

Before

Width:  |  Height:  |  Size: 478 B

View File

@@ -1,3 +0,0 @@
{# Text input. sc/text-input builds the full attr map (type/autocomplete/class+size
already merged, reusing inputs/default-input-classes + hh/add-class) into `attrs`. #}
<input {{ attrs|safe }}>

View File

@@ -1,60 +0,0 @@
{# Click-to-select typeahead (Alpine + tippy). Survives whole-form swaps; null-guarded
tippy?. / $refs.input? throughout. The Clojure wrapper (sc/typeahead) resolves the
initial {value,label} server-side and builds x_data + the hidden-input attrs. #}
<div class="relative"
x-data="{{ x_data }}"
x-modelable="value.value"
{% if x_model %}x-model="{{ x_model }}"{% endif %}
{% if key %}key="{{ key }}"{% endif %}>
{% if disabled %}
<span x-text="value.label"></span>
{% else %}
<a class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 cursor-pointer {{ width }}"
x-tooltip.on.click="{content: ()=>($refs.dropdown?.innerHTML ?? ''), placement: 'bottom', onMount(i) {htmx.process(i.popper); }, popperOptions: {strategy: 'fixed', modifiers: [{name: 'flip', options: {fallbackPlacements: ['top']}}]}, theme: 'dropdown', allowHTML: true, interactive:true}"
@keydown.down.prevent.stop="tippy?.show();"
@keydown.backspace="tippy?.hide(); value = {value: '', label: '' }"
tabindex="0"
x-init="{{ a_xinit }}"
x-ref="input"><input {{ hidden_attrs|safe }}>
<div class="flex w-full justify-items-stretch">
<span class="flex-grow text-left" x-text="value.label"></span>
<div class="w-3 h-3 m-1 inline ml-1 justify-self-end text-gray-500 self-center">
{% include "templates/components/svg-drop-down.html" %}
</div>
<div x-show="value.warning">
<div class="peer absolute inline-flex items-center z-10 justify-center w-6 h-6 text-xs font-black text-white border-3 border-white rounded-full -top-2 -right-2 dark:border-gray-900 bg-red-300"
x-tooltip="value.warning">!</div>
</div>
</div>
</a>
{% endif %}
<template x-ref="dropdown">
<ul class="dropdown-contents bg-gray-100 dark:bg-gray-600 ring-1"
@keydown.escape="$refs.input?.__x_tippy?.hide(); value = {value: '', label: '' }; "
x-destroy="if ($refs.input) {$refs.input.focus();}">
<input type="text"
autofocus
class="bg-gray-50 border-bottom text-sm rounded-t-lg block p-2.5 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500 bg-gray-100 w-full {{ width }}"
x-model="search"
placeholder="{{ placeholder }}"
@change.stop=""
@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.stop="$refs.input?.__x_tippy?.hide(); value = elements.length > 0 ? $data.elements[active >= 0 ? active : 0] : {'value': '', label: ''}; $refs.input?.focus()"
x-init="$el.focus(); $watch('search', s => { if($el.value.length > 2) {fetch(addQueryParam(baseUrl, 'q', s)).then(data=>data.json()).then(data => {elements = data; active=-1; $refs.input?.__x_tippy?.popperInstance?.update()}) }})">
<div class="dropdown-options 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-500 [&.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; $refs.input?.__x_tippy?.hide(); setTimeout(() => $refs.input?.focus(), 10)"
x-html="element.label"></a></li>
</template><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</li>
</template>
</div>
</ul>
</template>
</div>

View File

@@ -1,12 +0,0 @@
{# Field wrapper with label + always-present error <p> (the errors- variant of field-).
Owns the group / has-error toggle; `has_error` is set when the field has errors,
`extra` is the caller's own class. `attrs` carries any pass-through div attributes (the
per-row location cell hangs its hx-* / x-dispatch swap wiring here); `body` is the
pre-rendered inner control HTML; `errors_str` is the comma-joined string errors. #}
<div class="group {% if has_error %}has-error {% endif %}{{ extra }}"{{ attrs|safe }}>
{% if label %}
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ label }}</label>
{% endif %}
{{ body|safe }}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ errors_str }}</p>
</div>

View File

@@ -1,8 +0,0 @@
<div id="interop-smoke" class="p-2">
<h3>{{ title }}</h3>
{# a Hiccup-rendered component, passed in pre-rendered and emitted verbatim #}
{{ hiccup_frag|safe }}
<input x-ref="input"
x-model="value.value"
@keydown.down.prevent.stop="tippy?.show()" />
</div>

View File

@@ -1,5 +0,0 @@
{# Top-level plain bulk-edit form (no wizard). The resolved (not-locked) invoice id set
rides in hidden ids[] fields so the selection survives form-changed / submit posts
without an EDN snapshot or a filter round-trip. #}
<form id="bulk-edit-form"{{ form_attrs|safe }}>{{ ids_hidden|safe }}{{ modal|safe }}
</form>

View File

@@ -1,7 +0,0 @@
{# Running TOTAL / BALANCE percentage rows in their own swappable <tbody>, a sibling of
the input rows, so a percentage edit refreshes them with a targeted swap (Rule 4) and
never replaces the input-bearing rows above. Replaces the old per-cell bulk-edit-total
/ bulk-edit-balance routes. #}
<tbody id="expense-totals">
{{ rows|safe }}
</tbody>

View File

@@ -1,7 +0,0 @@
{# Plain sales-summary edit form (no wizard). db/id rides in a hidden field; all other
state is the live form, re-derived against the entity each request (no EDN snapshot,
no step-params). #}
<form id="summary-edit-form"{{ form_attrs|safe }}>
<input type="hidden" name="db/id" value="{{ db_id }}">
{{ modal|safe }}
</form>

View File

@@ -1,21 +0,0 @@
{# Sales-summary modal body: a read-only Debits | Credits two-column view of the auto
items (each account is inline-editable), a swappable totals/balance block, and an
editable Manual Items section with a working "New Summary Item" add. #}
<div class="space-y-4 p-2">
<div class="grid grid-cols-2 gap-6">
<div>
<div class="font-semibold text-sm mb-2">Debits</div>
<div class="space-y-1">{{ debit_rows|safe }}</div>
</div>
<div>
<div class="font-semibold text-sm mb-2">Credits</div>
<div class="space-y-1">{{ credit_rows|safe }}</div>
</div>
</div>
<div id="summary-totals">{{ totals|safe }}</div>
<div class="mt-4 border-t pt-3">
<div class="font-semibold text-sm mb-2">Manual Items</div>
<div class="space-y-2" id="manual-items">{{ manual_rows|safe }}</div>
<div class="mt-2 flex justify-center">{{ new_item_button|safe }}</div>
</div>
</div>

View File

@@ -1,22 +0,0 @@
{# Expense-account grid -- fully template-driven. A single for-loop over the per-row
view-models (bulk-code/account-row-vm), each delegating to account-row.html. The
trailing "New account" button (a-button partial) posts the whole #bulk-code-form
(op=new-account). #}
<div class="shrink overflow-y-scroll">
<table class="w-full text-sm text-left text-gray-500 dark:text-gray-400 shrink">
<thead class="text-xs text-gray-800 uppercase bg-gray-50 dark:bg-gray-700 dark:text-gray-400 group-[.raw]:sticky group-[.raw]:z-10 group-[.raw]:top-0">
<tr>
<th class="px-4 py-3" scope="col">Account</th>
<th class="px-4 py-3 w-32" scope="col">Location</th>
<th class="px-4 py-3 w-16" scope="col">%</th>
<th class="px-4 py-3 w-16" scope="col"></th>
</tr>
</thead>
<tbody>
{% for row in accounts.rows %}{% include "templates/transaction-bulk-code/account-row.html" %}{% endfor %}
<tr class="new-row border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700">
<td class="px-4 py-2" colspan="4">{% with color=accounts.new_account.color extra=accounts.new_account.extra attrs=accounts.new_account.attrs indicator=accounts.new_account.indicator body=accounts.new_account.body %}{% include "templates/components/a-button.html" %}{% endwith %}</td>
</tr>
</tbody>
</table>
</div>

View File

@@ -1,34 +0,0 @@
{# One expense-account row from a loop-bound `row` view-model. All structure, wiring, and
field names are literal here, built from `row.index` + the shared `urls`; only data (the
Alpine x-data, db/id, errors, and the typeahead / location / money control contexts)
comes from the view-model. The location cell (#account-location-N) swaps just itself on
account change; the remove button swaps the whole #bulk-code-form. #}
<tr class="account-row border-b dark:border-gray-600 group hover:bg-gray-100 dark:hover:bg-gray-700"
id="account-row-{{ row.index }}" x-data="{{ row.x_data }}" x-ref="p" x-show="show"
x-init="$nextTick(() => show=true)"
x-transition:enter="transition-opacity duration-500" x-transition:enter-start="opacity-0" x-transition:enter-end="opacity-100"
x-transition:leave="transition duration-500" x-transition:leave-start="opacity-100" x-transition:leave-end="opacity-0">
<input type="hidden" name="accounts[{{ row.index }}][db/id]"{% if row.db_id_value %} value="{{ row.db_id_value }}"{% endif %}>
<td class="px-4 py-2">
<div class="group {% if row.account_has_error %}has-error {% endif %}">
<div class="flex flex-col">{% with width="" x_data=row.account.x_data x_model=row.account.x_model key=row.account.key disabled=row.account.disabled a_xinit=row.account.a_xinit placeholder=row.account.placeholder hidden_attrs=row.account.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}</div>
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.account_error }}</p>
</div>
</td>
<td class="px-4 py-2" id="account-location-{{ row.index }}">
<div class="group {% if row.location_has_error %}has-error {% endif %}"
hx-post="{{ urls.changed }}" hx-target="#account-location-{{ row.index }}" hx-select="#account-location-{{ row.index }}"
hx-vals='{"name":"accounts[{{ row.index }}][location]"{% if client_id %},"client-id":{{ client_id }}{% endif %}}'
x-hx-val:account-id="accountId" x-dispatch:changed="accountId" hx-trigger="changed" hx-swap="outerHTML" hx-include="closest form">
{% with name=row.location.name variant="w-full" options=row.location.options %}{% include "templates/components/location-select.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.location_error }}</p>
</div>
</td>
<td class="px-4 py-2">
<div class="group {% if row.pct_has_error %}has-error {% endif %}">
{% with variant=row.pct.variant attrs=row.pct.attrs %}{% include "templates/components/money-input.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ row.pct_error }}</p>
</div>
</td>
<td class="px-4 py-2 align-top"><a href="" class="account-remove-action p-3 inline-flex items-center justify-center bg-white dark:bg-gray-600 items-center 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" hx-post="{{ urls.changed }}" hx-vals='{"op":"remove-account","row-index":{{ row.index }}}' hx-target="#bulk-code-form" hx-select="#bulk-code-form" hx-swap="outerHTML" hx-include="closest form"><div class="h-4 w-4">{% include "templates/components/svg-x.html" %}</div></a></td>
</tr>

View File

@@ -1,35 +0,0 @@
{# Bulk-code modal body: vendor typeahead (a change repopulates the default account via a
whole-form swap), status select, and the expense-account grid. All wiring, the status
options, and the field-wrapper classes are literal here; only data (selected values,
resolved labels, errors) comes from the view-model. #}
<div class="space-y-4 p-4">
<div class="grid grid-cols-2 gap-4">
<div hx-trigger="change" hx-post="{{ urls.changed }}" hx-vals='{"op":"vendor-changed"}' hx-target="#bulk-code-form" hx-select="#bulk-code-form" hx-swap="outerHTML" hx-sync="this:replace" hx-include="closest form">
<div class="group {% if vendor.has_error %}has-error {% endif %}">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Vendor</label>
{% with width="w-96" x_data=vendor.ta.x_data x_model=vendor.ta.x_model key=vendor.ta.key disabled=vendor.ta.disabled a_xinit=vendor.ta.a_xinit placeholder=vendor.ta.placeholder hidden_attrs=vendor.ta.hidden_attrs %}{% include "templates/components/typeahead.html" %}{% endwith %}
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ vendor.error }}</p>
</div>
</div>
<div>
<div class="group {% if status.has_error %}has-error {% endif %}">
<label class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">Status</label>
<select name="approval-status" class="bg-gray-50 border text-sm rounded-lg block p-2.5 border-gray-300 text-gray-900 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500 group-[.has-error]:bg-red-50 group-[.has-error]:border-red-500 group-[.has-error]:text-red-900 group-[.has-error]:placeholder-red-700 group-[.has-error]:focus:ring-red-500 group-[.has-error]:dark:bg-gray-700 group-[.has-error]:focus:border-red-500 group-[.has-error]:dark:text-red-500 group-[.has-error]:dark:placeholder-red-500 group-[.has-error]:dark:border-red-500">
<option value=""{% if not status.value %} selected{% endif %}>No Change</option>
<option value="approved"{% if status.value = "approved" %} selected{% endif %}>Approved</option>
<option value="unapproved"{% if status.value = "unapproved" %} selected{% endif %}>Unapproved</option>
<option value="suppressed"{% if status.value = "suppressed" %} selected{% endif %}>Suppressed</option>
<option value="requires-feedback"{% if status.value = "requires-feedback" %} selected{% endif %}>Client Review</option>
</select>
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ status.error }}</p>
</div>
</div>
<div class="col-span-2 pt-4">
<h3 class="text-lg font-medium mb-3">Expense Accounts</h3>
<div class="group">
<div id="account-entries" class="space-y-3">{% include "templates/transaction-bulk-code/account-grid.html" %}</div>
<p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ accounts.error }}</p>
</div>
</div>
</div>
</div>

View File

@@ -1,6 +0,0 @@
{# Bulk-code modal card: extends the shared modal-card base and fills its blocks with the
bulk-code head / body / footer partials. No side panel. #}
{% extends "templates/components/modal-card.html" %}
{% block head %}{% include "templates/transaction-bulk-code/head.html" %}{% endblock %}
{% block body %}{% include "templates/transaction-bulk-code/body.html" %}{% endblock %}
{% block footer %}{% include "templates/transaction-bulk-code/footer.html" %}{% endblock %}

View File

@@ -1,5 +0,0 @@
{# Modal footer: the form-errors sink on the left, the Save button on the right. Both
pull from the shared view-model; the button reuses components/button.html. #}
<div class="flex justify-end">
<div class="flex items-baseline gap-x-4">{% include "templates/transaction-bulk-code/form-errors.html" %}{% with color=save.color extra=save.extra attrs=save.attrs loading_label=save.loading_label body=save.body %}{% include "templates/components/button.html" %}{% endwith %}</div>
</div>

View File

@@ -1,4 +0,0 @@
{# Submit-error sink. A 4xx submit swaps the inner `.error-content` (hx-target-400);
the span is present only when there are form-level errors, matching the prior
hand-rolled markup byte-for-byte. #}
<div id="form-errors">{% if errors_str %}<span class="error-content"><p class="mt-2 text-xs text-red-600 dark:text-red-500 h-4">{{ errors_str }}</p></span>{% endif %}</div>

View File

@@ -1,5 +0,0 @@
{# Single render entrypoint for the bulk-code form. The route passes one view-model and
every sub-template composes from it via includes/blocks. The resolved (not-locked)
transaction id set rides in hidden ids[] fields so the selection survives
form-changed / submit posts without an EDN snapshot or a filter round-trip. #}
<form id="bulk-code-form" hx-ext="response-targets" hx-swap="outerHTML" hx-target-400="#form-errors .error-content" hx-trigger="submit" hx-target="this" hx-post="{{ urls.submit }}">{% for id in ids %}<input type="hidden" name="ids[{{ forloop.counter0 }}]" value="{{ id }}">{% endfor %}<div class="" @click.outside="open=false" id="bulkcodemodal">{% include "templates/transaction-bulk-code/card.html" %}</div></form>

View File

@@ -1,2 +0,0 @@
{# Modal header label: how many transactions this bulk-code operation will touch. #}
<div class="p-2">Bulk editing {{ head_count }} transactions</div>

View File

@@ -1,3 +0,0 @@
{# Modal-open response: the transitioner shell the modal stack expects, wrapping the whole
form. Composes in one render from the shared view-model. #}
<div id="transitioner" class="flex-1">{% include "templates/transaction-bulk-code/form.html" %}</div>

View File

@@ -1,2 +0,0 @@
{# Post-submit confirmation message embedded in the shared success modal. #}
<p>Successfully coded {{ count }} transactions.</p>

View File

@@ -1,5 +0,0 @@
{# Totals live in their own swappable <tbody> so an amount edit refreshes them with a
targeted swap, never replacing the input-bearing rows above (caret survives). #}
<tbody id="account-totals">
{{ rows|safe }}
</tbody>

View File

@@ -1,4 +0,0 @@
<div x-data="{{ x_data }}">
{{ status_hidden|safe }}
<div class="inline-flex rounded-md shadow-sm" role="group">{{ buttons|safe }}</div>
</div>

View File

@@ -1,39 +0,0 @@
{# Read-only transaction summary shown in the modal's left side panel. #}
<div class="p-4 space-y-4">
<h3 class="text-sm font-semibold text-gray-900 uppercase tracking-wider">Details</h3>
<div class="space-y-3">
<div>
<div class="text-xs font-medium text-gray-500">Amount</div>
<div class="text-sm font-medium text-gray-900">{{ amount }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Date</div>
<div class="text-sm text-gray-900">{{ date }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Bank Account</div>
<div class="text-sm text-gray-900">{{ bank_account }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Post Date</div>
<div class="text-sm text-gray-900">{{ post_date }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Description</div>
<div class="text-sm text-gray-900 truncate cursor-help"
title="{{ description_original }}">{{ description_simple }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Check Number</div>
<div class="text-sm text-gray-900">{{ check_number }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Status</div>
<div class="text-sm text-gray-900">{{ status }}</div>
</div>
<div>
<div class="text-xs font-medium text-gray-500">Transaction Type</div>
<div class="text-sm text-gray-900">{{ type }}</div>
</div>
</div>
</div>

View File

@@ -1,7 +0,0 @@
{# Top-level plain form. The entity id rides in a hidden field; all other state is the
live form, re-derived against the entity each request (no serialized snapshot, no
wizard step-params). #}
<form id="edit-form"{{ form_attrs|safe }}>
<input type="hidden" name="db/id" value="{{ db_id }}">
{{ modal|safe }}
</form>

View File

@@ -1,15 +0,0 @@
{# Modal card chrome (header / optional side panel / body / footer). Single-step, so
no timeline, no back/next nav -- just the Done button in the footer. Enter triggers
the save button via $refs.next. #}
<div class="modal-card bg-white rounded-lg shadow dark:bg-gray-700 dark:text-white modal-content flex flex-col max-h-screen max-w-screen md:w-[950px] md:h-[650px] w-full h-full last-modal-step transition duration-150"
@keydown.enter.prevent.stop="if ($refs.next) {$refs.next.click()}"
x-data="">
<div class="flex items-start justify-between p-4 border-b rounded-t dark:border-gray-600 shrink-0">{{ head|safe }}</div>
<div class="flex shrink overflow-auto grow">
{% if side_panel %}
<div class="grow-0 w-64 bg-gray-50 border-r hidden md:block overflow-y-auto max-h-full">{{ side_panel|safe }}</div>
{% endif %}
<div class="px-6 py-2 space-y-6 overflow-y-scroll w-full shrink grow">{{ body|safe }}</div>
</div>
<div class="p-4 border-t">{{ footer|safe }}</div>
</div>

View File

@@ -1,6 +0,0 @@
<div class="ml-3">
<span class="block text-sm font-medium">{{ number }}</span>
<span class="block text-sm text-gray-500">{{ vendor }}</span>
<span class="block text-sm text-gray-500">{{ date }}</span>
<span class="block text-sm font-medium">{{ amount }}</span>
</div>

View File

@@ -1,27 +0,0 @@
<div class="my-4 p-4 bg-blue-50 rounded">
<h3 class="text-lg font-bold mb-2">Linked Payment{{ external_link|safe }}</h3>
<div class="space-y-2">
<div class="flex justify-between">
<div class="font-medium">Payment #</div>
<div>{{ number }}</div>
</div>
<div class="flex justify-between">
<div class="font-medium">Vendor</div>
<div>{{ vendor }}</div>
</div>
<div class="flex justify-between">
<div class="font-medium">Amount</div>
<div>{{ amount }}</div>
</div>
<div class="flex justify-between">
<div class="font-medium">Status</div>
<div>{{ status }}</div>
</div>
<div class="flex justify-between">
<div class="font-medium">Date</div>
<div>{{ date }}</div>
</div>
{{ payment_id_hidden|safe }}<div class="mt-4"{{ unlink_attrs|safe }}>{{ unlink_button|safe }}
</div>
</div>
</div>

View File

@@ -1,32 +0,0 @@
{# The single step's body: memo + the activeForm tab switcher (link payment / unpaid /
autopay / rule / manual) + the five x-show panels. Fragments are pre-rendered. #}
<div class="space-y-1">
<div>
{{ memo_field|safe }}
<div x-data="{{ x_data }}" @unlinked="canChange=true">
<div class="flex space-x-2 mb-4">{{ action_hidden|safe }}{{ tabs|safe }}</div>
<div x-show="activeForm === 'link-payment'"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100">{{ panel_payment|safe }}</div>
<div x-show="activeForm === 'link-unpaid-invoices'"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100">{{ panel_unpaid|safe }}</div>
<div x-show="activeForm === 'link-autopay-invoices'"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100">{{ panel_autopay|safe }}</div>
<div x-show="activeForm === 'apply-rule'"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100">{{ panel_rule|safe }}</div>
<div x-show="activeForm === 'manual'"
x-transition:enter="transition ease-out duration-500"
x-transition:enter-start="opacity-0 transform scale-95"
x-transition:enter-end="opacity-100 transform scale-100">
<div>{{ panel_manual|safe }}</div>
</div>
</div>
</div>
</div>

View File

@@ -1,11 +0,0 @@
{# Vendor field (a change repopulates the default account via a whole-form swap) + either
the simple single-row coding or the advanced account grid. #}
<div id="manual-coding-section">
{{ mode_hidden|safe }}<div {{ vendor_changed_attrs|safe }}>{{ vendor_field|safe }}
</div>
{% if is_simple %}
<div x-data="{{ simple_xdata }}">{{ simple_mode|safe }}</div>
{% else %}
<div>{{ toggle_link|safe }}{{ accounts_field|safe }}</div>
{% endif %}
</div>

View File

@@ -1 +0,0 @@
<div class="text-center py-4 text-gray-500">{{ message }}</div>

View File

@@ -1,10 +0,0 @@
{# Shared shell for the autopay/unpaid/rule match panels: heading + action hidden +
prompt label + a radio-card of options. #}
<div>
<h3 class="text-lg font-bold mb-4">{{ heading }}</h3>
{{ action_hidden|safe }}
<div class="space-y-2">
<label class="block text-sm font-medium mb-1">{{ prompt }}</label>
{{ radio|safe }}
</div>
</div>

View File

@@ -1,2 +0,0 @@
{# Outer wrapper for the payment tab; the unlink-payment route swaps #payment-matches. #}
<div id="payment-matches">{{ inner|safe }}</div>

View File

@@ -1,4 +0,0 @@
<div class="ml-3">
<span class="block text-sm font-medium">{{ note }}</span>
<span class="block text-sm text-gray-500">{{ description }}</span>
</div>

View File

@@ -1,14 +0,0 @@
{# Simple mode: a single account row (account typeahead + location select) rendered at a
fixed index 0, plus the link to switch to the advanced grid. Selecting the account
swaps just the location cell (#simple-account-location). #}
<div>
<span>{{ row_id_hidden|safe }}
<div class="flex gap-2 mt-2">
{{ account_field|safe }}
<div id="simple-account-location">{{ location_field|safe }}</div>
{{ amount_hidden|safe }}
</div>
</span>
<div class="mt-1"><a class="text-sm text-blue-600 hover:underline cursor-pointer"
{{ toggle_attrs|safe }}>Switch to advanced mode</a></div>
</div>

View File

@@ -1,3 +0,0 @@
{# Wrapper the modal stack expects around the opened form (the wizard transition hooks
are gone -- there is only one step). #}
<div id="transitioner" class="flex-1">{{ body|safe }}</div>