@freighttech/shipment-tracking 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -46,21 +46,67 @@ function mapItem(item) {
46
46
  const emptyForm = {
47
47
  carrierCode: "",
48
48
  apiEndpoint: "",
49
- authConfig: "",
49
+ authValues: {},
50
50
  rateLimitRequests: 60,
51
51
  rateLimitWindowSeconds: 60,
52
52
  isActive: true
53
53
  };
54
+ function authConfigToValues(authConfig) {
55
+ if (!authConfig) return {};
56
+ return Object.fromEntries(
57
+ Object.entries(authConfig).map(([k, v]) => [k, typeof v === "string" ? v : JSON.stringify(v)])
58
+ );
59
+ }
54
60
  function rowToForm(row) {
55
61
  return {
56
62
  carrierCode: row.carrierCode,
57
63
  apiEndpoint: row.apiEndpoint ?? "",
58
- authConfig: row.authConfig ? JSON.stringify(row.authConfig, null, 2) : "",
64
+ authValues: authConfigToValues(row.authConfig),
59
65
  rateLimitRequests: row.rateLimitRequests,
60
66
  rateLimitWindowSeconds: row.rateLimitWindowSeconds,
61
67
  isActive: row.isActive
62
68
  };
63
69
  }
70
+ const CARRIER_AUTH_FIELDS = {
71
+ msc: [
72
+ { key: "client_id", label: "Client ID" },
73
+ { key: "scope", label: "Scope" },
74
+ { key: "token_url", label: "Token URL" },
75
+ { key: "aud", label: "Audience", placeholder: "defaults to Token URL if blank" },
76
+ { key: "certificate_base64", label: "Certificate (PFX, base64)", secret: true, multiline: true }
77
+ ],
78
+ maersk: [
79
+ { key: "client_id", label: "Client ID" },
80
+ { key: "client_secret", label: "Client Secret", secret: true }
81
+ ],
82
+ "hapag-lloyd": [
83
+ { key: "client_id", label: "Client ID" },
84
+ { key: "client_secret", label: "Client Secret", secret: true }
85
+ ],
86
+ evergreen: [
87
+ { key: "client_id", label: "Client ID" },
88
+ { key: "client_secret", label: "Client Secret", secret: true },
89
+ { key: "token_url", label: "Token URL" }
90
+ ],
91
+ zim: [
92
+ { key: "client_id", label: "Client ID" },
93
+ { key: "client_secret", label: "Client Secret", secret: true },
94
+ { key: "subscription_key", label: "Subscription Key", secret: true }
95
+ ],
96
+ cosco: [
97
+ { key: "app_key", label: "App Key", secret: true },
98
+ { key: "customer_id", label: "Customer ID" },
99
+ { key: "scac_code", label: "SCAC Code" }
100
+ ],
101
+ "cma-cgm": [
102
+ { key: "api_key", label: "API Key", secret: true }
103
+ ]
104
+ };
105
+ function valuesToAuthConfig(values) {
106
+ return Object.fromEntries(
107
+ Object.entries(values).map(([k, v]) => [k.trim(), v]).filter(([k, v]) => k !== "" && String(v).trim() !== "")
108
+ );
109
+ }
64
110
  const emptyBicForm = {
65
111
  isEnabled: false,
66
112
  username: "",
@@ -382,14 +428,6 @@ function TrackingAuthConfigPage() {
382
428
  flash(t("shipment_tracking.carrier_configs.validation.carrierCodeRequired", "Carrier code is required"), "error");
383
429
  return;
384
430
  }
385
- if (form.authConfig.trim()) {
386
- try {
387
- JSON.parse(form.authConfig);
388
- } catch {
389
- flash(t("shipment_tracking.carrier_configs.validation.authConfigInvalidJson", "Auth config must be valid JSON"), "error");
390
- return;
391
- }
392
- }
393
431
  if (form.apiEndpoint.trim()) {
394
432
  try {
395
433
  new URL(form.apiEndpoint.trim());
@@ -400,7 +438,8 @@ function TrackingAuthConfigPage() {
400
438
  }
401
439
  setSubmitting(true);
402
440
  try {
403
- const authConfig = form.authConfig.trim() ? JSON.parse(form.authConfig) : void 0;
441
+ const assembled = valuesToAuthConfig(form.authValues);
442
+ const authConfig = Object.keys(assembled).length ? assembled : void 0;
404
443
  if (isEdit) {
405
444
  const body = { id: editingId };
406
445
  if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim();
@@ -553,18 +592,100 @@ function TrackingAuthConfigPage() {
553
592
  )
554
593
  ] }),
555
594
  /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
556
- /* @__PURE__ */ jsx(Label, { htmlFor: "authConfig", children: t("shipment_tracking.carrier_configs.fields.authConfig", "Auth Config (JSON)") }),
557
- /* @__PURE__ */ jsx(
558
- Textarea,
559
- {
560
- id: "authConfig",
561
- value: form.authConfig,
562
- onChange: (event) => setForm((prev) => ({ ...prev, authConfig: event.target.value })),
563
- placeholder: '{"apiKey": "..."}',
564
- rows: 3,
565
- className: "font-mono text-xs"
595
+ /* @__PURE__ */ jsx(Label, { children: t("shipment_tracking.carrier_configs.fields.auth", "Authentication") }),
596
+ (() => {
597
+ const spec = CARRIER_AUTH_FIELDS[form.carrierCode.trim().toLowerCase()];
598
+ const setValue = (key, value) => setForm((prev) => ({ ...prev, authValues: { ...prev.authValues, [key]: value } }));
599
+ if (spec) {
600
+ return spec.map((f) => /* @__PURE__ */ jsxs("div", { className: "grid gap-1.5", children: [
601
+ /* @__PURE__ */ jsx(Label, { htmlFor: `auth-${f.key}`, className: "text-xs text-muted-foreground", children: f.label }),
602
+ f.multiline ? /* @__PURE__ */ jsx(
603
+ Textarea,
604
+ {
605
+ id: `auth-${f.key}`,
606
+ value: form.authValues[f.key] ?? "",
607
+ onChange: (event) => setValue(f.key, event.target.value),
608
+ placeholder: f.placeholder,
609
+ rows: 3,
610
+ className: "font-mono text-xs"
611
+ }
612
+ ) : /* @__PURE__ */ jsx(
613
+ Input,
614
+ {
615
+ id: `auth-${f.key}`,
616
+ type: f.secret ? "password" : "text",
617
+ autoComplete: "off",
618
+ value: form.authValues[f.key] ?? "",
619
+ onChange: (event) => setValue(f.key, event.target.value),
620
+ placeholder: f.placeholder
621
+ }
622
+ )
623
+ ] }, f.key));
566
624
  }
567
- )
625
+ return /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
626
+ Object.entries(form.authValues).map(([k, v], idx) => /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
627
+ /* @__PURE__ */ jsx(
628
+ Input,
629
+ {
630
+ className: "w-1/3 font-mono text-xs",
631
+ defaultValue: k,
632
+ placeholder: "key",
633
+ onBlur: (event) => {
634
+ const nk = event.target.value.trim();
635
+ if (nk === k) return;
636
+ setForm((prev) => {
637
+ const next = { ...prev.authValues };
638
+ const val = next[k] ?? "";
639
+ delete next[k];
640
+ if (nk) next[nk] = val;
641
+ return { ...prev, authValues: next };
642
+ });
643
+ }
644
+ }
645
+ ),
646
+ /* @__PURE__ */ jsx(
647
+ Input,
648
+ {
649
+ className: "flex-1 text-xs",
650
+ value: v,
651
+ placeholder: "value",
652
+ onChange: (event) => setValue(k, event.target.value)
653
+ }
654
+ ),
655
+ /* @__PURE__ */ jsx(
656
+ Button,
657
+ {
658
+ type: "button",
659
+ variant: "outline",
660
+ size: "sm",
661
+ onClick: () => setForm((prev) => {
662
+ const next = { ...prev.authValues };
663
+ delete next[k];
664
+ return { ...prev, authValues: next };
665
+ }),
666
+ children: t("shipment_tracking.carrier_configs.auth.remove", "Remove")
667
+ }
668
+ )
669
+ ] }, idx)),
670
+ /* @__PURE__ */ jsxs(
671
+ Button,
672
+ {
673
+ type: "button",
674
+ variant: "outline",
675
+ size: "sm",
676
+ className: "justify-self-start",
677
+ onClick: () => setForm((prev) => ({
678
+ ...prev,
679
+ authValues: { ...prev.authValues, [`field_${Object.keys(prev.authValues).length + 1}`]: "" }
680
+ })),
681
+ children: [
682
+ /* @__PURE__ */ jsx(Plus, { className: "mr-1 h-3.5 w-3.5" }),
683
+ t("shipment_tracking.carrier_configs.auth.addField", "Add field")
684
+ ]
685
+ }
686
+ )
687
+ ] });
688
+ })()
568
689
  ] }),
