@freighttech/shipment-tracking 0.7.0 → 0.7.1

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.
@@ -24,11 +24,79 @@ import { Button as ButtonV2 } from "@freighttech/ui/primitives-v2";
24
24
  import { Input } from "@freighttech/ui/primitives/input";
25
25
  import { Label } from "@freighttech/ui/primitives/label";
26
26
  import { Checkbox } from "@freighttech/ui/primitives/checkbox";
27
- import { Textarea } from "@freighttech/ui/primitives/textarea";
28
27
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@freighttech/ui/primitives/card";
29
28
  import { Badge } from "@freighttech/ui/primitives/badge";
30
29
  import { Loader2, CheckCircle2, XCircle, Shield, Plus } from "lucide-react";
31
30
  import { ShipsGoConfigSection } from "../../components/ShipsGoConfigSection.js";
31
+ const CLIENT_ID = { key: "client_id", label: "Client ID" };
32
+ const CLIENT_SECRET = { key: "client_secret", label: "Client secret", secret: true };
33
+ const CARRIER_SPECS = [
34
+ {
35
+ value: "maersk",
36
+ label: "Maersk",
37
+ defaultEndpoint: "https://api.maersk.com/track-and-trace-private/events",
38
+ credentials: [CLIENT_ID, CLIENT_SECRET]
39
+ },
40
+ {
41
+ value: "msc",
42
+ label: "MSC",
43
+ defaultEndpoint: "https://api.tech.msc.com/msc/trackandtrace/v2.2/events",
44
+ // The MSC OAuth account identity (client_id/scope/token_url/aud) is supplied
45
+ // per config, not baked into the adapter — so the form collects it here
46
+ // alongside the certificate. `aud` defaults to `token_url` when left blank.
47
+ credentials: [
48
+ CLIENT_ID,
49
+ { key: "scope", label: "Scope" },
50
+ { key: "token_url", label: "Token URL" },
51
+ { key: "aud", label: "Audience", optional: true, placeholder: "defaults to Token URL" },
52
+ { key: "certificate_base64", label: "Certificate (base64 PFX)", secret: true }
53
+ ]
54
+ },
55
+ {
56
+ value: "cma-cgm",
57
+ label: "CMA CGM",
58
+ defaultEndpoint: "https://apis.cma-cgm.net/operation/trackandtrace/v1/events",
59
+ credentials: [{ key: "api_key", label: "API key", secret: true }]
60
+ },
61
+ {
62
+ value: "hapag-lloyd",
63
+ label: "Hapag-Lloyd",
64
+ defaultEndpoint: "https://api.hlag.com/hlag/external/v2/events",
65
+ credentials: [CLIENT_ID, CLIENT_SECRET]
66
+ },
67
+ {
68
+ value: "zim",
69
+ label: "ZIM",
70
+ defaultEndpoint: "https://apigw.zim.com/trackAndTrace/v1",
71
+ credentials: [CLIENT_ID, CLIENT_SECRET, { key: "subscription_key", label: "Subscription key", secret: true }]
72
+ },
73
+ {
74
+ value: "cosco",
75
+ label: "COSCO",
76
+ defaultEndpoint: "https://apis.cargosmart.com/openapi/cs2/ctvc/COSU",
77
+ credentials: [
78
+ { key: "app_key", label: "App key", secret: true },
79
+ { key: "customer_id", label: "Customer ID" },
80
+ { key: "scac_code", label: "SCAC code", optional: true, placeholder: "COSU" }
81
+ ]
82
+ },
83
+ {
84
+ value: "evergreen",
85
+ label: "Evergreen",
86
+ defaultEndpoint: "",
87
+ endpointRequired: true,
88
+ credentials: [CLIENT_ID, CLIENT_SECRET, { key: "token_url", label: "Token URL" }]
89
+ }
90
+ ];
91
+ function specFor(carrierCode) {
92
+ return CARRIER_SPECS.find((c) => c.value === carrierCode);
93
+ }
94
+ function credentialFieldsFor(carrierCode, authConfig) {
95
+ const spec = specFor(carrierCode);
96
+ if (spec) return spec.credentials;
97
+ const keys = authConfig ? Object.keys(authConfig) : [];
98
+ return keys.map((k) => ({ key: k, label: k, secret: /secret|password|key|token|cert/i.test(k) }));
99
+ }
32
100
  function mapItem(item) {
33
101
  const id = typeof item.id === "string" ? item.id : null;
34
102
  if (!id) return null;
@@ -43,70 +111,31 @@ function mapItem(item) {
43
111
  createdAt: item.createdAt ?? null
44
112
  };
45
113
  }
46
- const emptyForm = {
47
- carrierCode: "",
48
- apiEndpoint: "",
49
- authValues: {},
50
- rateLimitRequests: 60,
51
- rateLimitWindowSeconds: 60,
52
- isActive: true
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
- );
114
+ function formForCarrier(carrierCode) {
115
+ const spec = specFor(carrierCode);
116
+ return {
117
+ carrierCode,
118
+ apiEndpoint: spec?.defaultEndpoint ?? "",
119
+ credentials: {},
120
+ rateLimitRequests: 60,
121
+ rateLimitWindowSeconds: 60,
122
+ isActive: true
123
+ };
59
124
  }
125
+ const emptyForm = formForCarrier(CARRIER_SPECS[0].value);
60
126
  function rowToForm(row) {
127
+ const credentials = {};
128
+ const auth = row.authConfig ?? {};
129
+ for (const [k, v] of Object.entries(auth)) credentials[k] = v == null ? "" : String(v);
61
130
  return {
62
131
  carrierCode: row.carrierCode,
63
- apiEndpoint: row.apiEndpoint ?? "",
64
- authValues: authConfigToValues(row.authConfig),
132
+ apiEndpoint: row.apiEndpoint ?? specFor(row.carrierCode)?.defaultEndpoint ?? "",
133
+ credentials,
65
134
  rateLimitRequests: row.rateLimitRequests,
66
135
  rateLimitWindowSeconds: row.rateLimitWindowSeconds,
67
136
  isActive: row.isActive
68
137
  };
69
138
  }
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
- }
110
139
  const emptyBicForm = {
111
140
  isEnabled: false,
112
141
  username: "",
@@ -343,14 +372,25 @@ function TrackingAuthConfigPage() {
343
372
  const [form, setForm] = useState(emptyForm);
344
373
  const [submitting, setSubmitting] = useState(false);
345
374
  const isEdit = editingId !== null;
375
+ const credFields = credentialFieldsFor(form.carrierCode, form.credentials);
376
+ const spec = specFor(form.carrierCode);
377
+ const setField = useCallback(
378
+ (key, value) => setForm((p) => ({ ...p, [key]: value })),
379
+ []
380
+ );
381
+ const setCredential = useCallback(
382
+ (key, value) => setForm((p) => ({ ...p, credentials: { ...p.credentials, [key]: value } })),
383
+ []
384
+ );
385
+ const changeCarrier = useCallback((carrierCode) => setForm(formForCarrier(carrierCode)), []);
346
386
  const columns = useMemo(
347
387
  () => [
348
388
  {
349
389
  data: "carrierCode",
350
- title: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier Code"),
390
+ title: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier"),
351
391
  width: 150,
352
392
  readOnly: true,
353
- renderer: (value) => /* @__PURE__ */ jsx("span", { className: "font-medium", children: String(value ?? "") })
393
+ renderer: (value) => /* @__PURE__ */ jsx("span", { className: "font-medium", children: specFor(String(value))?.label ?? String(value ?? "") })
354
394
  },
355
395
  {
356
396
  data: "apiEndpoint",
@@ -425,26 +465,44 @@ function TrackingAuthConfigPage() {
425
465
  const handleSubmit = useCallback(async () => {
426
466
  if (submitting) return;
427
467
  if (!isEdit && !form.carrierCode.trim()) {
428
- flash(t("shipment_tracking.carrier_configs.validation.carrierCodeRequired", "Carrier code is required"), "error");
468
+ flash(t("shipment_tracking.carrier_configs.validation.carrierCodeRequired", "Carrier is required"), "error");
469
+ return;
470
+ }
471
+ const currentSpec = specFor(form.carrierCode);
472
+ const endpoint = form.apiEndpoint.trim();
473
+ if (currentSpec?.endpointRequired && !endpoint) {
474
+ flash(t("shipment_tracking.carrier_configs.validation.endpointRequired", "API endpoint is required for this carrier"), "error");
429
475
  return;
430
476
  }
431
- if (form.apiEndpoint.trim()) {
477
+ if (endpoint) {
432
478
  try {
433
- new URL(form.apiEndpoint.trim());
479
+ new URL(endpoint);
434
480
  } catch {
435
481
  flash(t("shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl", "API endpoint must be a valid URL"), "error");
436
482
  return;
437
483
  }
438
484
  }
485
+ const fields = credentialFieldsFor(form.carrierCode, form.credentials);
486
+ const authConfig = {};
487
+ for (const f of fields) {
488
+ const val = (form.credentials[f.key] ?? "").trim();
489
+ if (!val) {
490
+ if (f.optional) continue;
491
+ flash(
492
+ t("shipment_tracking.carrier_configs.validation.credentialsRequired", "Fill in all credential fields"),
493
+ "error"
494
+ );
495
+ return;
496
+ }
497
+ authConfig[f.key] = val;
498
+ }
439
499
  setSubmitting(true);
440
500
  try {
441
- const assembled = valuesToAuthConfig(form.authValues);
442
- const authConfig = Object.keys(assembled).length ? assembled : void 0;
501
+ const hasAuth = Object.keys(authConfig).length > 0;
443
502
  if (isEdit) {
444
503
  const body = { id: editingId };
445
- if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim();
446
- else body.apiEndpoint = null;
447
- body.authConfig = authConfig ?? null;
504
+ body.apiEndpoint = endpoint || null;
505
+ body.authConfig = hasAuth ? authConfig : null;
448
506
  body.rateLimitRequests = form.rateLimitRequests;
449
507
  body.rateLimitWindowSeconds = form.rateLimitWindowSeconds;
450
508
  body.isActive = form.isActive;
@@ -460,8 +518,8 @@ function TrackingAuthConfigPage() {
460
518
  rateLimitWindowSeconds: form.rateLimitWindowSeconds,
461
519
  isActive: form.isActive
462
520
  };
463
- if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim();
464
- if (authConfig) body.authConfig = authConfig;
521
+ if (endpoint) body.apiEndpoint = endpoint;
522
+ if (hasAuth) body.authConfig = authConfig;
465
523
  await apiCallOrThrow("/api/shipment_tracking/carrier-configs", {
466
524
  method: "POST",
467
525
  body: JSON.stringify(body)
@@ -566,16 +624,15 @@ function TrackingAuthConfigPage() {
566
624
  /* @__PURE__ */ jsx(DialogHeader, { children: /* @__PURE__ */ jsx(DialogTitle, { children: isEdit ? t("shipment_tracking.carrier_configs.edit", "Edit Carrier Config") : t("shipment_tracking.carrier_configs.create", "Add Carrier Config") }) }),
567
625
  /* @__PURE__ */ jsxs("div", { className: "grid gap-4 py-2", children: [
568
626
  /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
569
- /* @__PURE__ */ jsx(Label, { htmlFor: "carrierCode", children: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier Code") }),
570
- /* @__PURE__ */ jsx(
571
- Input,
627
+ /* @__PURE__ */ jsx(Label, { htmlFor: "carrierCode", children: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier") }),
628
+ isEdit ? /* @__PURE__ */ jsx(Input, { id: "carrierCode", value: spec?.label ?? form.carrierCode, disabled: true }) : /* @__PURE__ */ jsx(
629
+ "select",
572
630
  {
573
631
  id: "carrierCode",
632
+ className: "border rounded h-9 px-2 text-sm",
574
633
  value: form.carrierCode,
575
- onChange: (event) => setForm((prev) => ({ ...prev, carrierCode: event.target.value })),
576
- placeholder: "e.g. maersk, msc, cma-cgm",
577
- disabled: isEdit,
578
- autoFocus: !isEdit
634
+ onChange: (event) => changeCarrier(event.target.value),
635
+ children: CARRIER_SPECS.map((c) => /* @__PURE__ */ jsx("option", { value: c.value, children: c.label }, c.value))
579
636
  }
580
637
  )
581
638
  ] }),
@@ -586,107 +643,29 @@ function TrackingAuthConfigPage() {
586
643
  {
587
644
  id: "apiEndpoint",
588
645
  value: form.apiEndpoint,
589
- onChange: (event) => setForm((prev) => ({ ...prev, apiEndpoint: event.target.value })),
646
+ onChange: (event) => setField("apiEndpoint", event.target.value),
590
647
  placeholder: "https://api.example.com/tracking"
591
648
  }
592
- )
649
+ ),
650
+ spec?.endpointRequired && /* @__PURE__ */ jsx("p", { className: "text-xs text-muted-foreground", children: t("shipment_tracking.carrier_configs.hints.endpointRequired", "This carrier requires an explicit API endpoint.") })
593
651
  ] }),
594
- /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
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));
652
+ credFields.map((f) => /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
653
+ /* @__PURE__ */ jsxs(Label, { htmlFor: `cred-${f.key}`, children: [
654
+ t(`shipment_tracking.carrier_configs.cred.${f.key}`, f.label),
655
+ f.optional ? ` (${t("common.optional", "optional")})` : ""
656
+ ] }),
657
+ /* @__PURE__ */ jsx(
658
+ Input,
659
+ {
660
+ id: `cred-${f.key}`,
661
+ type: f.secret ? "password" : "text",
662
+ autoComplete: "off",
663
+ value: form.credentials[f.key] ?? "",
664
+ onChange: (event) => setCredential(f.key, event.target.value),
665
+ placeholder: f.placeholder
624
666
  }
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
- })()
689
- ] }),
667
+ )
668
+ ] }, f.key)),
690
669
  /* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
691
670
  /* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
692
671
  /* @__PURE__ */ jsx(Label, { htmlFor: "rateLimitRequests", children: t("shipment_tracking.carrier_configs.fields.rateLimitRequests", "Rate Limit (requests)") }),
@@ -698,7 +677,7 @@ function TrackingAuthConfigPage() {
698
677
  min: 1,
699
678
  max: 1e4,
700
679
  value: form.rateLimitRequests,
701
- onChange: (event) => setForm((prev) => ({ ...prev, rateLimitRequests: Number(event.target.value) || 60 }))
680
+ onChange: (event) => setField("rateLimitRequests", Number(event.target.value) || 60)
702
681
  }
703
682
  )
704
683
  ] }),
@@ -712,7 +691,7 @@ function TrackingAuthConfigPage() {
712
691
  min: 1,
713
692
  max: 86400,
714
693
  value: form.rateLimitWindowSeconds,
715
- onChange: (event) => setForm((prev) => ({ ...prev, rateLimitWindowSeconds: Number(event.target.value) || 60 }))
694
+ onChange: (event) => setField("rateLimitWindowSeconds", Number(event.target.value) || 60)
716
695
  }
717
696
  )
