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>
51 lines
1.9 KiB
TypeScript
51 lines
1.9 KiB
TypeScript
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);
|
|
}
|
|
}),
|
|
);
|