569
690
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
570
691
  /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../../src/modules/shipment_tracking/backend/carrier-configs/page.tsx"],
4
- "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useCallback, useMemo, useState } from 'react'\nimport { Page, PageBody } from '@freighttech/ui/backend/Page'\nimport { BooleanIcon } from '@freighttech/ui/backend/ValueIcons'\nimport {\n DynamicTable,\n TableSkeleton,\n useDynamicTablePage,\n} from '@freighttech/ui/backend/dynamic-table'\nimport type { ColumnDef, ContextMenuAction } from '@freighttech/ui/backend/dynamic-table'\nimport { apiCallOrThrow } from '@freighttech/ui/backend/utils/apiCall'\nimport { flash } from '@freighttech/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n} from '@freighttech/ui/primitives/dialog'\nimport { Button } from '@freighttech/ui/primitives/button'\nimport { Button as ButtonV2 } from '@freighttech/ui/primitives-v2'\nimport { Input } from '@freighttech/ui/primitives/input'\nimport { Label } from '@freighttech/ui/primitives/label'\nimport { Checkbox } from '@freighttech/ui/primitives/checkbox'\nimport { Textarea } from '@freighttech/ui/primitives/textarea'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@freighttech/ui/primitives/card'\nimport { Badge } from '@freighttech/ui/primitives/badge'\nimport { Loader2, CheckCircle2, XCircle, Shield, Plus } from 'lucide-react'\nimport { ShipsGoConfigSection } from '../../components/ShipsGoConfigSection'\n\n// \u2500\u2500\u2500 Carrier Config Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype CarrierConfigRow = {\n id: string\n carrierCode: string\n apiEndpoint: string | null\n authConfig: Record<string, unknown> | null\n rateLimitRequests: number\n rateLimitWindowSeconds: number\n isActive: boolean\n createdAt: string | null\n}\n\nfunction mapItem(item: Record<string, unknown>): CarrierConfigRow | null {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n\n return {\n id,\n carrierCode: (item.carrierCode as string) ?? '',\n apiEndpoint: (item.apiEndpoint as string) ?? null,\n authConfig: (item.authConfig as Record<string, unknown>) ?? null,\n rateLimitRequests: typeof item.rateLimitRequests === 'number' ? item.rateLimitRequests : 60,\n rateLimitWindowSeconds: typeof item.rateLimitWindowSeconds === 'number' ? item.rateLimitWindowSeconds : 60,\n isActive: item.isActive === true,\n createdAt: (item.createdAt as string) ?? null,\n }\n}\n\ntype FormState = {\n carrierCode: string\n apiEndpoint: string\n authConfig: string\n rateLimitRequests: number\n rateLimitWindowSeconds: number\n isActive: boolean\n}\n\nconst emptyForm: FormState = {\n carrierCode: '',\n apiEndpoint: '',\n authConfig: '',\n rateLimitRequests: 60,\n rateLimitWindowSeconds: 60,\n isActive: true,\n}\n\nfunction rowToForm(row: CarrierConfigRow): FormState {\n return {\n carrierCode: row.carrierCode,\n apiEndpoint: row.apiEndpoint ?? '',\n authConfig: row.authConfig ? JSON.stringify(row.authConfig, null, 2) : '',\n rateLimitRequests: row.rateLimitRequests,\n rateLimitWindowSeconds: row.rateLimitWindowSeconds,\n isActive: row.isActive,\n }\n}\n\n// \u2500\u2500\u2500 BIC Config Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype BicConfigData = {\n id: string\n organizationId: string\n tenantId: string\n isEnabled: boolean\n username: string\n baseUrl: string\n createdAt: string\n updatedAt: string\n} | null\n\ntype BicFormState = {\n isEnabled: boolean\n username: string\n password: string\n baseUrl: string\n}\n\nconst emptyBicForm: BicFormState = {\n isEnabled: false,\n username: '',\n password: '',\n baseUrl: 'https://api.bic-code.org',\n}\n\n// \u2500\u2500\u2500 BIC Config Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction BicConfigSection() {\n const t = useT()\n const [config, setConfig] = React.useState<BicConfigData>(null)\n const [form, setForm] = React.useState<BicFormState>(emptyBicForm)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [testing, setTesting] = React.useState(false)\n const [testResult, setTestResult] = React.useState<{ success: boolean; message: string } | null>(null)\n\n const fetchConfig = React.useCallback(async () => {\n setLoading(true)\n try {\n const { result } = await apiCallOrThrow<{ config: BicConfigData }>('/api/shipment_tracking/bic-configs')\n setConfig(result?.config ?? null)\n if (result?.config) {\n setForm({\n isEnabled: result.config.isEnabled,\n username: result.config.username,\n password: '', // Password is never returned from API\n baseUrl: result.config.baseUrl,\n })\n } else {\n setForm(emptyBicForm)\n }\n } catch {\n // Config doesn't exist yet, that's fine\n setConfig(null)\n setForm(emptyBicForm)\n } finally {\n setLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchConfig()\n }, [fetchConfig])\n\n const handleTest = React.useCallback(async () => {\n if (!form.username || !form.password) {\n flash(t('shipment_tracking.bic_config.validation.usernameRequired', 'Username is required'), 'error')\n return\n }\n\n setTesting(true)\n setTestResult(null)\n\n try {\n const { result } = await apiCallOrThrow<{ success: boolean; message: string }>(\n '/api/shipment_tracking/bic-configs/test',\n {\n method: 'POST',\n body: JSON.stringify({\n username: form.username,\n password: form.password,\n baseUrl: form.baseUrl,\n }),\n }\n )\n\n setTestResult(result ?? { success: false, message: 'Unknown error' })\n\n if (result?.success) {\n flash(t('shipment_tracking.bic_config.flash.testSuccess', 'BIC API connection successful'), 'success')\n } else {\n flash(result?.message ?? t('shipment_tracking.bic_config.flash.testFailed', 'BIC API connection failed'), 'error')\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Connection test failed'\n setTestResult({ success: false, message })\n flash(message, 'error')\n } finally {\n setTesting(false)\n }\n }, [form, t])\n\n const handleSave = React.useCallback(async () => {\n if (!form.username) {\n flash(t('shipment_tracking.bic_config.validation.usernameRequired', 'Username is required'), 'error')\n return\n }\n\n // Password is required for new config or when enabling\n if (!config && !form.password) {\n flash(t('shipment_tracking.bic_config.validation.passwordRequired', 'Password is required'), 'error')\n return\n }\n\n setSaving(true)\n try {\n const body: Record<string, unknown> = {\n isEnabled: form.isEnabled,\n username: form.username,\n baseUrl: form.baseUrl || 'https://api.bic-code.org',\n }\n\n // Only include password if it was changed\n if (form.password) {\n body.password = form.password\n } else if (config) {\n // If editing and no new password, we still need to send something\n // The API should handle this gracefully\n body.password = '__UNCHANGED__'\n }\n\n await apiCallOrThrow('/api/shipment_tracking/bic-configs', {\n method: 'PUT',\n body: JSON.stringify(body),\n })\n\n flash(t('shipment_tracking.bic_config.flash.saved', 'BIC configuration saved'), 'success')\n fetchConfig()\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to save'\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }, [form, config, t, fetchConfig])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSave()\n }\n },\n [handleSave]\n )\n\n if (loading) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5\" />\n {t('shipment_tracking.bic_config.title', 'BIC Facility API')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n </CardContent>\n </Card>\n )\n }\n\n return (\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5\" />\n <CardTitle>{t('shipment_tracking.bic_config.title', 'BIC Facility API')}</CardTitle>\n </div>\n <div className=\"flex items-center gap-2\">\n {config ? (\n <>\n <Badge variant=\"outline\">\n {t('shipment_tracking.bic_config.status.configured', 'Configured')}\n </Badge>\n <Badge variant={config.isEnabled ? 'default' : 'secondary'}>\n {config.isEnabled\n ? t('shipment_tracking.bic_config.status.enabled', 'Enabled')\n : t('shipment_tracking.bic_config.status.disabled', 'Disabled')}\n </Badge>\n </>\n ) : (\n <Badge variant=\"secondary\">\n {t('shipment_tracking.bic_config.status.notConfigured', 'Not Configured')}\n </Badge>\n )}\n </div>\n </div>\n <CardDescription>\n {t(\n 'shipment_tracking.bic_config.description',\n 'Configure BIC Facility Code API credentials to enrich shipment tracking locations with coordinates, addresses, and terminal operator information.'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent onKeyDown={handleKeyDown}>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"bicEnabled\"\n checked={form.isEnabled}\n onCheckedChange={(checked) =>\n setForm((prev) => ({ ...prev, isEnabled: checked === true }))\n }\n />\n <Label htmlFor=\"bicEnabled\" className=\"cursor-pointer font-medium\">\n {t('shipment_tracking.bic_config.fields.isEnabled', 'Enable BIC Enrichment')}\n </Label>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicUsername\">\n {t('shipment_tracking.bic_config.fields.username', 'Username')}\n </Label>\n <Input\n id=\"bicUsername\"\n value={form.username}\n onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}\n placeholder=\"your-bic-username\"\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicPassword\">\n {t('shipment_tracking.bic_config.fields.password', 'Password')}\n </Label>\n <Input\n id=\"bicPassword\"\n type=\"password\"\n value={form.password}\n onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}\n placeholder={config ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : 'your-bic-password'}\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'shipment_tracking.bic_config.hints.password',\n 'Your password will be encrypted and stored securely.'\n )}\n </p>\n </div>\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicBaseUrl\">\n {t('shipment_tracking.bic_config.fields.baseUrl', 'API Base URL')}\n </Label>\n <Input\n id=\"bicBaseUrl\"\n value={form.baseUrl}\n onChange={(e) => setForm((prev) => ({ ...prev, baseUrl: e.target.value }))}\n placeholder=\"https://api.bic-code.org\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('shipment_tracking.bic_config.hints.baseUrl', 'Default: https://api.bic-code.org')}\n </p>\n </div>\n\n {testResult && (\n <div\n className={`flex items-center gap-2 rounded-md p-3 text-sm ${\n testResult.success\n ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'\n : 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'\n }`}\n >\n {testResult.success ? (\n <CheckCircle2 className=\"h-4 w-4\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n <span>{testResult.message}</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2 pt-2\">\n <Button\n variant=\"outline\"\n onClick={handleTest}\n disabled={testing || !form.username || !form.password}\n >\n {testing && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {t('shipment_tracking.bic_config.actions.testConnection', 'Test Connection')}\n </Button>\n <Button onClick={handleSave} disabled={saving}>\n {saving && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {t('shipment_tracking.bic_config.actions.save', 'Save Configuration')}\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\n// \u2500\u2500\u2500 Main Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default function TrackingAuthConfigPage() {\n const t = useT()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editingId, setEditingId] = useState<string | null>(null)\n const [form, setForm] = useState<FormState>(emptyForm)\n const [submitting, setSubmitting] = useState(false)\n\n const isEdit = editingId !== null\n\n const columns = useMemo<ColumnDef[]>(\n () => [\n {\n data: 'carrierCode',\n title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code'),\n width: 150,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"font-medium\">{String(value ?? '')}</span>\n ),\n },\n {\n data: 'apiEndpoint',\n title: t('shipment_tracking.carrier_configs.fields.apiEndpoint', 'API Endpoint'),\n width: 280,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"text-sm truncate max-w-[260px] block\">{String(value || '-')}</span>\n ),\n },\n {\n data: 'rateLimit',\n title: t('shipment_tracking.carrier_configs.fields.rateLimit', 'Rate Limit'),\n width: 120,\n readOnly: true,\n renderer: (_value: unknown, rowData: Record<string, unknown>) => (\n <span className=\"text-sm\">\n {String(rowData.rateLimitRequests ?? 60)} / {String(rowData.rateLimitWindowSeconds ?? 60)}s\n </span>\n ),\n },\n {\n data: 'isActive',\n title: t('shipment_tracking.carrier_configs.fields.isActive', 'Active'),\n width: 80,\n readOnly: true,\n renderer: (value: unknown) => <BooleanIcon value={value === true || value === 'true'} />,\n },\n ],\n [t],\n )\n\n const openCreateDialog = useCallback(() => {\n setEditingId(null)\n setForm(emptyForm)\n setDialogOpen(true)\n }, [])\n\n const addButton = useMemo(\n () => (\n <ButtonV2 variant=\"primary\" size=\"sm\" icon={<Plus className=\"h-4 w-4\" />} onClick={openCreateDialog}>\n {t('shipment_tracking.carrier_configs.create', 'Add Carrier Config')}\n </ButtonV2>\n ),\n [openCreateDialog, t],\n )\n\n const table = useDynamicTablePage<CarrierConfigRow>({\n source: '/api/shipment_tracking/carrier-configs',\n // Saved views. Without this the perspective tab strip auto-hides\n // (hidePerspectiveTabs ?? !hasPerspectives), so users cannot save or\n // switch a view on this list at all.\n perspectives: 'shipment_tracking_carrier_configs',\n columns,\n tableName: t('shipment_tracking.carrier_configs.title', 'Carrier Configs'),\n defaultPageSize: 20,\n mapApiItem: mapItem,\n cellEdit: false,\n tableProps: {\n height: '100%',\n uiConfig: {\n readOnlyStyle: 'normal',\n hideAddRowButton: true,\n searchBarEnd: addButton,\n },\n },\n })\n\n const openEditDialog = useCallback(\n (id: string) => {\n const items = (table.query.data as { items?: Record<string, unknown>[] })?.items ?? []\n const mapped = items.map(mapItem).filter((x): x is CarrierConfigRow => x !== null)\n const row = mapped.find((r) => r.id === id)\n if (!row) return\n setEditingId(id)\n setForm(rowToForm(row))\n setDialogOpen(true)\n },\n [table.query.data],\n )\n\n const handleSubmit = useCallback(async () => {\n if (submitting) return\n\n if (!isEdit && !form.carrierCode.trim()) {\n flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier code is required'), 'error')\n return\n }\n\n if (form.authConfig.trim()) {\n try {\n JSON.parse(form.authConfig)\n } catch {\n flash(t('shipment_tracking.carrier_configs.validation.authConfigInvalidJson', 'Auth config must be valid JSON'), 'error')\n return\n }\n }\n\n if (form.apiEndpoint.trim()) {\n try {\n new URL(form.apiEndpoint.trim())\n } catch {\n flash(t('shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl', 'API endpoint must be a valid URL'), 'error')\n return\n }\n }\n\n setSubmitting(true)\n try {\n const authConfig = form.authConfig.trim() ? JSON.parse(form.authConfig) : undefined\n\n if (isEdit) {\n const body: Record<string, unknown> = { id: editingId }\n if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()\n else body.apiEndpoint = null\n body.authConfig = authConfig ?? null\n body.rateLimitRequests = form.rateLimitRequests\n body.rateLimitWindowSeconds = form.rateLimitWindowSeconds\n body.isActive = form.isActive\n\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'PUT',\n body: JSON.stringify(body),\n })\n flash(t('shipment_tracking.carrier_configs.flash.updated', 'Carrier config updated'), 'success')\n } else {\n const body: Record<string, unknown> = {\n carrierCode: form.carrierCode.trim(),\n rateLimitRequests: form.rateLimitRequests,\n rateLimitWindowSeconds: form.rateLimitWindowSeconds,\n isActive: form.isActive,\n }\n if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()\n if (authConfig) body.authConfig = authConfig\n\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'POST',\n body: JSON.stringify(body),\n })\n flash(t('shipment_tracking.carrier_configs.flash.created', 'Carrier config created'), 'success')\n }\n\n setDialogOpen(false)\n table.refresh()\n } catch {\n flash(isEdit ? t('shipment_tracking.carrier_configs.flash.updateFailed', 'Failed to update carrier config') : t('shipment_tracking.carrier_configs.flash.createFailed', 'Failed to create carrier config'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [form, isEdit, editingId, submitting, table.refresh, t])\n\n const handleDialogKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n const handleDelete = useCallback(\n async (id: string) => {\n if (!confirm(t('shipment_tracking.carrier_configs.confirmDelete', 'Delete this carrier config?'))) return\n\n try {\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'DELETE',\n body: JSON.stringify({ id }),\n })\n flash(t('shipment_tracking.carrier_configs.flash.deleted', 'Carrier config deleted'), 'success')\n table.refresh()\n } catch {\n flash(t('shipment_tracking.carrier_configs.flash.deleteFailed', 'Failed to delete carrier config'), 'error')\n }\n },\n [table.refresh, t],\n )\n\n const rowActions = useCallback(\n (rowData: any): ContextMenuAction[] => {\n if (!rowData?.id) return []\n return [\n { id: 'edit', label: t('common.edit', 'Edit') },\n { id: 'delete', label: t('common.delete', 'Delete') },\n ]\n },\n [t],\n )\n\n const handleRowAction = useCallback(\n (actionId: string, rowData: any) => {\n const id = rowData?.id as string | undefined\n if (!id) return\n if (actionId === 'edit') openEditDialog(id)\n else if (actionId === 'delete') handleDelete(id)\n },\n [openEditDialog, handleDelete],\n )\n\n const tableHeight = useMemo(() => {\n const rowH = 40\n const headerH = 40\n const toolbarH = 50\n const minHeight = 200\n const maxHeight = 400\n const rowCount = (table.query.data as { total?: number })?.total ?? 0\n const contentHeight = toolbarH + headerH + rowCount * rowH + 20\n return Math.min(Math.max(contentHeight, minHeight), maxHeight)\n }, [table.query.data])\n\n if (table.isLoading) {\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <BicConfigSection />\n <ShipsGoConfigSection />\n <Card>\n <CardHeader>\n <CardTitle>{t('shipment_tracking.carrier_configs.title', 'Carrier Configs')}</CardTitle>\n </CardHeader>\n <CardContent>\n <TableSkeleton rows={5} columns={4} />\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n {/* BIC Config Section */}\n <BicConfigSection />\n\n {/* ShipsGo Aggregator Config Section */}\n <ShipsGoConfigSection />\n\n {/* Carrier Configs Section */}\n <Card>\n <CardHeader>\n <CardTitle>{t('shipment_tracking.carrier_configs.title', 'Carrier Configs')}</CardTitle>\n <CardDescription>\n {t(\n 'shipment_tracking.auth_config.description',\n 'Configure authentication for carrier tracking APIs and facility enrichment services.'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div style={{ height: tableHeight }}>\n <DynamicTable\n {...table.props}\n striped\n density=\"md\"\n rowActions={rowActions}\n onRowAction={handleRowAction}\n />\n </div>\n </CardContent>\n </Card>\n\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogContent className=\"sm:max-w-lg\" onKeyDown={handleDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>{isEdit ? t('shipment_tracking.carrier_configs.edit', 'Edit Carrier Config') : t('shipment_tracking.carrier_configs.create', 'Add Carrier Config')}</DialogTitle>\n </DialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"carrierCode\">{t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code')}</Label>\n <Input\n id=\"carrierCode\"\n value={form.carrierCode}\n onChange={(event) => setForm((prev) => ({ ...prev, carrierCode: event.target.value }))}\n placeholder=\"e.g. maersk, msc, cma-cgm\"\n disabled={isEdit}\n autoFocus={!isEdit}\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"apiEndpoint\">{t('shipment_tracking.carrier_configs.fields.apiEndpoint', 'API Endpoint')}</Label>\n <Input\n id=\"apiEndpoint\"\n value={form.apiEndpoint}\n onChange={(event) => setForm((prev) => ({ ...prev, apiEndpoint: event.target.value }))}\n placeholder=\"https://api.example.com/tracking\"\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"authConfig\">{t('shipment_tracking.carrier_configs.fields.authConfig', 'Auth Config (JSON)')}</Label>\n <Textarea\n id=\"authConfig\"\n value={form.authConfig}\n onChange={(event) => setForm((prev) => ({ ...prev, authConfig: event.target.value }))}\n placeholder='{\"apiKey\": \"...\"}'\n rows={3}\n className=\"font-mono text-xs\"\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"rateLimitRequests\">{t('shipment_tracking.carrier_configs.fields.rateLimitRequests', 'Rate Limit (requests)')}</Label>\n <Input\n id=\"rateLimitRequests\"\n type=\"number\"\n min={1}\n max={10000}\n value={form.rateLimitRequests}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, rateLimitRequests: Number(event.target.value) || 60 }))\n }\n />\n </div>\n <div className=\"grid gap-2\">\n <Label htmlFor=\"rateLimitWindowSeconds\">{t('shipment_tracking.carrier_configs.fields.rateLimitWindowSeconds', 'Window (seconds)')}</Label>\n <Input\n id=\"rateLimitWindowSeconds\"\n type=\"number\"\n min={1}\n max={86400}\n value={form.rateLimitWindowSeconds}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, rateLimitWindowSeconds: Number(event.target.value) || 60 }))\n }\n />\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"isActive\"\n checked={form.isActive}\n onCheckedChange={(checked) =>\n setForm((prev) => ({ ...prev, isActive: checked === true }))\n }\n />\n <Label htmlFor=\"isActive\" className=\"cursor-pointer\">\n {t('shipment_tracking.carrier_configs.fields.isActive', 'Active')}\n </Label>\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDialogOpen(false)} disabled={submitting}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={handleSubmit} disabled={submitting}>\n {submitting ? t('common.saving', 'Saving...') : isEdit ? t('common.save', 'Save') : t('common.create', 'Create')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useCallback, useMemo, useState } from 'react'\nimport { Page, PageBody } from '@freighttech/ui/backend/Page'\nimport { BooleanIcon } from '@freighttech/ui/backend/ValueIcons'\nimport {\n DynamicTable,\n TableSkeleton,\n useDynamicTablePage,\n} from '@freighttech/ui/backend/dynamic-table'\nimport type { ColumnDef, ContextMenuAction } from '@freighttech/ui/backend/dynamic-table'\nimport { apiCallOrThrow } from '@freighttech/ui/backend/utils/apiCall'\nimport { flash } from '@freighttech/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n} from '@freighttech/ui/primitives/dialog'\nimport { Button } from '@freighttech/ui/primitives/button'\nimport { Button as ButtonV2 } from '@freighttech/ui/primitives-v2'\nimport { Input } from '@freighttech/ui/primitives/input'\nimport { Label } from '@freighttech/ui/primitives/label'\nimport { Checkbox } from '@freighttech/ui/primitives/checkbox'\nimport { Textarea } from '@freighttech/ui/primitives/textarea'\nimport { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@freighttech/ui/primitives/card'\nimport { Badge } from '@freighttech/ui/primitives/badge'\nimport { Loader2, CheckCircle2, XCircle, Shield, Plus } from 'lucide-react'\nimport { ShipsGoConfigSection } from '../../components/ShipsGoConfigSection'\n\n// \u2500\u2500\u2500 Carrier Config Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype CarrierConfigRow = {\n id: string\n carrierCode: string\n apiEndpoint: string | null\n authConfig: Record<string, unknown> | null\n rateLimitRequests: number\n rateLimitWindowSeconds: number\n isActive: boolean\n createdAt: string | null\n}\n\nfunction mapItem(item: Record<string, unknown>): CarrierConfigRow | null {\n const id = typeof item.id === 'string' ? item.id : null\n if (!id) return null\n\n return {\n id,\n carrierCode: (item.carrierCode as string) ?? '',\n apiEndpoint: (item.apiEndpoint as string) ?? null,\n authConfig: (item.authConfig as Record<string, unknown>) ?? null,\n rateLimitRequests: typeof item.rateLimitRequests === 'number' ? item.rateLimitRequests : 60,\n rateLimitWindowSeconds: typeof item.rateLimitWindowSeconds === 'number' ? item.rateLimitWindowSeconds : 60,\n isActive: item.isActive === true,\n createdAt: (item.createdAt as string) ?? null,\n }\n}\n\ntype FormState = {\n carrierCode: string\n apiEndpoint: string\n /** authConfig as flat key\u2192value; assembled into the authConfig object on save. */\n authValues: Record<string, string>\n rateLimitRequests: number\n rateLimitWindowSeconds: number\n isActive: boolean\n}\n\nconst emptyForm: FormState = {\n carrierCode: '',\n apiEndpoint: '',\n authValues: {},\n rateLimitRequests: 60,\n rateLimitWindowSeconds: 60,\n isActive: true,\n}\n\n/** Flatten a stored authConfig object into editable string values. */\nfunction authConfigToValues(authConfig: Record<string, unknown> | null): Record<string, string> {\n if (!authConfig) return {}\n return Object.fromEntries(\n Object.entries(authConfig).map(([k, v]) => [k, typeof v === 'string' ? v : JSON.stringify(v)]),\n )\n}\n\nfunction rowToForm(row: CarrierConfigRow): FormState {\n return {\n carrierCode: row.carrierCode,\n apiEndpoint: row.apiEndpoint ?? '',\n authValues: authConfigToValues(row.authConfig),\n rateLimitRequests: row.rateLimitRequests,\n rateLimitWindowSeconds: row.rateLimitWindowSeconds,\n isActive: row.isActive,\n }\n}\n\n// \u2500\u2500\u2500 Per-carrier auth field specs \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// Labeled inputs for the carriers whose authConfig shape we know (mirrors the\n// keys each adapter reads). Carriers not listed fall back to a generic\n// key/value editor. `secret` masks the input; `multiline` uses a textarea.\n\ntype CarrierAuthField = { key: string; label: string; secret?: boolean; multiline?: boolean; placeholder?: string }\n\nconst CARRIER_AUTH_FIELDS: Record<string, CarrierAuthField[]> = {\n msc: [\n { key: 'client_id', label: 'Client ID' },\n { key: 'scope', label: 'Scope' },\n { key: 'token_url', label: 'Token URL' },\n { key: 'aud', label: 'Audience', placeholder: 'defaults to Token URL if blank' },\n { key: 'certificate_base64', label: 'Certificate (PFX, base64)', secret: true, multiline: true },\n ],\n maersk: [\n { key: 'client_id', label: 'Client ID' },\n { key: 'client_secret', label: 'Client Secret', secret: true },\n ],\n 'hapag-lloyd': [\n { key: 'client_id', label: 'Client ID' },\n { key: 'client_secret', label: 'Client Secret', secret: true },\n ],\n evergreen: [\n { key: 'client_id', label: 'Client ID' },\n { key: 'client_secret', label: 'Client Secret', secret: true },\n { key: 'token_url', label: 'Token URL' },\n ],\n zim: [\n { key: 'client_id', label: 'Client ID' },\n { key: 'client_secret', label: 'Client Secret', secret: true },\n { key: 'subscription_key', label: 'Subscription Key', secret: true },\n ],\n cosco: [\n { key: 'app_key', label: 'App Key', secret: true },\n { key: 'customer_id', label: 'Customer ID' },\n { key: 'scac_code', label: 'SCAC Code' },\n ],\n 'cma-cgm': [\n { key: 'api_key', label: 'API Key', secret: true },\n ],\n}\n\n/** Assemble the authConfig object from form values, dropping blank entries. */\nfunction valuesToAuthConfig(values: Record<string, string>): Record<string, string> {\n return Object.fromEntries(\n Object.entries(values)\n .map(([k, v]) => [k.trim(), v] as const)\n .filter(([k, v]) => k !== '' && String(v).trim() !== ''),\n )\n}\n\n// \u2500\u2500\u2500 BIC Config Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype BicConfigData = {\n id: string\n organizationId: string\n tenantId: string\n isEnabled: boolean\n username: string\n baseUrl: string\n createdAt: string\n updatedAt: string\n} | null\n\ntype BicFormState = {\n isEnabled: boolean\n username: string\n password: string\n baseUrl: string\n}\n\nconst emptyBicForm: BicFormState = {\n isEnabled: false,\n username: '',\n password: '',\n baseUrl: 'https://api.bic-code.org',\n}\n\n// \u2500\u2500\u2500 BIC Config Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction BicConfigSection() {\n const t = useT()\n const [config, setConfig] = React.useState<BicConfigData>(null)\n const [form, setForm] = React.useState<BicFormState>(emptyBicForm)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [testing, setTesting] = React.useState(false)\n const [testResult, setTestResult] = React.useState<{ success: boolean; message: string } | null>(null)\n\n const fetchConfig = React.useCallback(async () => {\n setLoading(true)\n try {\n const { result } = await apiCallOrThrow<{ config: BicConfigData }>('/api/shipment_tracking/bic-configs')\n setConfig(result?.config ?? null)\n if (result?.config) {\n setForm({\n isEnabled: result.config.isEnabled,\n username: result.config.username,\n password: '', // Password is never returned from API\n baseUrl: result.config.baseUrl,\n })\n } else {\n setForm(emptyBicForm)\n }\n } catch {\n // Config doesn't exist yet, that's fine\n setConfig(null)\n setForm(emptyBicForm)\n } finally {\n setLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchConfig()\n }, [fetchConfig])\n\n const handleTest = React.useCallback(async () => {\n if (!form.username || !form.password) {\n flash(t('shipment_tracking.bic_config.validation.usernameRequired', 'Username is required'), 'error')\n return\n }\n\n setTesting(true)\n setTestResult(null)\n\n try {\n const { result } = await apiCallOrThrow<{ success: boolean; message: string }>(\n '/api/shipment_tracking/bic-configs/test',\n {\n method: 'POST',\n body: JSON.stringify({\n username: form.username,\n password: form.password,\n baseUrl: form.baseUrl,\n }),\n }\n )\n\n setTestResult(result ?? { success: false, message: 'Unknown error' })\n\n if (result?.success) {\n flash(t('shipment_tracking.bic_config.flash.testSuccess', 'BIC API connection successful'), 'success')\n } else {\n flash(result?.message ?? t('shipment_tracking.bic_config.flash.testFailed', 'BIC API connection failed'), 'error')\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Connection test failed'\n setTestResult({ success: false, message })\n flash(message, 'error')\n } finally {\n setTesting(false)\n }\n }, [form, t])\n\n const handleSave = React.useCallback(async () => {\n if (!form.username) {\n flash(t('shipment_tracking.bic_config.validation.usernameRequired', 'Username is required'), 'error')\n return\n }\n\n // Password is required for new config or when enabling\n if (!config && !form.password) {\n flash(t('shipment_tracking.bic_config.validation.passwordRequired', 'Password is required'), 'error')\n return\n }\n\n setSaving(true)\n try {\n const body: Record<string, unknown> = {\n isEnabled: form.isEnabled,\n username: form.username,\n baseUrl: form.baseUrl || 'https://api.bic-code.org',\n }\n\n // Only include password if it was changed\n if (form.password) {\n body.password = form.password\n } else if (config) {\n // If editing and no new password, we still need to send something\n // The API should handle this gracefully\n body.password = '__UNCHANGED__'\n }\n\n await apiCallOrThrow('/api/shipment_tracking/bic-configs', {\n method: 'PUT',\n body: JSON.stringify(body),\n })\n\n flash(t('shipment_tracking.bic_config.flash.saved', 'BIC configuration saved'), 'success')\n fetchConfig()\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to save'\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }, [form, config, t, fetchConfig])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSave()\n }\n },\n [handleSave]\n )\n\n if (loading) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5\" />\n {t('shipment_tracking.bic_config.title', 'BIC Facility API')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n </CardContent>\n </Card>\n )\n }\n\n return (\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5\" />\n <CardTitle>{t('shipment_tracking.bic_config.title', 'BIC Facility API')}</CardTitle>\n </div>\n <div className=\"flex items-center gap-2\">\n {config ? (\n <>\n <Badge variant=\"outline\">\n {t('shipment_tracking.bic_config.status.configured', 'Configured')}\n </Badge>\n <Badge variant={config.isEnabled ? 'default' : 'secondary'}>\n {config.isEnabled\n ? t('shipment_tracking.bic_config.status.enabled', 'Enabled')\n : t('shipment_tracking.bic_config.status.disabled', 'Disabled')}\n </Badge>\n </>\n ) : (\n <Badge variant=\"secondary\">\n {t('shipment_tracking.bic_config.status.notConfigured', 'Not Configured')}\n </Badge>\n )}\n </div>\n </div>\n <CardDescription>\n {t(\n 'shipment_tracking.bic_config.description',\n 'Configure BIC Facility Code API credentials to enrich shipment tracking locations with coordinates, addresses, and terminal operator information.'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent onKeyDown={handleKeyDown}>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"bicEnabled\"\n checked={form.isEnabled}\n onCheckedChange={(checked) =>\n setForm((prev) => ({ ...prev, isEnabled: checked === true }))\n }\n />\n <Label htmlFor=\"bicEnabled\" className=\"cursor-pointer font-medium\">\n {t('shipment_tracking.bic_config.fields.isEnabled', 'Enable BIC Enrichment')}\n </Label>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicUsername\">\n {t('shipment_tracking.bic_config.fields.username', 'Username')}\n </Label>\n <Input\n id=\"bicUsername\"\n value={form.username}\n onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}\n placeholder=\"your-bic-username\"\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicPassword\">\n {t('shipment_tracking.bic_config.fields.password', 'Password')}\n </Label>\n <Input\n id=\"bicPassword\"\n type=\"password\"\n value={form.password}\n onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}\n placeholder={config ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : 'your-bic-password'}\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'shipment_tracking.bic_config.hints.password',\n 'Your password will be encrypted and stored securely.'\n )}\n </p>\n </div>\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicBaseUrl\">\n {t('shipment_tracking.bic_config.fields.baseUrl', 'API Base URL')}\n </Label>\n <Input\n id=\"bicBaseUrl\"\n value={form.baseUrl}\n onChange={(e) => setForm((prev) => ({ ...prev, baseUrl: e.target.value }))}\n placeholder=\"https://api.bic-code.org\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('shipment_tracking.bic_config.hints.baseUrl', 'Default: https://api.bic-code.org')}\n </p>\n </div>\n\n {testResult && (\n <div\n className={`flex items-center gap-2 rounded-md p-3 text-sm ${\n testResult.success\n ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'\n : 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'\n }`}\n >\n {testResult.success ? (\n <CheckCircle2 className=\"h-4 w-4\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n <span>{testResult.message}</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2 pt-2\">\n <Button\n variant=\"outline\"\n onClick={handleTest}\n disabled={testing || !form.username || !form.password}\n >\n {testing && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {t('shipment_tracking.bic_config.actions.testConnection', 'Test Connection')}\n </Button>\n <Button onClick={handleSave} disabled={saving}>\n {saving && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {t('shipment_tracking.bic_config.actions.save', 'Save Configuration')}\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\n// \u2500\u2500\u2500 Main Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default function TrackingAuthConfigPage() {\n const t = useT()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editingId, setEditingId] = useState<string | null>(null)\n const [form, setForm] = useState<FormState>(emptyForm)\n const [submitting, setSubmitting] = useState(false)\n\n const isEdit = editingId !== null\n\n const columns = useMemo<ColumnDef[]>(\n () => [\n {\n data: 'carrierCode',\n title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code'),\n width: 150,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"font-medium\">{String(value ?? '')}</span>\n ),\n },\n {\n data: 'apiEndpoint',\n title: t('shipment_tracking.carrier_configs.fields.apiEndpoint', 'API Endpoint'),\n width: 280,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"text-sm truncate max-w-[260px] block\">{String(value || '-')}</span>\n ),\n },\n {\n data: 'rateLimit',\n title: t('shipment_tracking.carrier_configs.fields.rateLimit', 'Rate Limit'),\n width: 120,\n readOnly: true,\n renderer: (_value: unknown, rowData: Record<string, unknown>) => (\n <span className=\"text-sm\">\n {String(rowData.rateLimitRequests ?? 60)} / {String(rowData.rateLimitWindowSeconds ?? 60)}s\n </span>\n ),\n },\n {\n data: 'isActive',\n title: t('shipment_tracking.carrier_configs.fields.isActive', 'Active'),\n width: 80,\n readOnly: true,\n renderer: (value: unknown) => <BooleanIcon value={value === true || value === 'true'} />,\n },\n ],\n [t],\n )\n\n const openCreateDialog = useCallback(() => {\n setEditingId(null)\n setForm(emptyForm)\n setDialogOpen(true)\n }, [])\n\n const addButton = useMemo(\n () => (\n <ButtonV2 variant=\"primary\" size=\"sm\" icon={<Plus className=\"h-4 w-4\" />} onClick={openCreateDialog}>\n {t('shipment_tracking.carrier_configs.create', 'Add Carrier Config')}\n </ButtonV2>\n ),\n [openCreateDialog, t],\n )\n\n const table = useDynamicTablePage<CarrierConfigRow>({\n source: '/api/shipment_tracking/carrier-configs',\n // Saved views. Without this the perspective tab strip auto-hides\n // (hidePerspectiveTabs ?? !hasPerspectives), so users cannot save or\n // switch a view on this list at all.\n perspectives: 'shipment_tracking_carrier_configs',\n columns,\n tableName: t('shipment_tracking.carrier_configs.title', 'Carrier Configs'),\n defaultPageSize: 20,\n mapApiItem: mapItem,\n cellEdit: false,\n tableProps: {\n height: '100%',\n uiConfig: {\n readOnlyStyle: 'normal',\n hideAddRowButton: true,\n searchBarEnd: addButton,\n },\n },\n })\n\n const openEditDialog = useCallback(\n (id: string) => {\n const items = (table.query.data as { items?: Record<string, unknown>[] })?.items ?? []\n const mapped = items.map(mapItem).filter((x): x is CarrierConfigRow => x !== null)\n const row = mapped.find((r) => r.id === id)\n if (!row) return\n setEditingId(id)\n setForm(rowToForm(row))\n setDialogOpen(true)\n },\n [table.query.data],\n )\n\n const handleSubmit = useCallback(async () => {\n if (submitting) return\n\n if (!isEdit && !form.carrierCode.trim()) {\n flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier code is required'), 'error')\n return\n }\n\n if (form.apiEndpoint.trim()) {\n try {\n new URL(form.apiEndpoint.trim())\n } catch {\n flash(t('shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl', 'API endpoint must be a valid URL'), 'error')\n return\n }\n }\n\n setSubmitting(true)\n try {\n const assembled = valuesToAuthConfig(form.authValues)\n const authConfig = Object.keys(assembled).length ? assembled : undefined\n\n if (isEdit) {\n const body: Record<string, unknown> = { id: editingId }\n if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()\n else body.apiEndpoint = null\n body.authConfig = authConfig ?? null\n body.rateLimitRequests = form.rateLimitRequests\n body.rateLimitWindowSeconds = form.rateLimitWindowSeconds\n body.isActive = form.isActive\n\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'PUT',\n body: JSON.stringify(body),\n })\n flash(t('shipment_tracking.carrier_configs.flash.updated', 'Carrier config updated'), 'success')\n } else {\n const body: Record<string, unknown> = {\n carrierCode: form.carrierCode.trim(),\n rateLimitRequests: form.rateLimitRequests,\n rateLimitWindowSeconds: form.rateLimitWindowSeconds,\n isActive: form.isActive,\n }\n if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()\n if (authConfig) body.authConfig = authConfig\n\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'POST',\n body: JSON.stringify(body),\n })\n flash(t('shipment_tracking.carrier_configs.flash.created', 'Carrier config created'), 'success')\n }\n\n setDialogOpen(false)\n table.refresh()\n } catch {\n flash(isEdit ? t('shipment_tracking.carrier_configs.flash.updateFailed', 'Failed to update carrier config') : t('shipment_tracking.carrier_configs.flash.createFailed', 'Failed to create carrier config'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [form, isEdit, editingId, submitting, table.refresh, t])\n\n const handleDialogKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n const handleDelete = useCallback(\n async (id: string) => {\n if (!confirm(t('shipment_tracking.carrier_configs.confirmDelete', 'Delete this carrier config?'))) return\n\n try {\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'DELETE',\n body: JSON.stringify({ id }),\n })\n flash(t('shipment_tracking.carrier_configs.flash.deleted', 'Carrier config deleted'), 'success')\n table.refresh()\n } catch {\n flash(t('shipment_tracking.carrier_configs.flash.deleteFailed', 'Failed to delete carrier config'), 'error')\n }\n },\n [table.refresh, t],\n )\n\n const rowActions = useCallback(\n (rowData: any): ContextMenuAction[] => {\n if (!rowData?.id) return []\n return [\n { id: 'edit', label: t('common.edit', 'Edit') },\n { id: 'delete', label: t('common.delete', 'Delete') },\n ]\n },\n [t],\n )\n\n const handleRowAction = useCallback(\n (actionId: string, rowData: any) => {\n const id = rowData?.id as string | undefined\n if (!id) return\n if (actionId === 'edit') openEditDialog(id)\n else if (actionId === 'delete') handleDelete(id)\n },\n [openEditDialog, handleDelete],\n )\n\n const tableHeight = useMemo(() => {\n const rowH = 40\n const headerH = 40\n const toolbarH = 50\n const minHeight = 200\n const maxHeight = 400\n const rowCount = (table.query.data as { total?: number })?.total ?? 0\n const contentHeight = toolbarH + headerH + rowCount * rowH + 20\n return Math.min(Math.max(contentHeight, minHeight), maxHeight)\n }, [table.query.data])\n\n if (table.isLoading) {\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <BicConfigSection />\n <ShipsGoConfigSection />\n <Card>\n <CardHeader>\n <CardTitle>{t('shipment_tracking.carrier_configs.title', 'Carrier Configs')}</CardTitle>\n </CardHeader>\n <CardContent>\n <TableSkeleton rows={5} columns={4} />\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n {/* BIC Config Section */}\n <BicConfigSection />\n\n {/* ShipsGo Aggregator Config Section */}\n <ShipsGoConfigSection />\n\n {/* Carrier Configs Section */}\n <Card>\n <CardHeader>\n <CardTitle>{t('shipment_tracking.carrier_configs.title', 'Carrier Configs')}</CardTitle>\n <CardDescription>\n {t(\n 'shipment_tracking.auth_config.description',\n 'Configure authentication for carrier tracking APIs and facility enrichment services.'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div style={{ height: tableHeight }}>\n <DynamicTable\n {...table.props}\n striped\n density=\"md\"\n rowActions={rowActions}\n onRowAction={handleRowAction}\n />\n </div>\n </CardContent>\n </Card>\n\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogContent className=\"sm:max-w-lg\" onKeyDown={handleDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>{isEdit ? t('shipment_tracking.carrier_configs.edit', 'Edit Carrier Config') : t('shipment_tracking.carrier_configs.create', 'Add Carrier Config')}</DialogTitle>\n </DialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"carrierCode\">{t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code')}</Label>\n <Input\n id=\"carrierCode\"\n value={form.carrierCode}\n onChange={(event) => setForm((prev) => ({ ...prev, carrierCode: event.target.value }))}\n placeholder=\"e.g. maersk, msc, cma-cgm\"\n disabled={isEdit}\n autoFocus={!isEdit}\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"apiEndpoint\">{t('shipment_tracking.carrier_configs.fields.apiEndpoint', 'API Endpoint')}</Label>\n <Input\n id=\"apiEndpoint\"\n value={form.apiEndpoint}\n onChange={(event) => setForm((prev) => ({ ...prev, apiEndpoint: event.target.value }))}\n placeholder=\"https://api.example.com/tracking\"\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label>{t('shipment_tracking.carrier_configs.fields.auth', 'Authentication')}</Label>\n {(() => {\n const spec = CARRIER_AUTH_FIELDS[form.carrierCode.trim().toLowerCase()]\n const setValue = (key: string, value: string) =>\n setForm((prev) => ({ ...prev, authValues: { ...prev.authValues, [key]: value } }))\n if (spec) {\n return spec.map((f) => (\n <div key={f.key} className=\"grid gap-1.5\">\n <Label htmlFor={`auth-${f.key}`} className=\"text-xs text-muted-foreground\">{f.label}</Label>\n {f.multiline ? (\n <Textarea\n id={`auth-${f.key}`}\n value={form.authValues[f.key] ?? ''}\n onChange={(event) => setValue(f.key, event.target.value)}\n placeholder={f.placeholder}\n rows={3}\n className=\"font-mono text-xs\"\n />\n ) : (\n <Input\n id={`auth-${f.key}`}\n type={f.secret ? 'password' : 'text'}\n autoComplete=\"off\"\n value={form.authValues[f.key] ?? ''}\n onChange={(event) => setValue(f.key, event.target.value)}\n placeholder={f.placeholder}\n />\n )}\n </div>\n ))\n }\n // Unknown carrier: generic key/value editor (keys renamed on blur to avoid focus loss).\n return (\n <div className=\"grid gap-2\">\n {Object.entries(form.authValues).map(([k, v], idx) => (\n <div key={idx} className=\"flex items-center gap-2\">\n <Input\n className=\"w-1/3 font-mono text-xs\"\n defaultValue={k}\n placeholder=\"key\"\n onBlur={(event) => {\n const nk = event.target.value.trim()\n if (nk === k) return\n setForm((prev) => {\n const next = { ...prev.authValues }\n const val = next[k] ?? ''\n delete next[k]\n if (nk) next[nk] = val\n return { ...prev, authValues: next }\n })\n }}\n />\n <Input\n className=\"flex-1 text-xs\"\n value={v}\n placeholder=\"value\"\n onChange={(event) => setValue(k, event.target.value)}\n />\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n onClick={() =>\n setForm((prev) => {\n const next = { ...prev.authValues }\n delete next[k]\n return { ...prev, authValues: next }\n })\n }\n >\n {t('shipment_tracking.carrier_configs.auth.remove', 'Remove')}\n </Button>\n </div>\n ))}\n <Button\n type=\"button\"\n variant=\"outline\"\n size=\"sm\"\n className=\"justify-self-start\"\n onClick={() =>\n setForm((prev) => ({\n ...prev,\n authValues: { ...prev.authValues, [`field_${Object.keys(prev.authValues).length + 1}`]: '' },\n }))\n }\n >\n <Plus className=\"mr-1 h-3.5 w-3.5\" />\n {t('shipment_tracking.carrier_configs.auth.addField', 'Add field')}\n </Button>\n </div>\n )\n })()}\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"rateLimitRequests\">{t('shipment_tracking.carrier_configs.fields.rateLimitRequests', 'Rate Limit (requests)')}</Label>\n <Input\n id=\"rateLimitRequests\"\n type=\"number\"\n min={1}\n max={10000}\n value={form.rateLimitRequests}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, rateLimitRequests: Number(event.target.value) || 60 }))\n }\n />\n </div>\n <div className=\"grid gap-2\">\n <Label htmlFor=\"rateLimitWindowSeconds\">{t('shipment_tracking.carrier_configs.fields.rateLimitWindowSeconds', 'Window (seconds)')}</Label>\n <Input\n id=\"rateLimitWindowSeconds\"\n type=\"number\"\n min={1}\n max={86400}\n value={form.rateLimitWindowSeconds}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, rateLimitWindowSeconds: Number(event.target.value) || 60 }))\n }\n />\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"isActive\"\n checked={form.isActive}\n onCheckedChange={(checked) =>\n setForm((prev) => ({ ...prev, isActive: checked === true }))\n }\n />\n <Label htmlFor=\"isActive\" className=\"cursor-pointer\">\n {t('shipment_tracking.carrier_configs.fields.isActive', 'Active')}\n </Label>\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDialogOpen(false)} disabled={submitting}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={handleSubmit} disabled={submitting}>\n {submitting ? t('common.saving', 'Saving...') : isEdit ? t('common.save', 'Save') : t('common.create', 'Create')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freighttech/shipment-tracking",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -63,7 +63,8 @@ function mapItem(item: Record<string, unknown>): CarrierConfigRow | null {
63
63
  type FormState = {
64
64
  carrierCode: string
65
65
  apiEndpoint: string
66
- authConfig: string
66
+ /** authConfig as flat key→value; assembled into the authConfig object on save. */
67
+ authValues: Record<string, string>
67
68
  rateLimitRequests: number
68
69
  rateLimitWindowSeconds: number
69
70
  isActive: boolean
@@ -72,23 +73,83 @@ type FormState = {
72
73
  const emptyForm: FormState = {
73
74
  carrierCode: '',
74
75
  apiEndpoint: '',
75
- authConfig: '',
76
+ authValues: {},
76
77
  rateLimitRequests: 60,
77
78
  rateLimitWindowSeconds: 60,
78
79
  isActive: true,
79
80
  }
80
81
 
82
+ /** Flatten a stored authConfig object into editable string values. */
83
+ function authConfigToValues(authConfig: Record<string, unknown> | null): Record<string, string> {
84
+ if (!authConfig) return {}
85
+ return Object.fromEntries(
86
+ Object.entries(authConfig).map(([k, v]) => [k, typeof v === 'string' ? v : JSON.stringify(v)]),
87
+ )
88
+ }
89
+
81
90
  function rowToForm(row: CarrierConfigRow): FormState {
82
91
  return {
83
92
  carrierCode: row.carrierCode,
84
93
  apiEndpoint: row.apiEndpoint ?? '',
85
- authConfig: row.authConfig ? JSON.stringify(row.authConfig, null, 2) : '',
94
+ authValues: authConfigToValues(row.authConfig),
86
95
  rateLimitRequests: row.rateLimitRequests,
87
96
  rateLimitWindowSeconds: row.rateLimitWindowSeconds,
88
97
  isActive: row.isActive,
89
98
  }
90
99
  }
91
100
 
101
+ // ─── Per-carrier auth field specs ────────────────────────────
102
+ // Labeled inputs for the carriers whose authConfig shape we know (mirrors the
103
+ // keys each adapter reads). Carriers not listed fall back to a generic
104
+ // key/value editor. `secret` masks the input; `multiline` uses a textarea.
105
+
106
+ type CarrierAuthField = { key: string; label: string; secret?: boolean; multiline?: boolean; placeholder?: string }
107
+
108
+ const CARRIER_AUTH_FIELDS: Record<string, CarrierAuthField[]> = {
109
+ msc: [
110
+ { key: 'client_id', label: 'Client ID' },
111
+ { key: 'scope', label: 'Scope' },
112
+ { key: 'token_url', label: 'Token URL' },
113
+ { key: 'aud', label: 'Audience', placeholder: 'defaults to Token URL if blank' },
114
+ { key: 'certificate_base64', label: 'Certificate (PFX, base64)', secret: true, multiline: true },
115
+ ],
116
+ maersk: [
117
+ { key: 'client_id', label: 'Client ID' },
118
+ { key: 'client_secret', label: 'Client Secret', secret: true },
119
+ ],
120
+ 'hapag-lloyd': [
121
+ { key: 'client_id', label: 'Client ID' },
122
+ { key: 'client_secret', label: 'Client Secret', secret: true },
123
+ ],
124
+ evergreen: [
125
+ { key: 'client_id', label: 'Client ID' },
126
+ { key: 'client_secret', label: 'Client Secret', secret: true },
127
+ { key: 'token_url', label: 'Token URL' },
128
+ ],
129
+ zim: [
130
+ { key: 'client_id', label: 'Client ID' },
131
+ { key: 'client_secret', label: 'Client Secret', secret: true },
132
+ { key: 'subscription_key', label: 'Subscription Key', secret: true },
133
+ ],
134
+ cosco: [
135
+ { key: 'app_key', label: 'App Key', secret: true },
136
+ { key: 'customer_id', label: 'Customer ID' },
137
+ { key: 'scac_code', label: 'SCAC Code' },
138
+ ],
139
+ 'cma-cgm': [
140
+ { key: 'api_key', label: 'API Key', secret: true },
141
+ ],
142
+ }
143
+
144
+ /** Assemble the authConfig object from form values, dropping blank entries. */
145
+ function valuesToAuthConfig(values: Record<string, string>): Record<string, string> {
146
+ return Object.fromEntries(
147
+ Object.entries(values)
148
+ .map(([k, v]) => [k.trim(), v] as const)
149
+ .filter(([k, v]) => k !== '' && String(v).trim() !== ''),
150
+ )
151
+ }
152
+
92
153
  // ─── BIC Config Types ────────────────────────────────────────
93
154
 
94
155
  type BicConfigData = {
@@ -508,15 +569,6 @@ export default function TrackingAuthConfigPage() {
508
569
  return
509
570
  }
510
571
 
511
- if (form.authConfig.trim()) {
512
- try {
513
- JSON.parse(form.authConfig)
514
- } catch {
515
- flash(t('shipment_tracking.carrier_configs.validation.authConfigInvalidJson', 'Auth config must be valid JSON'), 'error')
516
- return
517
- }
518
- }
519
-
520
572
  if (form.apiEndpoint.trim()) {
521
573
  try {
522
574
  new URL(form.apiEndpoint.trim())
@@ -528,7 +580,8 @@ export default function TrackingAuthConfigPage() {
528
580
 
529
581
  setSubmitting(true)
530
582
  try {
531
- const authConfig = form.authConfig.trim() ? JSON.parse(form.authConfig) : undefined
583
+ const assembled = valuesToAuthConfig(form.authValues)
584
+ const authConfig = Object.keys(assembled).length ? assembled : undefined
532
585
 
533
586
  if (isEdit) {
534
587
  const body: Record<string, unknown> = { id: editingId }
@@ -712,15 +765,98 @@ export default function TrackingAuthConfigPage() {
712
765
  </div>
713
766
 
714
767
  <div className="grid gap-2">
715
- <Label htmlFor="authConfig">{t('shipment_tracking.carrier_configs.fields.authConfig', 'Auth Config (JSON)')}</Label>
716
- <Textarea
717
- id="authConfig"
718
- value={form.authConfig}
719
- onChange={(event) => setForm((prev) => ({ ...prev, authConfig: event.target.value }))}
720
- placeholder='{"apiKey": "..."}'
721
- rows={3}
722
- className="font-mono text-xs"
723
- />
768
+ <Label>{t('shipment_tracking.carrier_configs.fields.auth', 'Authentication')}</Label>
769
+ {(() => {
770
+ const spec = CARRIER_AUTH_FIELDS[form.carrierCode.trim().toLowerCase()]
771
+ const setValue = (key: string, value: string) =>
772
+ setForm((prev) => ({ ...prev, authValues: { ...prev.authValues, [key]: value } }))
773
+ if (spec) {
774
+ return spec.map((f) => (
775
+ <div key={f.key} className="grid gap-1.5">
776
+ <Label htmlFor={`auth-${f.key}`} className="text-xs text-muted-foreground">{f.label}</Label>
777
+ {f.multiline ? (
778
+ <Textarea
779
+ id={`auth-${f.key}`}
780
+ value={form.authValues[f.key] ?? ''}
781
+ onChange={(event) => setValue(f.key, event.target.value)}
782
+ placeholder={f.placeholder}
783
+ rows={3}
784
+ className="font-mono text-xs"
785
+ />
786
+ ) : (
787
+ <Input
788
+ id={`auth-${f.key}`}
789
+ type={f.secret ? 'password' : 'text'}
790
+ autoComplete="off"
791
+ value={form.authValues[f.key] ?? ''}
792
+ onChange={(event) => setValue(f.key, event.target.value)}
793
+ placeholder={f.placeholder}
794
+ />
795
+ )}
796
+ </div>
797
+ ))
798
+ }
799
+ // Unknown carrier: generic key/value editor (keys renamed on blur to avoid focus loss).
800
+ return (
801
+ <div className="grid gap-2">
802
+ {Object.entries(form.authValues).map(([k, v], idx) => (
803
+ <div key={idx} className="flex items-center gap-2">
804
+ <Input
805
+ className="w-1/3 font-mono text-xs"
806
+ defaultValue={k}
807
+ placeholder="key"
808
+ onBlur={(event) => {
809
+ const nk = event.target.value.trim()
810
+ if (nk === k) return
811
+ setForm((prev) => {
812
+ const next = { ...prev.authValues }
813
+ const val = next[k] ?? ''
814
+ delete next[k]
815
+ if (nk) next[nk] = val
816
+ return { ...prev, authValues: next }
817
+ })
818
+ }}
819
+ />
820
+ <Input
821
+ className="flex-1 text-xs"
822
+ value={v}
823
+ placeholder="value"
824
+ onChange={(event) => setValue(k, event.target.value)}
825
+ />
826
+ <Button
827
+ type="button"
828
+ variant="outline"
829
+ size="sm"
830
+ onClick={() =>
831
+ setForm((prev) => {
832
+ const next = { ...prev.authValues }
833
+ delete next[k]
834
+ return { ...prev, authValues: next }
835
+ })
836
+ }
837
+ >
838
+ {t('shipment_tracking.carrier_configs.auth.remove', 'Remove')}
839
+ </Button>
840
+ </div>
841
+ ))}
842
+ <Button
843
+ type="button"
844
+ variant="outline"
845
+ size="sm"
846
+ className="justify-self-start"
847
+ onClick={() =>
848
+ setForm((prev) => ({
849
+ ...prev,
850
+ authValues: { ...prev.authValues, [`field_${Object.keys(prev.authValues).length + 1}`]: '' },
851
+ }))
852
+ }
853
+ >
854
+ <Plus className="mr-1 h-3.5 w-3.5" />
855
+ {t('shipment_tracking.carrier_configs.auth.addField', 'Add field')}
856
+ </Button>
857
+ </div>
858
+ )
859
+ })()}
724
860
  </div>
725
861
 
726
862
  <div className="grid grid-cols-2 gap-4">