feat(uat-batch): M43 — form-template bindings + inline field history
Closes plan item 43 (Form-template fields bind to Interest/Client data —
autofill, override-preservation history, dual-surface audit trail).
Phase 1 — Editor:
- New bindable-fields catalog (src/lib/templates/bindable-fields.ts):
client/yacht/interest paths, each tagged with the entity, column, and
default input type. Source of truth for what can bind + what
interest_field_history.field_path strings the writers should use.
- formFieldSchema gains optional bindTo, validated against the catalog
as an allow-list (no arbitrary paths sneak through).
- form-template-form admin sheet: per-field "Bind to" dropdown grouped
by entity, auto-derives label/key/type when a binding is picked,
shows "Autofills from + writes back to {label} . {path}" badge.
Phase 2 — Runtime + history writes:
- supplemental-forms.service.applySubmission already wrote
interest_field_history rows for client name/email/address from the
earlier 0081 migration session. Extended to also capture phone +
yacht (name, length, width, draft) diffs that were silently going
to the entity without an audit row, and to push insert-path
overrides for the no-existing-address case.
- Field paths aligned with the bindable-fields catalog so detail-page
lookups work via exact-match WHERE field_path = ?.
Phase 3 — Inline history surface:
- New /api/v1/clients/[id]/field-history (mirror of the existing
interests endpoint).
- shared/field-history: FieldHistoryProvider wraps a detail tab and
fires a single keyed GET; FieldHistoryIcon consumes the context and
renders a small clock affordance only when at least one override
exists, opening a popover with the reverse-chrono diff list.
- Client + Interest detail Overview tabs wrapped in the provider;
EditableRow gains an optional historyPath prop; ContactsEditor
renders the icon next to the canonical primary email/phone.
1454/1454 vitest, tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { BINDABLE_PATHS } from '@/lib/templates/bindable-fields';
|
||||
|
||||
export const formFieldSchema = z.object({
|
||||
key: z.string().min(1).max(80),
|
||||
label: z.string().min(1).max(200),
|
||||
@@ -7,6 +9,19 @@ export const formFieldSchema = z.object({
|
||||
required: z.boolean().optional().default(false),
|
||||
options: z.array(z.string()).optional(),
|
||||
helpText: z.string().optional(),
|
||||
/**
|
||||
* Optional binding to an Interest/Client/Yacht column. When set, the
|
||||
* supplemental-form runtime prefills the field from the linked entity
|
||||
* AND writes the submitted value back to that column on apply (with an
|
||||
* `interest_field_history` row capturing the override). Validated
|
||||
* against `BINDABLE_FIELDS` so unknown paths can't sneak in.
|
||||
*/
|
||||
bindTo: z
|
||||
.string()
|
||||
.refine((v) => BINDABLE_PATHS.includes(v), {
|
||||
message: 'Unknown bindable path',
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
|
||||
export const createFormTemplateSchema = z.object({
|
||||
|
||||
Reference in New Issue
Block a user