718
697
  ] })
@@ -723,7 +702,7 @@ function TrackingAuthConfigPage() {
723
702
  {
724
703
  id: "isActive",
725
704
  checked: form.isActive,
726
- onCheckedChange: (checked) => setForm((prev) => ({ ...prev, isActive: checked === true }))
705
+ onCheckedChange: (checked) => setField("isActive", checked === true)
727
706
  }
728
707
  ),
729
708
  /* @__PURE__ */ jsx(Label, { htmlFor: "isActive", className: "cursor-pointer", children: t("shipment_tracking.carrier_configs.fields.isActive", "Active") })
@@ -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 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": ";AA0TU,SAsBI,UArBF,KADF;AAxTV,YAAY,WAAW;AACvB,SAAS,aAAa,SAAS,gBAAgB;AAC/C,SAAS,MAAM,gBAAgB;AAC/B,SAAS,mBAAmB;AAC5B;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,OACK;AAEP,SAAS,sBAAsB;AAC/B,SAAS,aAAa;AACtB,SAAS,YAAY;AACrB;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,cAAc;AACvB,SAAS,UAAU,gBAAgB;AACnC,SAAS,aAAa;AACtB,SAAS,aAAa;AACtB,SAAS,gBAAgB;AACzB,SAAS,gBAAgB;AACzB,SAAS,MAAM,aAAa,iBAAiB,YAAY,iBAAiB;AAC1E,SAAS,aAAa;AACtB,SAAS,SAAS,cAAc,SAAS,QAAQ,YAAY;AAC7D,SAAS,4BAA4B;AAerC,SAAS,QAAQ,MAAwD;AACvE,QAAM,KAAK,OAAO,KAAK,OAAO,WAAW,KAAK,KAAK;AACnD,MAAI,CAAC,GAAI,QAAO;AAEhB,SAAO;AAAA,IACL;AAAA,IACA,aAAc,KAAK,eAA0B;AAAA,IAC7C,aAAc,KAAK,eAA0B;AAAA,IAC7C,YAAa,KAAK,cAA0C;AAAA,IAC5D,mBAAmB,OAAO,KAAK,sBAAsB,WAAW,KAAK,oBAAoB;AAAA,IACzF,wBAAwB,OAAO,KAAK,2BAA2B,WAAW,KAAK,yBAAyB;AAAA,IACxG,UAAU,KAAK,aAAa;AAAA,IAC5B,WAAY,KAAK,aAAwB;AAAA,EAC3C;AACF;AAYA,MAAM,YAAuB;AAAA,EAC3B,aAAa;AAAA,EACb,aAAa;AAAA,EACb,YAAY,CAAC;AAAA,EACb,mBAAmB;AAAA,EACnB,wBAAwB;AAAA,EACxB,UAAU;AACZ;AAGA,SAAS,mBAAmB,YAAoE;AAC9F,MAAI,CAAC,WAAY,QAAO,CAAC;AACzB,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,UAAU,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,OAAO,MAAM,WAAW,IAAI,KAAK,UAAU,CAAC,CAAC,CAAC;AAAA,EAC/F;AACF;AAEA,SAAS,UAAU,KAAkC;AACnD,SAAO;AAAA,IACL,aAAa,IAAI;AAAA,IACjB,aAAa,IAAI,eAAe;AAAA,IAChC,YAAY,mBAAmB,IAAI,UAAU;AAAA,IAC7C,mBAAmB,IAAI;AAAA,IACvB,wBAAwB,IAAI;AAAA,IAC5B,UAAU,IAAI;AAAA,EAChB;AACF;AASA,MAAM,sBAA0D;AAAA,EAC9D,KAAK;AAAA,IACH,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,IACvC,EAAE,KAAK,SAAS,OAAO,QAAQ;AAAA,IAC/B,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,IACvC,EAAE,KAAK,OAAO,OAAO,YAAY,aAAa,iCAAiC;AAAA,IAC/E,EAAE,KAAK,sBAAsB,OAAO,6BAA6B,QAAQ,MAAM,WAAW,KAAK;AAAA,EACjG;AAAA,EACA,QAAQ;AAAA,IACN,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,IACvC,EAAE,KAAK,iBAAiB,OAAO,iBAAiB,QAAQ,KAAK;AAAA,EAC/D;AAAA,EACA,eAAe;AAAA,IACb,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,IACvC,EAAE,KAAK,iBAAiB,OAAO,iBAAiB,QAAQ,KAAK;AAAA,EAC/D;AAAA,EACA,WAAW;AAAA,IACT,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,IACvC,EAAE,KAAK,iBAAiB,OAAO,iBAAiB,QAAQ,KAAK;AAAA,IAC7D,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,EACzC;AAAA,EACA,KAAK;AAAA,IACH,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,IACvC,EAAE,KAAK,iBAAiB,OAAO,iBAAiB,QAAQ,KAAK;AAAA,IAC7D,EAAE,KAAK,oBAAoB,OAAO,oBAAoB,QAAQ,KAAK;AAAA,EACrE;AAAA,EACA,OAAO;AAAA,IACL,EAAE,KAAK,WAAW,OAAO,WAAW,QAAQ,KAAK;AAAA,IACjD,EAAE,KAAK,eAAe,OAAO,cAAc;AAAA,IAC3C,EAAE,KAAK,aAAa,OAAO,YAAY;AAAA,EACzC;AAAA,EACA,WAAW;AAAA,IACT,EAAE,KAAK,WAAW,OAAO,WAAW,QAAQ,KAAK;AAAA,EACnD;AACF;AAGA,SAAS,mBAAmB,QAAwD;AAClF,SAAO,OAAO;AAAA,IACZ,OAAO,QAAQ,MAAM,EAClB,IAAI,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC,CAAU,EACtC,OAAO,CAAC,CAAC,GAAG,CAAC,MAAM,MAAM,MAAM,OAAO,CAAC,EAAE,KAAK,MAAM,EAAE;AAAA,EAC3D;AACF;AAsBA,MAAM,eAA6B;AAAA,EACjC,WAAW;AAAA,EACX,UAAU;AAAA,EACV,UAAU;AAAA,EACV,SAAS;AACX;AAIA,SAAS,mBAAmB;AAC1B,QAAM,IAAI,KAAK;AACf,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAwB,IAAI;AAC9D,QAAM,CAAC,MAAM,OAAO,IAAI,MAAM,SAAuB,YAAY;AACjE,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,IAAI;AACjD,QAAM,CAAC,QAAQ,SAAS,IAAI,MAAM,SAAS,KAAK;AAChD,QAAM,CAAC,SAAS,UAAU,IAAI,MAAM,SAAS,KAAK;AAClD,QAAM,CAAC,YAAY,aAAa,IAAI,MAAM,SAAuD,IAAI;AAErG,QAAM,cAAc,MAAM,YAAY,YAAY;AAChD,eAAW,IAAI;AACf,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM,eAA0C,oCAAoC;AACvG,gBAAU,QAAQ,UAAU,IAAI;AAChC,UAAI,QAAQ,QAAQ;AAClB,gBAAQ;AAAA,UACN,WAAW,OAAO,OAAO;AAAA,UACzB,UAAU,OAAO,OAAO;AAAA,UACxB,UAAU;AAAA;AAAA,UACV,SAAS,OAAO,OAAO;AAAA,QACzB,CAAC;AAAA,MACH,OAAO;AACL,gBAAQ,YAAY;AAAA,MACtB;AAAA,IACF,QAAQ;AAEN,gBAAU,IAAI;AACd,cAAQ,YAAY;AAAA,IACtB,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,CAAC;AAEL,QAAM,UAAU,MAAM;AACpB,gBAAY;AAAA,EACd,GAAG,CAAC,WAAW,CAAC;AAEhB,QAAM,aAAa,MAAM,YAAY,YAAY;AAC/C,QAAI,CAAC,KAAK,YAAY,CAAC,KAAK,UAAU;AACpC,YAAM,EAAE,4DAA4D,sBAAsB,GAAG,OAAO;AACpG;AAAA,IACF;AAEA,eAAW,IAAI;AACf,kBAAc,IAAI;AAElB,QAAI;AACF,YAAM,EAAE,OAAO,IAAI,MAAM;AAAA,QACvB;AAAA,QACA;AAAA,UACE,QAAQ;AAAA,UACR,MAAM,KAAK,UAAU;AAAA,YACnB,UAAU,KAAK;AAAA,YACf,UAAU,KAAK;AAAA,YACf,SAAS,KAAK;AAAA,UAChB,CAAC;AAAA,QACH;AAAA,MACF;AAEA,oBAAc,UAAU,EAAE,SAAS,OAAO,SAAS,gBAAgB,CAAC;AAEpE,UAAI,QAAQ,SAAS;AACnB,cAAM,EAAE,kDAAkD,+BAA+B,GAAG,SAAS;AAAA,MACvG,OAAO;AACL,cAAM,QAAQ,WAAW,EAAE,iDAAiD,2BAA2B,GAAG,OAAO;AAAA,MACnH;AAAA,IACF,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,oBAAc,EAAE,SAAS,OAAO,QAAQ,CAAC;AACzC,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,iBAAW,KAAK;AAAA,IAClB;AAAA,EACF,GAAG,CAAC,MAAM,CAAC,CAAC;AAEZ,QAAM,aAAa,MAAM,YAAY,YAAY;AAC/C,QAAI,CAAC,KAAK,UAAU;AAClB,YAAM,EAAE,4DAA4D,sBAAsB,GAAG,OAAO;AACpG;AAAA,IACF;AAGA,QAAI,CAAC,UAAU,CAAC,KAAK,UAAU;AAC7B,YAAM,EAAE,4DAA4D,sBAAsB,GAAG,OAAO;AACpG;AAAA,IACF;AAEA,cAAU,IAAI;AACd,QAAI;AACF,YAAM,OAAgC;AAAA,QACpC,WAAW,KAAK;AAAA,QAChB,UAAU,KAAK;AAAA,QACf,SAAS,KAAK,WAAW;AAAA,MAC3B;AAGA,UAAI,KAAK,UAAU;AACjB,aAAK,WAAW,KAAK;AAAA,MACvB,WAAW,QAAQ;AAGjB,aAAK,WAAW;AAAA,MAClB;AAEA,YAAM,eAAe,sCAAsC;AAAA,QACzD,QAAQ;AAAA,QACR,MAAM,KAAK,UAAU,IAAI;AAAA,MAC3B,CAAC;AAED,YAAM,EAAE,4CAA4C,yBAAyB,GAAG,SAAS;AACzF,kBAAY;AAAA,IACd,SAAS,KAAK;AACZ,YAAM,UAAU,eAAe,QAAQ,IAAI,UAAU;AACrD,YAAM,SAAS,OAAO;AAAA,IACxB,UAAE;AACA,gBAAU,KAAK;AAAA,IACjB;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,GAAG,WAAW,CAAC;AAEjC,QAAM,gBAAgB,MAAM;AAAA,IAC1B,CAAC,UAA+B;AAC9B,WAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,cAAM,eAAe;AACrB,mBAAW;AAAA,MACb;AAAA,IACF;AAAA,IACA,CAAC,UAAU;AAAA,EACb;AAEA,MAAI,SAAS;AACX,WACE,qBAAC,QACC;AAAA,0BAAC,cACC,+BAAC,aAAU,WAAU,2BACnB;AAAA,4BAAC,UAAO,WAAU,WAAU;AAAA,QAC3B,EAAE,sCAAsC,kBAAkB;AAAA,SAC7D,GACF;AAAA,MACA,oBAAC,eAAY,WAAU,yCACrB,8BAAC,WAAQ,WAAU,8CAA6C,GAClE;AAAA,OACF;AAAA,EAEJ;AAEA,SACE,qBAAC,QACC;AAAA,yBAAC,cACC;AAAA,2BAAC,SAAI,WAAU,qCACb;AAAA,6BAAC,SAAI,WAAU,2BACb;AAAA,8BAAC,UAAO,WAAU,WAAU;AAAA,UAC5B,oBAAC,aAAW,YAAE,sCAAsC,kBAAkB,GAAE;AAAA,WAC1E;AAAA,QACA,oBAAC,SAAI,WAAU,2BACZ,mBACC,iCACE;AAAA,8BAAC,SAAM,SAAQ,WACZ,YAAE,kDAAkD,YAAY,GACnE;AAAA,UACA,oBAAC,SAAM,SAAS,OAAO,YAAY,YAAY,aAC5C,iBAAO,YACJ,EAAE,+CAA+C,SAAS,IAC1D,EAAE,gDAAgD,UAAU,GAClE;AAAA,WACF,IAEA,oBAAC,SAAM,SAAQ,aACZ,YAAE,qDAAqD,gBAAgB,GAC1E,GAEJ;AAAA,SACF;AAAA,MACA,oBAAC,mBACE;AAAA,QACC;AAAA,QACA;AAAA,MACF,GACF;AAAA,OACF;AAAA,IACA,oBAAC,eAAY,WAAW,eACtB,+BAAC,SAAI,WAAU,cACb;AAAA,2BAAC,SAAI,WAAU,2BACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,SAAS,KAAK;AAAA,YACd,iBAAiB,CAAC,YAChB,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,WAAW,YAAY,KAAK,EAAE;AAAA;AAAA,QAEhE;AAAA,QACA,oBAAC,SAAM,SAAQ,cAAa,WAAU,8BACnC,YAAE,iDAAiD,uBAAuB,GAC7E;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,6BACb;AAAA,6BAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAM,SAAQ,eACZ,YAAE,gDAAgD,UAAU,GAC/D;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,IAAG;AAAA,cACH,OAAO,KAAK;AAAA,cACZ,UAAU,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,UAAU,EAAE,OAAO,MAAM,EAAE;AAAA,cAC1E,aAAY;AAAA;AAAA,UACd;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAM,SAAQ,eACZ,YAAE,gDAAgD,UAAU,GAC/D;AAAA,UACA;AAAA,YAAC;AAAA;AAAA,cACC,IAAG;AAAA,cACH,MAAK;AAAA,cACL,OAAO,KAAK;AAAA,cACZ,UAAU,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,UAAU,EAAE,OAAO,MAAM,EAAE;AAAA,cAC1E,aAAa,SAAS,qDAAa;AAAA;AAAA,UACrC;AAAA,UACA,oBAAC,OAAE,WAAU,iCACV;AAAA,YACC;AAAA,YACA;AAAA,UACF,GACF;AAAA,WACF;AAAA,SACF;AAAA,MAEA,qBAAC,SAAI,WAAU,cACb;AAAA,4BAAC,SAAM,SAAQ,cACZ,YAAE,+CAA+C,cAAc,GAClE;AAAA,QACA;AAAA,UAAC;AAAA;AAAA,YACC,IAAG;AAAA,YACH,OAAO,KAAK;AAAA,YACZ,UAAU,CAAC,MAAM,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,SAAS,EAAE,OAAO,MAAM,EAAE;AAAA,YACzE,aAAY;AAAA;AAAA,QACd;AAAA,QACA,oBAAC,OAAE,WAAU,iCACV,YAAE,8CAA8C,mCAAmC,GACtF;AAAA,SACF;AAAA,MAEC,cACC;AAAA,QAAC;AAAA;AAAA,UACC,WAAW,kDACT,WAAW,UACP,wEACA,6DACN;AAAA,UAEC;AAAA,uBAAW,UACV,oBAAC,gBAAa,WAAU,WAAU,IAElC,oBAAC,WAAQ,WAAU,WAAU;AAAA,YAE/B,oBAAC,UAAM,qBAAW,SAAQ;AAAA;AAAA;AAAA,MAC5B;AAAA,MAGF,qBAAC,SAAI,WAAU,gCACb;AAAA;AAAA,UAAC;AAAA;AAAA,YACC,SAAQ;AAAA,YACR,SAAS;AAAA,YACT,UAAU,WAAW,CAAC,KAAK,YAAY,CAAC,KAAK;AAAA,YAE5C;AAAA,yBAAW,oBAAC,WAAQ,WAAU,6BAA4B;AAAA,cAC1D,EAAE,uDAAuD,iBAAiB;AAAA;AAAA;AAAA,QAC7E;AAAA,QACA,qBAAC,UAAO,SAAS,YAAY,UAAU,QACpC;AAAA,oBAAU,oBAAC,WAAQ,WAAU,6BAA4B;AAAA,UACzD,EAAE,6CAA6C,oBAAoB;AAAA,WACtE;AAAA,SACF;AAAA,OACF,GACF;AAAA,KACF;AAEJ;AAIe,SAAR,yBAA0C;AAC/C,QAAM,IAAI,KAAK;AAEf,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAClD,QAAM,CAAC,WAAW,YAAY,IAAI,SAAwB,IAAI;AAC9D,QAAM,CAAC,MAAM,OAAO,IAAI,SAAoB,SAAS;AACrD,QAAM,CAAC,YAAY,aAAa,IAAI,SAAS,KAAK;AAElD,QAAM,SAAS,cAAc;AAE7B,QAAM,UAAU;AAAA,IACd,MAAM;AAAA,MACJ;AAAA,QACE,MAAM;AAAA,QACN,OAAO,EAAE,wDAAwD,cAAc;AAAA,QAC/E,OAAO;AAAA,QACP,UAAU;AAAA,QACV,UAAU,CAAC,UACT,oBAAC,UAAK,WAAU,eAAe,iBAAO,SAAS,EAAE,GAAE;AAAA,MAEvD;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,OAAO,EAAE,wDAAwD,cAAc;AAAA,QAC/E,OAAO;AAAA,QACP,UAAU;AAAA,QACV,UAAU,CAAC,UACT,oBAAC,UAAK,WAAU,wCAAwC,iBAAO,SAAS,GAAG,GAAE;AAAA,MAEjF;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,OAAO,EAAE,sDAAsD,YAAY;AAAA,QAC3E,OAAO;AAAA,QACP,UAAU;AAAA,QACV,UAAU,CAAC,QAAiB,YAC1B,qBAAC,UAAK,WAAU,WACb;AAAA,iBAAO,QAAQ,qBAAqB,EAAE;AAAA,UAAE;AAAA,UAAI,OAAO,QAAQ,0BAA0B,EAAE;AAAA,UAAE;AAAA,WAC5F;AAAA,MAEJ;AAAA,MACA;AAAA,QACE,MAAM;AAAA,QACN,OAAO,EAAE,qDAAqD,QAAQ;AAAA,QACtE,OAAO;AAAA,QACP,UAAU;AAAA,QACV,UAAU,CAAC,UAAmB,oBAAC,eAAY,OAAO,UAAU,QAAQ,UAAU,QAAQ;AAAA,MACxF;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,mBAAmB,YAAY,MAAM;AACzC,iBAAa,IAAI;AACjB,YAAQ,SAAS;AACjB,kBAAc,IAAI;AAAA,EACpB,GAAG,CAAC,CAAC;AAEL,QAAM,YAAY;AAAA,IAChB,MACE,oBAAC,YAAS,SAAQ,WAAU,MAAK,MAAK,MAAM,oBAAC,QAAK,WAAU,WAAU,GAAI,SAAS,kBAChF,YAAE,4CAA4C,oBAAoB,GACrE;AAAA,IAEF,CAAC,kBAAkB,CAAC;AAAA,EACtB;AAEA,QAAM,QAAQ,oBAAsC;AAAA,IAClD,QAAQ;AAAA;AAAA;AAAA;AAAA,IAIR,cAAc;AAAA,IACd;AAAA,IACA,WAAW,EAAE,2CAA2C,iBAAiB;AAAA,IACzE,iBAAiB;AAAA,IACjB,YAAY;AAAA,IACZ,UAAU;AAAA,IACV,YAAY;AAAA,MACV,QAAQ;AAAA,MACR,UAAU;AAAA,QACR,eAAe;AAAA,QACf,kBAAkB;AAAA,QAClB,cAAc;AAAA,MAChB;AAAA,IACF;AAAA,EACF,CAAC;AAED,QAAM,iBAAiB;AAAA,IACrB,CAAC,OAAe;AACd,YAAM,QAAS,MAAM,MAAM,MAAgD,SAAS,CAAC;AACrF,YAAM,SAAS,MAAM,IAAI,OAAO,EAAE,OAAO,CAAC,MAA6B,MAAM,IAAI;AACjF,YAAM,MAAM,OAAO,KAAK,CAAC,MAAM,EAAE,OAAO,EAAE;AAC1C,UAAI,CAAC,IAAK;AACV,mBAAa,EAAE;AACf,cAAQ,UAAU,GAAG,CAAC;AACtB,oBAAc,IAAI;AAAA,IACpB;AAAA,IACA,CAAC,MAAM,MAAM,IAAI;AAAA,EACnB;AAEA,QAAM,eAAe,YAAY,YAAY;AAC3C,QAAI,WAAY;AAEhB,QAAI,CAAC,UAAU,CAAC,KAAK,YAAY,KAAK,GAAG;AACvC,YAAM,EAAE,oEAAoE,0BAA0B,GAAG,OAAO;AAChH;AAAA,IACF;AAEA,QAAI,KAAK,YAAY,KAAK,GAAG;AAC3B,UAAI;AACF,YAAI,IAAI,KAAK,YAAY,KAAK,CAAC;AAAA,MACjC,QAAQ;AACN,cAAM,EAAE,sEAAsE,kCAAkC,GAAG,OAAO;AAC1H;AAAA,MACF;AAAA,IACF;AAEA,kBAAc,IAAI;AAClB,QAAI;AACF,YAAM,YAAY,mBAAmB,KAAK,UAAU;AACpD,YAAM,aAAa,OAAO,KAAK,SAAS,EAAE,SAAS,YAAY;AAE/D,UAAI,QAAQ;AACV,cAAM,OAAgC,EAAE,IAAI,UAAU;AACtD,YAAI,KAAK,YAAY,KAAK,EAAG,MAAK,cAAc,KAAK,YAAY,KAAK;AAAA,YACjE,MAAK,cAAc;AACxB,aAAK,aAAa,cAAc;AAChC,aAAK,oBAAoB,KAAK;AAC9B,aAAK,yBAAyB,KAAK;AACnC,aAAK,WAAW,KAAK;AAErB,cAAM,eAAe,0CAA0C;AAAA,UAC7D,QAAQ;AAAA,UACR,MAAM,KAAK,UAAU,IAAI;AAAA,QAC3B,CAAC;AACD,cAAM,EAAE,mDAAmD,wBAAwB,GAAG,SAAS;AAAA,MACjG,OAAO;AACL,cAAM,OAAgC;AAAA,UACpC,aAAa,KAAK,YAAY,KAAK;AAAA,UACnC,mBAAmB,KAAK;AAAA,UACxB,wBAAwB,KAAK;AAAA,UAC7B,UAAU,KAAK;AAAA,QACjB;AACA,YAAI,KAAK,YAAY,KAAK,EAAG,MAAK,cAAc,KAAK,YAAY,KAAK;AACtE,YAAI,WAAY,MAAK,aAAa;AAElC,cAAM,eAAe,0CAA0C;AAAA,UAC7D,QAAQ;AAAA,UACR,MAAM,KAAK,UAAU,IAAI;AAAA,QAC3B,CAAC;AACD,cAAM,EAAE,mDAAmD,wBAAwB,GAAG,SAAS;AAAA,MACjG;AAEA,oBAAc,KAAK;AACnB,YAAM,QAAQ;AAAA,IAChB,QAAQ;AACN,YAAM,SAAS,EAAE,wDAAwD,iCAAiC,IAAI,EAAE,wDAAwD,iCAAiC,GAAG,OAAO;AAAA,IACrN,UAAE;AACA,oBAAc,KAAK;AAAA,IACrB;AAAA,EACF,GAAG,CAAC,MAAM,QAAQ,WAAW,YAAY,MAAM,SAAS,CAAC,CAAC;AAE1D,QAAM,sBAAsB;AAAA,IAC1B,CAAC,UAA+B;AAC9B,WAAK,MAAM,WAAW,MAAM,YAAY,MAAM,QAAQ,SAAS;AAC7D,cAAM,eAAe;AACrB,qBAAa;AAAA,MACf;AAAA,IACF;AAAA,IACA,CAAC,YAAY;AAAA,EACf;AAEA,QAAM,eAAe;AAAA,IACnB,OAAO,OAAe;AACpB,UAAI,CAAC,QAAQ,EAAE,mDAAmD,6BAA6B,CAAC,EAAG;AAEnG,UAAI;AACF,cAAM,eAAe,0CAA0C;AAAA,UAC7D,QAAQ;AAAA,UACR,MAAM,KAAK,UAAU,EAAE,GAAG,CAAC;AAAA,QAC7B,CAAC;AACD,cAAM,EAAE,mDAAmD,wBAAwB,GAAG,SAAS;AAC/F,cAAM,QAAQ;AAAA,MAChB,QAAQ;AACN,cAAM,EAAE,wDAAwD,iCAAiC,GAAG,OAAO;AAAA,MAC7G;AAAA,IACF;AAAA,IACA,CAAC,MAAM,SAAS,CAAC;AAAA,EACnB;AAEA,QAAM,aAAa;AAAA,IACjB,CAAC,YAAsC;AACrC,UAAI,CAAC,SAAS,GAAI,QAAO,CAAC;AAC1B,aAAO;AAAA,QACL,EAAE,IAAI,QAAQ,OAAO,EAAE,eAAe,MAAM,EAAE;AAAA,QAC9C,EAAE,IAAI,UAAU,OAAO,EAAE,iBAAiB,QAAQ,EAAE;AAAA,MACtD;AAAA,IACF;AAAA,IACA,CAAC,CAAC;AAAA,EACJ;AAEA,QAAM,kBAAkB;AAAA,IACtB,CAAC,UAAkB,YAAiB;AAClC,YAAM,KAAK,SAAS;AACpB,UAAI,CAAC,GAAI;AACT,UAAI,aAAa,OAAQ,gBAAe,EAAE;AAAA,eACjC,aAAa,SAAU,cAAa,EAAE;AAAA,IACjD;AAAA,IACA,CAAC,gBAAgB,YAAY;AAAA,EAC/B;AAEA,QAAM,cAAc,QAAQ,MAAM;AAChC,UAAM,OAAO;AACb,UAAM,UAAU;AAChB,UAAM,WAAW;AACjB,UAAM,YAAY;AAClB,UAAM,YAAY;AAClB,UAAM,WAAY,MAAM,MAAM,MAA6B,SAAS;AACpE,UAAM,gBAAgB,WAAW,UAAU,WAAW,OAAO;AAC7D,WAAO,KAAK,IAAI,KAAK,IAAI,eAAe,SAAS,GAAG,SAAS;AAAA,EAC/D,GAAG,CAAC,MAAM,MAAM,IAAI,CAAC;AAErB,MAAI,MAAM,WAAW;AACnB,WACE,oBAAC,QACC,+BAAC,YAAS,WAAU,aAClB;AAAA,0BAAC,oBAAiB;AAAA,MAClB,oBAAC,wBAAqB;AAAA,MACtB,qBAAC,QACC;AAAA,4BAAC,cACC,8BAAC,aAAW,YAAE,2CAA2C,iBAAiB,GAAE,GAC9E;AAAA,QACA,oBAAC,eACC,8BAAC,iBAAc,MAAM,GAAG,SAAS,GAAG,GACtC;AAAA,SACF;AAAA,OACF,GACF;AAAA,EAEJ;AAEA,SACE,oBAAC,QACC,+BAAC,YAAS,WAAU,aAElB;AAAA,wBAAC,oBAAiB;AAAA,IAGlB,oBAAC,wBAAqB;AAAA,IAGtB,qBAAC,QACC;AAAA,2BAAC,cACC;AAAA,4BAAC,aAAW,YAAE,2CAA2C,iBAAiB,GAAE;AAAA,QAC5E,oBAAC,mBACE;AAAA,UACC;AAAA,UACA;AAAA,QACF,GACF;AAAA,SACF;AAAA,MACA,oBAAC,eACC,8BAAC,SAAI,OAAO,EAAE,QAAQ,YAAY,GAChC;AAAA,QAAC;AAAA;AAAA,UACE,GAAG,MAAM;AAAA,UACV,SAAO;AAAA,UACP,SAAQ;AAAA,UACR;AAAA,UACA,aAAa;AAAA;AAAA,MACf,GACF,GACF;AAAA,OACF;AAAA,IAEA,oBAAC,UAAO,MAAM,YAAY,cAAc,eACtC,+BAAC,iBAAc,WAAU,eAAc,WAAW,qBAChD;AAAA,0BAAC,gBACC,8BAAC,eAAa,mBAAS,EAAE,0CAA0C,qBAAqB,IAAI,EAAE,4CAA4C,oBAAoB,GAAE,GAClK;AAAA,MAEA,qBAAC,SAAI,WAAU,mBACb;AAAA,6BAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAM,SAAQ,eAAe,YAAE,wDAAwD,cAAc,GAAE;AAAA,UACxG;AAAA,YAAC;AAAA;AAAA,cACC,IAAG;AAAA,cACH,OAAO,KAAK;AAAA,cACZ,UAAU,CAAC,UAAU,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,aAAa,MAAM,OAAO,MAAM,EAAE;AAAA,cACrF,aAAY;AAAA,cACZ,UAAU;AAAA,cACV,WAAW,CAAC;AAAA;AAAA,UACd;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAM,SAAQ,eAAe,YAAE,wDAAwD,cAAc,GAAE;AAAA,UACxG;AAAA,YAAC;AAAA;AAAA,cACC,IAAG;AAAA,cACH,OAAO,KAAK;AAAA,cACZ,UAAU,CAAC,UAAU,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,aAAa,MAAM,OAAO,MAAM,EAAE;AAAA,cACrF,aAAY;AAAA;AAAA,UACd;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,cACb;AAAA,8BAAC,SAAO,YAAE,iDAAiD,gBAAgB,GAAE;AAAA,WAC3E,MAAM;AACN,kBAAM,OAAO,oBAAoB,KAAK,YAAY,KAAK,EAAE,YAAY,CAAC;AACtE,kBAAM,WAAW,CAAC,KAAa,UAC7B,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,YAAY,EAAE,GAAG,KAAK,YAAY,CAAC,GAAG,GAAG,MAAM,EAAE,EAAE;AACnF,gBAAI,MAAM;AACR,qBAAO,KAAK,IAAI,CAAC,MACf,qBAAC,SAAgB,WAAU,gBACzB;AAAA,oCAAC,SAAM,SAAS,QAAQ,EAAE,GAAG,IAAI,WAAU,iCAAiC,YAAE,OAAM;AAAA,gBACnF,EAAE,YACD;AAAA,kBAAC;AAAA;AAAA,oBACC,IAAI,QAAQ,EAAE,GAAG;AAAA,oBACjB,OAAO,KAAK,WAAW,EAAE,GAAG,KAAK;AAAA,oBACjC,UAAU,CAAC,UAAU,SAAS,EAAE,KAAK,MAAM,OAAO,KAAK;AAAA,oBACvD,aAAa,EAAE;AAAA,oBACf,MAAM;AAAA,oBACN,WAAU;AAAA;AAAA,gBACZ,IAEA;AAAA,kBAAC;AAAA;AAAA,oBACC,IAAI,QAAQ,EAAE,GAAG;AAAA,oBACjB,MAAM,EAAE,SAAS,aAAa;AAAA,oBAC9B,cAAa;AAAA,oBACb,OAAO,KAAK,WAAW,EAAE,GAAG,KAAK;AAAA,oBACjC,UAAU,CAAC,UAAU,SAAS,EAAE,KAAK,MAAM,OAAO,KAAK;AAAA,oBACvD,aAAa,EAAE;AAAA;AAAA,gBACjB;AAAA,mBAnBM,EAAE,GAqBZ,CACD;AAAA,YACH;AAEA,mBACE,qBAAC,SAAI,WAAU,cACZ;AAAA,qBAAO,QAAQ,KAAK,UAAU,EAAE,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,QAC5C,qBAAC,SAAc,WAAU,2BACvB;AAAA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAU;AAAA,oBACV,cAAc;AAAA,oBACd,aAAY;AAAA,oBACZ,QAAQ,CAAC,UAAU;AACjB,4BAAM,KAAK,MAAM,OAAO,MAAM,KAAK;AACnC,0BAAI,OAAO,EAAG;AACd,8BAAQ,CAAC,SAAS;AAChB,8BAAM,OAAO,EAAE,GAAG,KAAK,WAAW;AAClC,8BAAM,MAAM,KAAK,CAAC,KAAK;AACvB,+BAAO,KAAK,CAAC;AACb,4BAAI,GAAI,MAAK,EAAE,IAAI;AACnB,+BAAO,EAAE,GAAG,MAAM,YAAY,KAAK;AAAA,sBACrC,CAAC;AAAA,oBACH;AAAA;AAAA,gBACF;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,WAAU;AAAA,oBACV,OAAO;AAAA,oBACP,aAAY;AAAA,oBACZ,UAAU,CAAC,UAAU,SAAS,GAAG,MAAM,OAAO,KAAK;AAAA;AAAA,gBACrD;AAAA,gBACA;AAAA,kBAAC;AAAA;AAAA,oBACC,MAAK;AAAA,oBACL,SAAQ;AAAA,oBACR,MAAK;AAAA,oBACL,SAAS,MACP,QAAQ,CAAC,SAAS;AAChB,4BAAM,OAAO,EAAE,GAAG,KAAK,WAAW;AAClC,6BAAO,KAAK,CAAC;AACb,6BAAO,EAAE,GAAG,MAAM,YAAY,KAAK;AAAA,oBACrC,CAAC;AAAA,oBAGF,YAAE,iDAAiD,QAAQ;AAAA;AAAA,gBAC9D;AAAA,mBApCQ,GAqCV,CACD;AAAA,cACD;AAAA,gBAAC;AAAA;AAAA,kBACC,MAAK;AAAA,kBACL,SAAQ;AAAA,kBACR,MAAK;AAAA,kBACL,WAAU;AAAA,kBACV,SAAS,MACP,QAAQ,CAAC,UAAU;AAAA,oBACjB,GAAG;AAAA,oBACH,YAAY,EAAE,GAAG,KAAK,YAAY,CAAC,SAAS,OAAO,KAAK,KAAK,UAAU,EAAE,SAAS,CAAC,EAAE,GAAG,GAAG;AAAA,kBAC7F,EAAE;AAAA,kBAGJ;AAAA,wCAAC,QAAK,WAAU,oBAAmB;AAAA,oBAClC,EAAE,mDAAmD,WAAW;AAAA;AAAA;AAAA,cACnE;AAAA,eACF;AAAA,UAEJ,GAAG;AAAA,WACL;AAAA,QAEA,qBAAC,SAAI,WAAU,0BACb;AAAA,+BAAC,SAAI,WAAU,cACb;AAAA,gCAAC,SAAM,SAAQ,qBAAqB,YAAE,8DAA8D,uBAAuB,GAAE;AAAA,YAC7H;AAAA,cAAC;AAAA;AAAA,gBACC,IAAG;AAAA,gBACH,MAAK;AAAA,gBACL,KAAK;AAAA,gBACL,KAAK;AAAA,gBACL,OAAO,KAAK;AAAA,gBACZ,UAAU,CAAC,UACT,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,mBAAmB,OAAO,MAAM,OAAO,KAAK,KAAK,GAAG,EAAE;AAAA;AAAA,YAExF;AAAA,aACF;AAAA,UACA,qBAAC,SAAI,WAAU,cACb;AAAA,gCAAC,SAAM,SAAQ,0BAA0B,YAAE,mEAAmE,kBAAkB,GAAE;AAAA,YAClI;AAAA,cAAC;AAAA;AAAA,gBACC,IAAG;AAAA,gBACH,MAAK;AAAA,gBACL,KAAK;AAAA,gBACL,KAAK;AAAA,gBACL,OAAO,KAAK;AAAA,gBACZ,UAAU,CAAC,UACT,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,wBAAwB,OAAO,MAAM,OAAO,KAAK,KAAK,GAAG,EAAE;AAAA;AAAA,YAE7F;AAAA,aACF;AAAA,WACF;AAAA,QAEA,qBAAC,SAAI,WAAU,2BACb;AAAA;AAAA,YAAC;AAAA;AAAA,cACC,IAAG;AAAA,cACH,SAAS,KAAK;AAAA,cACd,iBAAiB,CAAC,YAChB,QAAQ,CAAC,UAAU,EAAE,GAAG,MAAM,UAAU,YAAY,KAAK,EAAE;AAAA;AAAA,UAE/D;AAAA,UACA,oBAAC,SAAM,SAAQ,YAAW,WAAU,kBACjC,YAAE,qDAAqD,QAAQ,GAClE;AAAA,WACF;AAAA,SACF;AAAA,MAEA,qBAAC,gBACC;AAAA,4BAAC,UAAO,SAAQ,WAAU,SAAS,MAAM,cAAc,KAAK,GAAG,UAAU,YACtE,YAAE,iBAAiB,QAAQ,GAC9B;AAAA,QACA,oBAAC,UAAO,SAAS,cAAc,UAAU,YACtC,uBAAa,EAAE,iBAAiB,WAAW,IAAI,SAAS,EAAE,eAAe,MAAM,IAAI,EAAE,iBAAiB,QAAQ,GACjH;AAAA,SACF;AAAA,OACF,GACF;AAAA,KACF,GACF;AAEJ;",
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 { 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 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\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// One entry per registered carrier adapter (see\n// lib/adapters/index.ts / services/carrierRegistry.ts). Each carrier maps to a\n// fixed set of auth_config keys and a default events endpoint (the value the\n// adapter falls back to when apiEndpoint is null). ShipsGo is intentionally\n// absent \u2014 it has its own dedicated config section, not a carrier-configs row.\ntype CredField = { key: string; label: string; secret?: boolean; placeholder?: string; optional?: boolean }\n\ntype CarrierSpec = {\n value: string\n label: string\n /** Adapter's default events endpoint; '' when the adapter has none. */\n defaultEndpoint: string\n /** Evergreen has no default endpoint \u2014 apiEndpoint must be supplied. */\n endpointRequired?: boolean\n credentials: CredField[]\n}\n\nconst CLIENT_ID: CredField = { key: 'client_id', label: 'Client ID' }\nconst CLIENT_SECRET: CredField = { key: 'client_secret', label: 'Client secret', secret: true }\n\nconst CARRIER_SPECS: CarrierSpec[] = [\n {\n value: 'maersk',\n label: 'Maersk',\n defaultEndpoint: 'https://api.maersk.com/track-and-trace-private/events',\n credentials: [CLIENT_ID, CLIENT_SECRET],\n },\n {\n value: 'msc',\n label: 'MSC',\n defaultEndpoint: 'https://api.tech.msc.com/msc/trackandtrace/v2.2/events',\n // The MSC OAuth account identity (client_id/scope/token_url/aud) is supplied\n // per config, not baked into the adapter \u2014 so the form collects it here\n // alongside the certificate. `aud` defaults to `token_url` when left blank.\n credentials: [\n CLIENT_ID,\n { key: 'scope', label: 'Scope' },\n { key: 'token_url', label: 'Token URL' },\n { key: 'aud', label: 'Audience', optional: true, placeholder: 'defaults to Token URL' },\n { key: 'certificate_base64', label: 'Certificate (base64 PFX)', secret: true },\n ],\n },\n {\n value: 'cma-cgm',\n label: 'CMA CGM',\n defaultEndpoint: 'https://apis.cma-cgm.net/operation/trackandtrace/v1/events',\n credentials: [{ key: 'api_key', label: 'API key', secret: true }],\n },\n {\n value: 'hapag-lloyd',\n label: 'Hapag-Lloyd',\n defaultEndpoint: 'https://api.hlag.com/hlag/external/v2/events',\n credentials: [CLIENT_ID, CLIENT_SECRET],\n },\n {\n value: 'zim',\n label: 'ZIM',\n defaultEndpoint: 'https://apigw.zim.com/trackAndTrace/v1',\n credentials: [CLIENT_ID, CLIENT_SECRET, { key: 'subscription_key', label: 'Subscription key', secret: true }],\n },\n {\n value: 'cosco',\n label: 'COSCO',\n defaultEndpoint: 'https://apis.cargosmart.com/openapi/cs2/ctvc/COSU',\n credentials: [\n { key: 'app_key', label: 'App key', secret: true },\n { key: 'customer_id', label: 'Customer ID' },\n { key: 'scac_code', label: 'SCAC code', optional: true, placeholder: 'COSU' },\n ],\n },\n {\n value: 'evergreen',\n label: 'Evergreen',\n defaultEndpoint: '',\n endpointRequired: true,\n credentials: [CLIENT_ID, CLIENT_SECRET, { key: 'token_url', label: 'Token URL' }],\n },\n]\n\nfunction specFor(carrierCode: string): CarrierSpec | undefined {\n return CARRIER_SPECS.find((c) => c.value === carrierCode)\n}\n\n/**\n * Credential fields for a carrier. For a known carrier this is its fixed set;\n * for an unrecognised legacy row (edit) we derive the fields from the stored\n * auth_config keys so the form never falls back to raw JSON.\n */\nfunction credentialFieldsFor(carrierCode: string, authConfig?: Record<string, unknown> | null): CredField[] {\n const spec = specFor(carrierCode)\n if (spec) return spec.credentials\n const keys = authConfig ? Object.keys(authConfig) : []\n return keys.map((k) => ({ key: k, label: k, secret: /secret|password|key|token|cert/i.test(k) }))\n}\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 credentials: Record<string, string>\n rateLimitRequests: number\n rateLimitWindowSeconds: number\n isActive: boolean\n}\n\nfunction formForCarrier(carrierCode: string): FormState {\n const spec = specFor(carrierCode)\n return {\n carrierCode,\n apiEndpoint: spec?.defaultEndpoint ?? '',\n credentials: {},\n rateLimitRequests: 60,\n rateLimitWindowSeconds: 60,\n isActive: true,\n }\n}\n\nconst emptyForm: FormState = formForCarrier(CARRIER_SPECS[0].value)\n\nfunction rowToForm(row: CarrierConfigRow): FormState {\n // auth_config is returned by the API (plaintext jsonb); prefill the fields.\n const credentials: Record<string, string> = {}\n const auth = row.authConfig ?? {}\n for (const [k, v] of Object.entries(auth)) credentials[k] = v == null ? '' : String(v)\n return {\n carrierCode: row.carrierCode,\n apiEndpoint: row.apiEndpoint ?? specFor(row.carrierCode)?.defaultEndpoint ?? '',\n credentials,\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 const credFields = credentialFieldsFor(form.carrierCode, form.credentials)\n const spec = specFor(form.carrierCode)\n\n const setField = useCallback(\n <K extends keyof FormState>(key: K, value: FormState[K]) => setForm((p) => ({ ...p, [key]: value })),\n [],\n )\n const setCredential = useCallback(\n (key: string, value: string) => setForm((p) => ({ ...p, credentials: { ...p.credentials, [key]: value } })),\n [],\n )\n const changeCarrier = useCallback((carrierCode: string) => setForm(formForCarrier(carrierCode)), [])\n\n const columns = useMemo<ColumnDef[]>(\n () => [\n {\n data: 'carrierCode',\n title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier'),\n width: 150,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"font-medium\">{specFor(String(value))?.label ?? 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 is required'), 'error')\n return\n }\n\n const currentSpec = specFor(form.carrierCode)\n const endpoint = form.apiEndpoint.trim()\n if (currentSpec?.endpointRequired && !endpoint) {\n flash(t('shipment_tracking.carrier_configs.validation.endpointRequired', 'API endpoint is required for this carrier'), 'error')\n return\n }\n if (endpoint) {\n try {\n new URL(endpoint)\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 // Assemble auth_config from the structured fields; require all non-optional.\n const fields = credentialFieldsFor(form.carrierCode, form.credentials)\n const authConfig: Record<string, string> = {}\n for (const f of fields) {\n const val = (form.credentials[f.key] ?? '').trim()\n if (!val) {\n if (f.optional) continue\n flash(\n t('shipment_tracking.carrier_configs.validation.credentialsRequired', 'Fill in all credential fields'),\n 'error',\n )\n return\n }\n authConfig[f.key] = val\n }\n\n setSubmitting(true)\n try {\n const hasAuth = Object.keys(authConfig).length > 0\n if (isEdit) {\n const body: Record<string, unknown> = { id: editingId }\n body.apiEndpoint = endpoint || null\n body.authConfig = hasAuth ? 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 (endpoint) body.apiEndpoint = endpoint\n if (hasAuth) 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')}</Label>\n {isEdit ? (\n <Input id=\"carrierCode\" value={spec?.label ?? form.carrierCode} disabled />\n ) : (\n <select\n id=\"carrierCode\"\n className=\"border rounded h-9 px-2 text-sm\"\n value={form.carrierCode}\n onChange={(event) => changeCarrier(event.target.value)}\n >\n {CARRIER_SPECS.map((c) => (\n <option key={c.value} value={c.value}>{c.label}</option>\n ))}\n </select>\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) => setField('apiEndpoint', event.target.value)}\n placeholder=\"https://api.example.com/tracking\"\n />\n {spec?.endpointRequired && (\n <p className=\"text-xs text-muted-foreground\">\n {t('shipment_tracking.carrier_configs.hints.endpointRequired', 'This carrier requires an explicit API endpoint.')}\n </p>\n )}\n </div>\n\n {/* Structured credentials (per carrier) */}\n {credFields.map((f) => (\n <div className=\"grid gap-2\" key={f.key}>\n <Label htmlFor={`cred-${f.key}`}>\n {t(`shipment_tracking.carrier_configs.cred.${f.key}`, f.label)}\n {f.optional ? ` (${t('common.optional', 'optional')})` : ''}\n </Label>\n <Input\n id={`cred-${f.key}`}\n type={f.secret ? 'password' : 'text'}\n autoComplete=\"off\"\n value={form.credentials[f.key] ?? ''}\n onChange={(event) => setCredential(f.key, event.target.value)}\n placeholder={f.placeholder}\n />\n </div>\n ))}\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 setField('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 setField('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) => setField('isActive', checked === true)}\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
  }
