feat(admin): vocabularies page for per-port pick lists

New /admin/vocabularies route + VocabulariesManager component. Catalog
at src/lib/vocabularies.ts defines 11 vocabularies grouped into
Interests / Berths / Expenses / Documents domains, each shipping with
the canonical defaults from src/lib/constants.ts (interest temps,
status-change reasons, tenure types, expense categories, document
types, plus the 5 berth-spec dropdowns).

Editor supports add / remove / reorder / inline-rename / reset-to-
defaults; only dirty cards save. Uses the existing
/api/v1/admin/settings PUT endpoint (already gated on
admin.manage_settings) so storage piggybacks on system_settings
(port_id, key) per the established pattern.

Reps need read access without holding manage_settings — added a
public-read /api/v1/vocabularies endpoint plus useVocabulary() hook
(5-minute staleTime). The admin manager invalidates the vocabularies
query on save so consumers (status-change dialog, expense form, etc.)
pick up new lists immediately.

Adds a Vocabularies card to the admin landing page.

Follow-up sweep owed: actual consumers (interest-card temperature pill,
berth-tabs select dropdowns, expense form category list, etc.) still
read from the hardcoded constants.ts arrays. Wire them through
useVocabulary in a separate pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 18:36:53 +02:00
parent 07b5756014
commit da7ce16344
6 changed files with 534 additions and 0 deletions

View File

@@ -0,0 +1,33 @@
import { NextResponse } from 'next/server';
import { withAuth } from '@/lib/api/helpers';
import { errorResponse } from '@/lib/errors';
import { listSettings } from '@/lib/services/settings.service';
import { resolveVocabulary, VOCABULARIES, type VocabularyKey } from '@/lib/vocabularies';
/**
* GET /api/v1/vocabularies
*
* Returns the resolved per-port vocabulary lists (admin overrides
* merged with shipped defaults). Any authenticated user in this port
* can read — vocabularies drive in-app pickers (status reasons,
* interest temperatures, expense categories, etc.) so reps need read
* access without holding `admin.manage_settings`.
*
* Edits still go through `/api/v1/admin/settings` (admin-only).
*/
export const GET = withAuth(async (_req, ctx) => {
try {
const { portSettings } = await listSettings(ctx.portId);
const overridesByKey = new Map<string, unknown>();
for (const row of portSettings) overridesByKey.set(row.key, row.value);
const data: Record<string, readonly string[]> = {};
for (const def of VOCABULARIES) {
data[def.key] = resolveVocabulary(def.key as VocabularyKey, overridesByKey.get(def.key));
}
return NextResponse.json({ data });
} catch (error) {
return errorResponse(error);
}
});