feat(uat-batch): M43 follow-up — yacht detail field history
Extends Phase 3 from the M43 commit to yacht detail: - New /api/v1/yachts/[id]/field-history endpoint joins through interests.yachtId (no schema migration needed) and filters to 'yacht.%' paths so client-scoped overrides on the same interest don't bleed into the yacht surface. - FieldHistoryScope.type accepts 'yacht'; provider URL routing generalised to /api/v1/<type>s/<id>/field-history. - yacht-tabs OverviewTab wrapped in the provider; Name + the three ft-dimension rows get historyPath wired (m-dimension rows skipped — they're a unit-converted view of the same source value, and the supplemental writer only ever stores ft). Addresses tab on Client detail intentionally left unwired — would need AddressesEditor (a shared component) to surface icons per row, which is more than the 5-min scope. 1454/1454 vitest, tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
50
src/app/api/v1/yachts/[id]/field-history/route.ts
Normal file
50
src/app/api/v1/yachts/[id]/field-history/route.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { and, desc, eq, sql } from 'drizzle-orm';
|
||||
|
||||
import { withAuth, withPermission } from '@/lib/api/helpers';
|
||||
import { db } from '@/lib/db';
|
||||
import { interestFieldHistory, interests } from '@/lib/db/schema';
|
||||
import { errorResponse } from '@/lib/errors';
|
||||
|
||||
/**
|
||||
* GET /api/v1/yachts/[id]/field-history
|
||||
*
|
||||
* Returns every supplemental-form override that touched the yacht,
|
||||
* resolved by joining interest_field_history through interests.yachtId.
|
||||
* The history table itself doesn't carry a yacht_id column (the writer
|
||||
* scopes by interest + client only) — joining at read time avoids a
|
||||
* schema migration just to support this rollup.
|
||||
*/
|
||||
export const GET = withAuth(
|
||||
withPermission('yachts', 'view', async (_req, ctx, params) => {
|
||||
try {
|
||||
const rows = await db
|
||||
.select({
|
||||
id: interestFieldHistory.id,
|
||||
fieldPath: interestFieldHistory.fieldPath,
|
||||
oldValue: interestFieldHistory.oldValue,
|
||||
newValue: interestFieldHistory.newValue,
|
||||
source: interestFieldHistory.source,
|
||||
createdAt: interestFieldHistory.createdAt,
|
||||
})
|
||||
.from(interestFieldHistory)
|
||||
.innerJoin(interests, eq(interests.id, interestFieldHistory.interestId))
|
||||
.where(
|
||||
and(
|
||||
eq(interestFieldHistory.portId, ctx.portId),
|
||||
eq(interests.yachtId, params.id!),
|
||||
// Restrict to actually-yacht-scoped paths so the rollup
|
||||
// doesn't surface "client email changed" rows on yacht detail
|
||||
// (those overrides came in via a supplemental form attached
|
||||
// to an interest that happens to link this yacht).
|
||||
sql`${interestFieldHistory.fieldPath} LIKE 'yacht.%'`,
|
||||
),
|
||||
)
|
||||
.orderBy(desc(interestFieldHistory.createdAt))
|
||||
.limit(100);
|
||||
return NextResponse.json({ data: rows });
|
||||
} catch (error) {
|
||||
return errorResponse(error);
|
||||
}
|
||||
}),
|
||||
);
|
||||
Reference in New Issue
Block a user