@@ -68,9 +68,21 @@
68
68
  "shipment_tracking.carrier_configs.flash.updateFailed": "Nie udało się zaktualizować konfiguracji przewoźnika",
69
69
  "shipment_tracking.carrier_configs.flash.updated": "Konfiguracja przewoźnika została zaktualizowana",
70
70
  "shipment_tracking.carrier_configs.title": "Konfiguracja przewoźników",
71
+ "shipment_tracking.carrier_configs.cred.api_key": "Klucz API",
72
+ "shipment_tracking.carrier_configs.cred.app_key": "App key",
73
+ "shipment_tracking.carrier_configs.cred.certificate_base64": "Certyfikat (base64 PFX)",
74
+ "shipment_tracking.carrier_configs.cred.client_id": "Client ID",
75
+ "shipment_tracking.carrier_configs.cred.client_secret": "Client secret",
76
+ "shipment_tracking.carrier_configs.cred.customer_id": "Customer ID",
77
+ "shipment_tracking.carrier_configs.cred.scac_code": "Kod SCAC",
78
+ "shipment_tracking.carrier_configs.cred.subscription_key": "Subscription key",
79
+ "shipment_tracking.carrier_configs.cred.token_url": "Adres URL tokenu",
80
+ "shipment_tracking.carrier_configs.hints.endpointRequired": "Ten przewoźnik wymaga jawnego adresu endpointu API.",
71
81
  "shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl": "Endpoint API musi być poprawnym adresem URL",
