Files
pn-new-crm/src/app/api/v1/yachts/[id]/field-history/route.ts
Matt 52493801e0
Some checks failed
Build & Push Docker Images / lint (push) Failing after 1m35s
Build & Push Docker Images / build-and-push (push) Has been skipped
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>
2026-05-22 12:57:47 +02:00

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);
}
}),
);