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:
35
src/app/api/v1/clients/[id]/field-history/route.ts
Normal file
35
src/app/api/v1/clients/[id]/field-history/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { and, desc, eq } from 'drizzle-orm';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { db } from '@/lib/db';
|
||||
import { interestFieldHistory } from '@/lib/db/schema';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
|
||||
/**
|
||||
* GET /api/v1/clients/[id]/field-history
|
||||
*
|
||||
* Returns every supplemental-form override that touched the client (rolling
|
||||
* up across all of their interests), newest first. Powers the inline clock
|
||||
* icon + popover on Client detail. Mirrors /interests/[id]/field-history.
|
||||
*/
|
||||
export const GET = withAuth(
|
||||
withPermission('clients', 'view', async (_req, ctx, params) => {
|
||||
try {
|
||||
const rows = await db
|
||||
.select()
|
||||
.from(interestFieldHistory)
|
||||
.where(
|
||||
and(
|
||||
eq(interestFieldHistory.portId, ctx.portId),
|
||||
eq(interestFieldHistory.clientId, params.id!),
|
||||
),
|
||||
)
|
||||
.orderBy(desc(interestFieldHistory.createdAt))
|
||||
.limit(100);
|
||||
return NextResponse.json({ data: rows });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user