72
82
  "shipment_tracking.carrier_configs.validation.authConfigInvalidJson": "Konfiguracja autoryzacji musi być poprawnym JSON",
73
- "shipment_tracking.carrier_configs.validation.carrierCodeRequired": "Kod przewoźnika jest wymagany",
83
+ "shipment_tracking.carrier_configs.validation.carrierCodeRequired": "Przewoźnik jest wymagany",
84
+ "shipment_tracking.carrier_configs.validation.credentialsRequired": "Uzupełnij wszystkie pola danych uwierzytelniających",
85
+ "shipment_tracking.carrier_configs.validation.endpointRequired": "Dla tego przewoźnika adres endpointu API jest wymagany",
74
86
  "shipment_tracking.details.bookingDetails": "Szczegóły rezerwacji",
75
87
  "shipment_tracking.details.coordinates": "Współrzędne",
76
88
  "shipment_tracking.details.currentPosition": "Aktualna pozycja",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@freighttech/shipment-tracking",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -25,12 +25,107 @@ import { Button as ButtonV2 } from '@freighttech/ui/primitives-v2'
25
25
  import { Input } from '@freighttech/ui/primitives/input'
26
26
  import { Label } from '@freighttech/ui/primitives/label'
27
27
  import { Checkbox } from '@freighttech/ui/primitives/checkbox'
