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,26 @@
'use client';
import { useQuery } from '@tanstack/react-query';
import { apiFetch } from '@/lib/api/client';
import { getVocabularyDef, type VocabularyKey } from '@/lib/vocabularies';
interface VocabulariesResponse {
data: Record<string, readonly string[]>;
}
/**
* Fetches the effective per-port vocabulary list for a key. Falls back
* to the shipped defaults when the request hasn't resolved or the key
* is missing from the response. Cached for 5 minutes — vocabularies
* change rarely and the admin Vocabularies page invalidates by save.
*/
export function useVocabulary(key: VocabularyKey): readonly string[] {
const def = getVocabularyDef(key);
const { data } = useQuery<VocabulariesResponse>({
queryKey: ['vocabularies'],
queryFn: () => apiFetch<VocabulariesResponse>('/api/v1/vocabularies'),
staleTime: 5 * 60 * 1000,
});
return data?.data[key] ?? def.defaults;
}