28
- import { Textarea } from '@freighttech/ui/primitives/textarea'
29
28
  import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@freighttech/ui/primitives/card'
30
29
  import { Badge } from '@freighttech/ui/primitives/badge'
31
30
  import { Loader2, CheckCircle2, XCircle, Shield, Plus } from 'lucide-react'
32
31
  import { ShipsGoConfigSection } from '../../components/ShipsGoConfigSection'
33
32
 
33
+ // ─── Carrier specs ───────────────────────────────────────────
34
+ // One entry per registered carrier adapter (see
35
+ // lib/adapters/index.ts / services/carrierRegistry.ts). Each carrier maps to a
36
+ // fixed set of auth_config keys and a default events endpoint (the value the
37
+ // adapter falls back to when apiEndpoint is null). ShipsGo is intentionally
38
+ // absent — it has its own dedicated config section, not a carrier-configs row.
39
+ type CredField = { key: string; label: string; secret?: boolean; placeholder?: string; optional?: boolean }
40
+
41
+ type CarrierSpec = {
42
+ value: string
43
+ label: string
44
+ /** Adapter's default events endpoint; '' when the adapter has none. */
45
+ defaultEndpoint: string
46
+ /** Evergreen has no default endpoint — apiEndpoint must be supplied. */
47
+ endpointRequired?: boolean
48
+ credentials: CredField[]
49
+ }
50
+
51
+ const CLIENT_ID: CredField = { key: 'client_id', label: 'Client ID' }
52
+ const CLIENT_SECRET: CredField = { key: 'client_secret', label: 'Client secret', secret: true }
53
+
54
+ const CARRIER_SPECS: CarrierSpec[] = [
55
+ {
56
+ value: 'maersk',
57
+ label: 'Maersk',
58
+ defaultEndpoint: 'https://api.maersk.com/track-and-trace-private/events',
59
+ credentials: [CLIENT_ID, CLIENT_SECRET],
60
+ },
61
+ {
62
+ value: 'msc',
63
+ label: 'MSC',
64
+ defaultEndpoint: 'https://api.tech.msc.com/msc/trackandtrace/v2.2/events',
65
+ // The MSC OAuth account identity (client_id/scope/token_url/aud) is supplied
66
+ // per config, not baked into the adapter — so the form collects it here
67
+ // alongside the certificate. `aud` defaults to `token_url` when left blank.
68
+ credentials: [
69
+ CLIENT_ID,
70
+ { key: 'scope', label: 'Scope' },
71
+ { key: 'token_url', label: 'Token URL' },
72
+ { key: 'aud', label: 'Audience', optional: true, placeholder: 'defaults to Token URL' },
73
+ { key: 'certificate_base64', label: 'Certificate (base64 PFX)', secret: true },
74
+ ],
75
+ },
76
+ {
77
+ value: 'cma-cgm',
78
+ label: 'CMA CGM',
79
+ defaultEndpoint: 'https://apis.cma-cgm.net/operation/trackandtrace/v1/events',
80
+ credentials: [{ key: 'api_key', label: 'API key', secret: true }],
81
+ },
82
+ {
83
+ value: 'hapag-lloyd',
84
+ label: 'Hapag-Lloyd',
85
+ defaultEndpoint: 'https://api.hlag.com/hlag/external/v2/events',
86
+ credentials: [CLIENT_ID, CLIENT_SECRET],
87
+ },
88
+ {
89
+ value: 'zim',
90
+ label: 'ZIM',
91
+ defaultEndpoint: 'https://apigw.zim.com/trackAndTrace/v1',
92
+ credentials: [CLIENT_ID, CLIENT_SECRET, { key: 'subscription_key', label: 'Subscription key', secret: true }],
93
+ },
94
+ {
95
+ value: 'cosco',
96
+ label: 'COSCO',
97
+ defaultEndpoint: 'https://apis.cargosmart.com/openapi/cs2/ctvc/COSU',
98
+ credentials: [
99
+ { key: 'app_key', label: 'App key', secret: true },
100
+ { key: 'customer_id', label: 'Customer ID' },
101
+ { key: 'scac_code', label: 'SCAC code', optional: true, placeholder: 'COSU' },
102
+ ],
103
+ },
104
+ {
105
+ value: 'evergreen',
106
+ label: 'Evergreen',
107
+ defaultEndpoint: '',
108
+ endpointRequired: true,
109
+ credentials: [CLIENT_ID, CLIENT_SECRET, { key: 'token_url', label: 'Token URL' }],
110
+ },
111
+ ]
112
+
113
+ function specFor(carrierCode: string): CarrierSpec | undefined {
114
+ return CARRIER_SPECS.find((c) => c.value === carrierCode)
115
+ }
116
+
117
+ /**
118
+ * Credential fields for a carrier. For a known carrier this is its fixed set;
119
+ * for an unrecognised legacy row (edit) we derive the fields from the stored
120
+ * auth_config keys so the form never falls back to raw JSON.
121
+ */
122
+ function credentialFieldsFor(carrierCode: string, authConfig?: Record<string, unknown> | null): CredField[] {
123
+ const spec = specFor(carrierCode)
124
+ if (spec) return spec.credentials
125
+ const keys = authConfig ? Object.keys(authConfig) : []
126
+ return keys.map((k) => ({ key: k, label: k, secret: /secret|password|key|token|cert/i.test(k) }))
127
+ }
128
+
34
129
  // ─── Carrier Config Types ────────────────────────────────────
35
130
 
36
131
  type CarrierConfigRow = {
@@ -63,93 +158,41 @@ function mapItem(item: Record<string, unknown>): CarrierConfigRow | null {
63
158
  type FormState = {
64
159
  carrierCode: string
65
160
  apiEndpoint: string
66
- /** authConfig as flat key→value; assembled into the authConfig object on save. */
67
- authValues: Record<string, string>
161
+ credentials: Record<string, string>
68
162
  rateLimitRequests: number
69
163
  rateLimitWindowSeconds: number
70
164
  isActive: boolean
71
165
  }
72
166
 
73
- const emptyForm: FormState = {
74
- carrierCode: '',
75
- apiEndpoint: '',
76
- authValues: {},
77
- rateLimitRequests: 60,
78
- rateLimitWindowSeconds: 60,
79
- isActive: true,
167
+ function formForCarrier(carrierCode: string): FormState {
168
+ const spec = specFor(carrierCode)
169
+ return {
170
+ carrierCode,
171
+ apiEndpoint: spec?.defaultEndpoint ?? '',
172
+ credentials: {},
173
+ rateLimitRequests: 60,
174
+ rateLimitWindowSeconds: 60,
175
+ isActive: true,
176
+ }
80
177
  }
81
178
 
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
- }
179
+ const emptyForm: FormState = formForCarrier(CARRIER_SPECS[0].value)
89
180
 
90
181
  function rowToForm(row: CarrierConfigRow): FormState {
182
+ // auth_config is returned by the API (plaintext jsonb); prefill the fields.
183
+ const credentials: Record<string, string> = {}
184
+ const auth = row.authConfig ?? {}
185
+ for (const [k, v] of Object.entries(auth)) credentials[k] = v == null ? '' : String(v)
91
186
  return {
92
187
  carrierCode: row.carrierCode,
93
- apiEndpoint: row.apiEndpoint ?? '',
94
- authValues: authConfigToValues(row.authConfig),
188
+ apiEndpoint: row.apiEndpoint ?? specFor(row.carrierCode)?.defaultEndpoint ?? '',
189
+ credentials,
95
190
  rateLimitRequests: row.rateLimitRequests,
96
191
  rateLimitWindowSeconds: row.rateLimitWindowSeconds,
97
192
  isActive: row.isActive,
98
193
  }
99
194
  }
100
195
 
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
-
153
196
  // ─── BIC Config Types ────────────────────────────────────────
154
197
 
155
198
  type BicConfigData = {
@@ -469,16 +512,28 @@ export default function TrackingAuthConfigPage() {
469
512
  const [submitting, setSubmitting] = useState(false)
470
513
 
471
514
  const isEdit = editingId !== null
515
+ const credFields = credentialFieldsFor(form.carrierCode, form.credentials)
516
+ const spec = specFor(form.carrierCode)
517
+
518
+ const setField = useCallback(
519
+ <K extends keyof FormState>(key: K, value: FormState[K]) => setForm((p) => ({ ...p, [key]: value })),
520
+ [],
521
+ )
522
+ const setCredential = useCallback(
523
+ (key: string, value: string) => setForm((p) => ({ ...p, credentials: { ...p.credentials, [key]: value } })),
524
+ [],
525
+ )
526
+ const changeCarrier = useCallback((carrierCode: string) => setForm(formForCarrier(carrierCode)), [])
472
527
 
473
528
  const columns = useMemo<ColumnDef[]>(
474
529
  () => [
475
530
  {
476
531
  data: 'carrierCode',
477
- title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code'),
532
+ title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier'),
478
533
  width: 150,
479
534
  readOnly: true,
480
535
  renderer: (value: unknown) => (
481
- <span className="font-medium">{String(value ?? '')}</span>
536
+ <span className="font-medium">{specFor(String(value))?.label ?? String(value ?? '')}</span>
482
537
  ),
483
538
  },
484
539
  {
@@ -565,29 +620,48 @@ export default function TrackingAuthConfigPage() {
565
620
  if (submitting) return
566
621
 
567
622
  if (!isEdit && !form.carrierCode.trim()) {
568
- flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier code is required'), 'error')
623
+ flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier is required'), 'error')
569
624
  return
570
625
  }
571
626
 
572
- if (form.apiEndpoint.trim()) {
627
+ const currentSpec = specFor(form.carrierCode)
628
+ const endpoint = form.apiEndpoint.trim()
629
+ if (currentSpec?.endpointRequired && !endpoint) {
630
+ flash(t('shipment_tracking.carrier_configs.validation.endpointRequired', 'API endpoint is required for this carrier'), 'error')
631
+ return
632
+ }
633
+ if (endpoint) {
573
634
  try {
574
- new URL(form.apiEndpoint.trim())
635
+ new URL(endpoint)
575
636
  } catch {
576
637
  flash(t('shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl', 'API endpoint must be a valid URL'), 'error')
577
638
  return
578
639
  }
579
640
  }
580
641
 
642
+ // Assemble auth_config from the structured fields; require all non-optional.
643
+ const fields = credentialFieldsFor(form.carrierCode, form.credentials)
644
+ const authConfig: Record<string, string> = {}
645
+ for (const f of fields) {
646
+ const val = (form.credentials[f.key] ?? '').trim()
647
+ if (!val) {
648
+ if (f.optional) continue
649
+ flash(
650
+ t('shipment_tracking.carrier_configs.validation.credentialsRequired', 'Fill in all credential fields'),
651
+ 'error',
652
+ )
653
+ return
654
+ }
655
+ authConfig[f.key] = val
656
+ }
657
+
581
658
  setSubmitting(true)
582
659
  try {
583
- const assembled = valuesToAuthConfig(form.authValues)
584
- const authConfig = Object.keys(assembled).length ? assembled : undefined
585
-
660
+ const hasAuth = Object.keys(authConfig).length > 0
586
661
  if (isEdit) {
587
662
  const body: Record<string, unknown> = { id: editingId }
588
- if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()
589
- else body.apiEndpoint = null
590
- body.authConfig = authConfig ?? null
663
+ body.apiEndpoint = endpoint || null
664
+ body.authConfig = hasAuth ? authConfig : null
591
665
  body.rateLimitRequests = form.rateLimitRequests
592
666
  body.rateLimitWindowSeconds = form.rateLimitWindowSeconds
593
667
  body.isActive = form.isActive
@@ -604,8 +678,8 @@ export default function TrackingAuthConfigPage() {
604
678
  rateLimitWindowSeconds: form.rateLimitWindowSeconds,
605
679
  isActive: form.isActive,
606
680
  }
607
- if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()
608
- if (authConfig) body.authConfig = authConfig
681
+ if (endpoint) body.apiEndpoint = endpoint
682
+ if (hasAuth) body.authConfig = authConfig
609
683
 
610
684
  await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {
611
685
  method: 'POST',
@@ -743,15 +817,21 @@ export default function TrackingAuthConfigPage() {
743
817
 
744
818
  <div className="grid gap-4 py-2">
745
819
  <div className="grid gap-2">
746
- <Label htmlFor="carrierCode">{t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code')}</Label>
747
- <Input
748
- id="carrierCode"
749
- value={form.carrierCode}
750
- onChange={(event) => setForm((prev) => ({ ...prev, carrierCode: event.target.value }))}
751
- placeholder="e.g. maersk, msc, cma-cgm"
752
- disabled={isEdit}
753
- autoFocus={!isEdit}
754
- />
820
+ <Label htmlFor="carrierCode">{t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier')}</Label>
821
+ {isEdit ? (
822
+ <Input id="carrierCode" value={spec?.label ?? form.carrierCode} disabled />
823
+ ) : (
824
+ <select
825
+ id="carrierCode"
826
+ className="border rounded h-9 px-2 text-sm"
827
+ value={form.carrierCode}
828
+ onChange={(event) => changeCarrier(event.target.value)}
829
+ >
830
+ {CARRIER_SPECS.map((c) => (
831
+ <option key={c.value} value={c.value}>{c.label}</option>
832
+ ))}
833
+ </select>
834
+ )}
755
835
  </div>
756
836
 
757
837
  <div className="grid gap-2">
@@ -759,105 +839,33 @@ export default function TrackingAuthConfigPage() {
759
839
  <Input
760
840
  id="apiEndpoint"
761
841
  value={form.apiEndpoint}
762
- onChange={(event) => setForm((prev) => ({ ...prev, apiEndpoint: event.target.value }))}
842
+ onChange={(event) => setField('apiEndpoint', event.target.value)}
763
843
  placeholder="https://api.example.com/tracking"
764
844
  />
845
+ {spec?.endpointRequired && (
846
+ <p className="text-xs text-muted-foreground">
847
+ {t('shipment_tracking.carrier_configs.hints.endpointRequired', 'This carrier requires an explicit API endpoint.')}
848
+ </p>
849
+ )}
765
850
  </div>
766
851
 
767
- <div className="grid gap-2">
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
- })()}
860
- </div>
852
+ {/* Structured credentials (per carrier) */}
853
+ {credFields.map((f) => (
854
+ <div className="grid gap-2" key={f.key}>
855
+ <Label htmlFor={`cred-${f.key}`}>
856
+ {t(`shipment_tracking.carrier_configs.cred.${f.key}`, f.label)}
857
+ {f.optional ? ` (${t('common.optional', 'optional')})` : ''}
858
+ </Label>
859
+ <Input
860
+ id={`cred-${f.key}`}
861
+ type={f.secret ? 'password' : 'text'}
862
+ autoComplete="off"
863
+ value={form.credentials[f.key] ?? ''}
864
+ onChange={(event) => setCredential(f.key, event.target.value)}
865
+ placeholder={f.placeholder}
866
+ />
867
+ </div>
868
+ ))}
861
869
 
862
870
  <div className="grid grid-cols-2 gap-4">
863
871
  <div className="grid gap-2">
@@ -869,7 +877,7 @@ export default function TrackingAuthConfigPage() {
869
877
  max={10000}
870
878
  value={form.rateLimitRequests}
871
879
  onChange={(event) =>
872
- setForm((prev) => ({ ...prev, rateLimitRequests: Number(event.target.value) || 60 }))
880
+ setField('rateLimitRequests', Number(event.target.value) || 60)
873
881
  }
874
882
  />
875
883
  </div>
@@ -882,7 +890,7 @@ export default function TrackingAuthConfigPage() {
882
890
  max={86400}
883
891
  value={form.rateLimitWindowSeconds}
884
892
  onChange={(event) =>
885
- setForm((prev) => ({ ...prev, rateLimitWindowSeconds: Number(event.target.value) || 60 }))
893
+ setField('rateLimitWindowSeconds', Number(event.target.value) || 60)
886
894
  }
887
895
  />
888
896
  </div>
@@ -892,9 +900,7 @@ export default function TrackingAuthConfigPage() {
892
900
  <Checkbox
893
901
  id="isActive"
894
902
  checked={form.isActive}
895
- onCheckedChange={(checked) =>
896
- setForm((prev) => ({ ...prev, isActive: checked === true }))
897
- }
903
+ onCheckedChange={(checked) => setField('isActive', checked === true)}
898
904
  />
899
905
  <Label htmlFor="isActive" className="cursor-pointer">
900
906
  {t('shipment_tracking.carrier_configs.fields.isActive', 'Active')}
@@ -68,9 +68,21 @@
68
68
  "shipment_tracking.carrier_configs.flash.updateFailed": "Nie udało się zaktualizować konfiguracji przewoźnika",
69
69
  "shipment_tracking.carrier_configs.flash.updated": "Konfiguracja przewoźnika została zaktualizowana",
70
70
  "shipment_tracking.carrier_configs.title": "Konfiguracja przewoźników",
71
+ "shipment_tracking.carrier_configs.cred.api_key": "Klucz API",
72
+ "shipment_tracking.carrier_configs.cred.app_key": "App key",
73
+ "shipment_tracking.carrier_configs.cred.certificate_base64": "Certyfikat (base64 PFX)",
74
+ "shipment_tracking.carrier_configs.cred.client_id": "Client ID",
75
+ "shipment_tracking.carrier_configs.cred.client_secret": "Client secret",
76
+ "shipment_tracking.carrier_configs.cred.customer_id": "Customer ID",
77
+ "shipment_tracking.carrier_configs.cred.scac_code": "Kod SCAC",
78
+ "shipment_tracking.carrier_configs.cred.subscription_key": "Subscription key",
79
+ "shipment_tracking.carrier_configs.cred.token_url": "Adres URL tokenu",
80
+ "shipment_tracking.carrier_configs.hints.endpointRequired": "Ten przewoźnik wymaga jawnego adresu endpointu API.",
71
81
  "shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl": "Endpoint API musi być poprawnym adresem URL",
72
82
  "shipment_tracking.carrier_configs.validation.authConfigInvalidJson": "Konfiguracja autoryzacji musi być poprawnym JSON",
73
- "shipment_tracking.carrier_configs.validation.carrierCodeRequired": "Kod przewoźnika jest wymagany",
83
+ "shipment_tracking.carrier_configs.validation.carrierCodeRequired": "Przewoźnik jest wymagany",
84
+ "shipment_tracking.carrier_configs.validation.credentialsRequired": "Uzupełnij wszystkie pola danych uwierzytelniających",
85
+ "shipment_tracking.carrier_configs.validation.endpointRequired": "Dla tego przewoźnika adres endpointu API jest wymagany",
74
86
  "shipment_tracking.details.bookingDetails": "Szczegóły rezerwacji",
75
87
  "shipment_tracking.details.coordinates": "Współrzędne",
76
88
  "shipment_tracking.details.currentPosition": "Aktualna pozycja",