@freighttech/shipment-tracking 0.6.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.
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.js +150 -50
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.js.map +2 -2
- package/dist/modules/shipment_tracking/i18n/pl.json +13 -1
- package/package.json +1 -1
- package/src/modules/shipment_tracking/backend/carrier-configs/page.tsx +197 -55
- package/src/modules/shipment_tracking/i18n/pl.json +13 -1
|
@@ -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,19 +111,26 @@ function mapItem(item) {
|
|
|
43
111
|
createdAt: item.createdAt ?? null
|
|
44
112
|
};
|
|
45
113
|
}
|
|
46
|
-
|
|
47
|
-
carrierCode
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const emptyForm = formForCarrier(CARRIER_SPECS[0].value);
|
|
54
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);
|
|
55
130
|
return {
|
|
56
131
|
carrierCode: row.carrierCode,
|
|
57
|
-
apiEndpoint: row.apiEndpoint ?? "",
|
|
58
|
-
|
|
132
|
+
apiEndpoint: row.apiEndpoint ?? specFor(row.carrierCode)?.defaultEndpoint ?? "",
|
|
133
|
+
credentials,
|
|
59
134
|
rateLimitRequests: row.rateLimitRequests,
|
|
60
135
|
rateLimitWindowSeconds: row.rateLimitWindowSeconds,
|
|
61
136
|
isActive: row.isActive
|
|
@@ -297,14 +372,25 @@ function TrackingAuthConfigPage() {
|
|
|
297
372
|
const [form, setForm] = useState(emptyForm);
|
|
298
373
|
const [submitting, setSubmitting] = useState(false);
|
|
299
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)), []);
|
|
300
386
|
const columns = useMemo(
|
|
301
387
|
() => [
|
|
302
388
|
{
|
|
303
389
|
data: "carrierCode",
|
|
304
|
-
title: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier
|
|
390
|
+
title: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier"),
|
|
305
391
|
width: 150,
|
|
306
392
|
readOnly: true,
|
|
307
|
-
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 ?? "") })
|
|
308
394
|
},
|
|
309
395
|
{
|
|
310
396
|
data: "apiEndpoint",
|
|
@@ -379,33 +465,44 @@ function TrackingAuthConfigPage() {
|
|
|
379
465
|
const handleSubmit = useCallback(async () => {
|
|
380
466
|
if (submitting) return;
|
|
381
467
|
if (!isEdit && !form.carrierCode.trim()) {
|
|
382
|
-
flash(t("shipment_tracking.carrier_configs.validation.carrierCodeRequired", "Carrier
|
|
468
|
+
flash(t("shipment_tracking.carrier_configs.validation.carrierCodeRequired", "Carrier is required"), "error");
|
|
383
469
|
return;
|
|
384
470
|
}
|
|
385
|
-
|
|
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");
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (endpoint) {
|
|
386
478
|
try {
|
|
387
|
-
|
|
479
|
+
new URL(endpoint);
|
|
388
480
|
} catch {
|
|
389
|
-
flash(t("shipment_tracking.carrier_configs.validation.
|
|
481
|
+
flash(t("shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl", "API endpoint must be a valid URL"), "error");
|
|
390
482
|
return;
|
|
391
483
|
}
|
|
392
484
|
}
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
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
|
+
);
|
|
398
495
|
return;
|
|
399
496
|
}
|
|
497
|
+
authConfig[f.key] = val;
|
|
400
498
|
}
|
|
401
499
|
setSubmitting(true);
|
|
402
500
|
try {
|
|
403
|
-
const
|
|
501
|
+
const hasAuth = Object.keys(authConfig).length > 0;
|
|
404
502
|
if (isEdit) {
|
|
405
503
|
const body = { id: editingId };
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
body.authConfig = authConfig ?? null;
|
|
504
|
+
body.apiEndpoint = endpoint || null;
|
|
505
|
+
body.authConfig = hasAuth ? authConfig : null;
|
|
409
506
|
body.rateLimitRequests = form.rateLimitRequests;
|
|
410
507
|
body.rateLimitWindowSeconds = form.rateLimitWindowSeconds;
|
|
411
508
|
body.isActive = form.isActive;
|
|
@@ -421,8 +518,8 @@ function TrackingAuthConfigPage() {
|
|
|
421
518
|
rateLimitWindowSeconds: form.rateLimitWindowSeconds,
|
|
422
519
|
isActive: form.isActive
|
|
423
520
|
};
|
|
424
|
-
if (
|
|
425
|
-
if (
|
|
521
|
+
if (endpoint) body.apiEndpoint = endpoint;
|
|
522
|
+
if (hasAuth) body.authConfig = authConfig;
|
|
426
523
|
await apiCallOrThrow("/api/shipment_tracking/carrier-configs", {
|
|
427
524
|
method: "POST",
|
|
428
525
|
body: JSON.stringify(body)
|
|
@@ -527,16 +624,15 @@ function TrackingAuthConfigPage() {
|
|
|
527
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") }) }),
|
|
528
625
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-4 py-2", children: [
|
|
529
626
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
530
|
-
/* @__PURE__ */ jsx(Label, { htmlFor: "carrierCode", children: t("shipment_tracking.carrier_configs.fields.carrierCode", "Carrier
|
|
531
|
-
/* @__PURE__ */ jsx(
|
|
532
|
-
|
|
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",
|
|
533
630
|
{
|
|
534
631
|
id: "carrierCode",
|
|
632
|
+
className: "border rounded h-9 px-2 text-sm",
|
|
535
633
|
value: form.carrierCode,
|
|
536
|
-
onChange: (event) =>
|
|
537
|
-
|
|
538
|
-
disabled: isEdit,
|
|
539
|
-
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))
|
|
540
636
|
}
|
|
541
637
|
)
|
|
542
638
|
] }),
|
|
@@ -547,25 +643,29 @@ function TrackingAuthConfigPage() {
|
|
|
547
643
|
{
|
|
548
644
|
id: "apiEndpoint",
|
|
549
645
|
value: form.apiEndpoint,
|
|
550
|
-
onChange: (event) =>
|
|
646
|
+
onChange: (event) => setField("apiEndpoint", event.target.value),
|
|
551
647
|
placeholder: "https://api.example.com/tracking"
|
|
552
648
|
}
|
|
553
|
-
)
|
|
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.") })
|
|
554
651
|
] }),
|
|
555
|
-
/* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
556
|
-
/* @__PURE__ */
|
|
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
|
+
] }),
|
|
557
657
|
/* @__PURE__ */ jsx(
|
|
558
|
-
|
|
658
|
+
Input,
|
|
559
659
|
{
|
|
560
|
-
id:
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
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
|
|
566
666
|
}
|
|
567
667
|
)
|
|
568
|
-
] }),
|
|
668
|
+
] }, f.key)),
|
|
569
669
|
/* @__PURE__ */ jsxs("div", { className: "grid grid-cols-2 gap-4", children: [
|
|
570
670
|
/* @__PURE__ */ jsxs("div", { className: "grid gap-2", children: [
|
|
571
671
|
/* @__PURE__ */ jsx(Label, { htmlFor: "rateLimitRequests", children: t("shipment_tracking.carrier_configs.fields.rateLimitRequests", "Rate Limit (requests)") }),
|
|
@@ -577,7 +677,7 @@ function TrackingAuthConfigPage() {
|
|
|
577
677
|
min: 1,
|
|
578
678
|
max: 1e4,
|
|
579
679
|
value: form.rateLimitRequests,
|
|
580
|
-
onChange: (event) =>
|
|
680
|
+
onChange: (event) => setField("rateLimitRequests", Number(event.target.value) || 60)
|
|
581
681
|
}
|
|
582
682
|
)
|
|
583
683
|
] }),
|
|
@@ -591,7 +691,7 @@ function TrackingAuthConfigPage() {
|
|
|
591
691
|
min: 1,
|
|
592
692
|
max: 86400,
|
|
593
693
|
value: form.rateLimitWindowSeconds,
|
|
594
|
-
onChange: (event) =>
|
|
694
|
+
onChange: (event) => setField("rateLimitWindowSeconds", Number(event.target.value) || 60)
|
|
595
695
|
}
|
|
596
696
|
)
|
|
597
697
|
] })
|
|
@@ -602,7 +702,7 @@ function TrackingAuthConfigPage() {
|
|
|
602
702
|
{
|
|
603
703
|
id: "isActive",
|
|
604
704
|
checked: form.isActive,
|
|
605
|
-
onCheckedChange: (checked) =>
|
|
705
|
+
onCheckedChange: (checked) => setField("isActive", checked === true)
|
|
606
706
|
}
|
|
607
707
|
),
|
|
608
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: string\n rateLimitRequests: number\n rateLimitWindowSeconds: number\n isActive: boolean\n}\n\nconst emptyForm: FormState = {\n carrierCode: '',\n apiEndpoint: '',\n authConfig: '',\n rateLimitRequests: 60,\n rateLimitWindowSeconds: 60,\n isActive: true,\n}\n\nfunction rowToForm(row: CarrierConfigRow): FormState {\n return {\n carrierCode: row.carrierCode,\n apiEndpoint: row.apiEndpoint ?? '',\n authConfig: row.authConfig ? JSON.stringify(row.authConfig, null, 2) : '',\n rateLimitRequests: row.rateLimitRequests,\n rateLimitWindowSeconds: row.rateLimitWindowSeconds,\n isActive: row.isActive,\n }\n}\n\n// \u2500\u2500\u2500 BIC Config Types \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\ntype BicConfigData = {\n id: string\n organizationId: string\n tenantId: string\n isEnabled: boolean\n username: string\n baseUrl: string\n createdAt: string\n updatedAt: string\n} | null\n\ntype BicFormState = {\n isEnabled: boolean\n username: string\n password: string\n baseUrl: string\n}\n\nconst emptyBicForm: BicFormState = {\n isEnabled: false,\n username: '',\n password: '',\n baseUrl: 'https://api.bic-code.org',\n}\n\n// \u2500\u2500\u2500 BIC Config Section \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nfunction BicConfigSection() {\n const t = useT()\n const [config, setConfig] = React.useState<BicConfigData>(null)\n const [form, setForm] = React.useState<BicFormState>(emptyBicForm)\n const [loading, setLoading] = React.useState(true)\n const [saving, setSaving] = React.useState(false)\n const [testing, setTesting] = React.useState(false)\n const [testResult, setTestResult] = React.useState<{ success: boolean; message: string } | null>(null)\n\n const fetchConfig = React.useCallback(async () => {\n setLoading(true)\n try {\n const { result } = await apiCallOrThrow<{ config: BicConfigData }>('/api/shipment_tracking/bic-configs')\n setConfig(result?.config ?? null)\n if (result?.config) {\n setForm({\n isEnabled: result.config.isEnabled,\n username: result.config.username,\n password: '', // Password is never returned from API\n baseUrl: result.config.baseUrl,\n })\n } else {\n setForm(emptyBicForm)\n }\n } catch {\n // Config doesn't exist yet, that's fine\n setConfig(null)\n setForm(emptyBicForm)\n } finally {\n setLoading(false)\n }\n }, [])\n\n React.useEffect(() => {\n fetchConfig()\n }, [fetchConfig])\n\n const handleTest = React.useCallback(async () => {\n if (!form.username || !form.password) {\n flash(t('shipment_tracking.bic_config.validation.usernameRequired', 'Username is required'), 'error')\n return\n }\n\n setTesting(true)\n setTestResult(null)\n\n try {\n const { result } = await apiCallOrThrow<{ success: boolean; message: string }>(\n '/api/shipment_tracking/bic-configs/test',\n {\n method: 'POST',\n body: JSON.stringify({\n username: form.username,\n password: form.password,\n baseUrl: form.baseUrl,\n }),\n }\n )\n\n setTestResult(result ?? { success: false, message: 'Unknown error' })\n\n if (result?.success) {\n flash(t('shipment_tracking.bic_config.flash.testSuccess', 'BIC API connection successful'), 'success')\n } else {\n flash(result?.message ?? t('shipment_tracking.bic_config.flash.testFailed', 'BIC API connection failed'), 'error')\n }\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Connection test failed'\n setTestResult({ success: false, message })\n flash(message, 'error')\n } finally {\n setTesting(false)\n }\n }, [form, t])\n\n const handleSave = React.useCallback(async () => {\n if (!form.username) {\n flash(t('shipment_tracking.bic_config.validation.usernameRequired', 'Username is required'), 'error')\n return\n }\n\n // Password is required for new config or when enabling\n if (!config && !form.password) {\n flash(t('shipment_tracking.bic_config.validation.passwordRequired', 'Password is required'), 'error')\n return\n }\n\n setSaving(true)\n try {\n const body: Record<string, unknown> = {\n isEnabled: form.isEnabled,\n username: form.username,\n baseUrl: form.baseUrl || 'https://api.bic-code.org',\n }\n\n // Only include password if it was changed\n if (form.password) {\n body.password = form.password\n } else if (config) {\n // If editing and no new password, we still need to send something\n // The API should handle this gracefully\n body.password = '__UNCHANGED__'\n }\n\n await apiCallOrThrow('/api/shipment_tracking/bic-configs', {\n method: 'PUT',\n body: JSON.stringify(body),\n })\n\n flash(t('shipment_tracking.bic_config.flash.saved', 'BIC configuration saved'), 'success')\n fetchConfig()\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Failed to save'\n flash(message, 'error')\n } finally {\n setSaving(false)\n }\n }, [form, config, t, fetchConfig])\n\n const handleKeyDown = React.useCallback(\n (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSave()\n }\n },\n [handleSave]\n )\n\n if (loading) {\n return (\n <Card>\n <CardHeader>\n <CardTitle className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5\" />\n {t('shipment_tracking.bic_config.title', 'BIC Facility API')}\n </CardTitle>\n </CardHeader>\n <CardContent className=\"flex items-center justify-center py-8\">\n <Loader2 className=\"h-6 w-6 animate-spin text-muted-foreground\" />\n </CardContent>\n </Card>\n )\n }\n\n return (\n <Card>\n <CardHeader>\n <div className=\"flex items-center justify-between\">\n <div className=\"flex items-center gap-2\">\n <Shield className=\"h-5 w-5\" />\n <CardTitle>{t('shipment_tracking.bic_config.title', 'BIC Facility API')}</CardTitle>\n </div>\n <div className=\"flex items-center gap-2\">\n {config ? (\n <>\n <Badge variant=\"outline\">\n {t('shipment_tracking.bic_config.status.configured', 'Configured')}\n </Badge>\n <Badge variant={config.isEnabled ? 'default' : 'secondary'}>\n {config.isEnabled\n ? t('shipment_tracking.bic_config.status.enabled', 'Enabled')\n : t('shipment_tracking.bic_config.status.disabled', 'Disabled')}\n </Badge>\n </>\n ) : (\n <Badge variant=\"secondary\">\n {t('shipment_tracking.bic_config.status.notConfigured', 'Not Configured')}\n </Badge>\n )}\n </div>\n </div>\n <CardDescription>\n {t(\n 'shipment_tracking.bic_config.description',\n 'Configure BIC Facility Code API credentials to enrich shipment tracking locations with coordinates, addresses, and terminal operator information.'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent onKeyDown={handleKeyDown}>\n <div className=\"grid gap-4\">\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"bicEnabled\"\n checked={form.isEnabled}\n onCheckedChange={(checked) =>\n setForm((prev) => ({ ...prev, isEnabled: checked === true }))\n }\n />\n <Label htmlFor=\"bicEnabled\" className=\"cursor-pointer font-medium\">\n {t('shipment_tracking.bic_config.fields.isEnabled', 'Enable BIC Enrichment')}\n </Label>\n </div>\n\n <div className=\"grid gap-4 md:grid-cols-2\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicUsername\">\n {t('shipment_tracking.bic_config.fields.username', 'Username')}\n </Label>\n <Input\n id=\"bicUsername\"\n value={form.username}\n onChange={(e) => setForm((prev) => ({ ...prev, username: e.target.value }))}\n placeholder=\"your-bic-username\"\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicPassword\">\n {t('shipment_tracking.bic_config.fields.password', 'Password')}\n </Label>\n <Input\n id=\"bicPassword\"\n type=\"password\"\n value={form.password}\n onChange={(e) => setForm((prev) => ({ ...prev, password: e.target.value }))}\n placeholder={config ? '\u2022\u2022\u2022\u2022\u2022\u2022\u2022\u2022' : 'your-bic-password'}\n />\n <p className=\"text-xs text-muted-foreground\">\n {t(\n 'shipment_tracking.bic_config.hints.password',\n 'Your password will be encrypted and stored securely.'\n )}\n </p>\n </div>\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"bicBaseUrl\">\n {t('shipment_tracking.bic_config.fields.baseUrl', 'API Base URL')}\n </Label>\n <Input\n id=\"bicBaseUrl\"\n value={form.baseUrl}\n onChange={(e) => setForm((prev) => ({ ...prev, baseUrl: e.target.value }))}\n placeholder=\"https://api.bic-code.org\"\n />\n <p className=\"text-xs text-muted-foreground\">\n {t('shipment_tracking.bic_config.hints.baseUrl', 'Default: https://api.bic-code.org')}\n </p>\n </div>\n\n {testResult && (\n <div\n className={`flex items-center gap-2 rounded-md p-3 text-sm ${\n testResult.success\n ? 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'\n : 'bg-red-50 text-red-700 dark:bg-red-900/20 dark:text-red-400'\n }`}\n >\n {testResult.success ? (\n <CheckCircle2 className=\"h-4 w-4\" />\n ) : (\n <XCircle className=\"h-4 w-4\" />\n )}\n <span>{testResult.message}</span>\n </div>\n )}\n\n <div className=\"flex items-center gap-2 pt-2\">\n <Button\n variant=\"outline\"\n onClick={handleTest}\n disabled={testing || !form.username || !form.password}\n >\n {testing && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {t('shipment_tracking.bic_config.actions.testConnection', 'Test Connection')}\n </Button>\n <Button onClick={handleSave} disabled={saving}>\n {saving && <Loader2 className=\"mr-2 h-4 w-4 animate-spin\" />}\n {t('shipment_tracking.bic_config.actions.save', 'Save Configuration')}\n </Button>\n </div>\n </div>\n </CardContent>\n </Card>\n )\n}\n\n// \u2500\u2500\u2500 Main Page \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nexport default function TrackingAuthConfigPage() {\n const t = useT()\n\n const [dialogOpen, setDialogOpen] = useState(false)\n const [editingId, setEditingId] = useState<string | null>(null)\n const [form, setForm] = useState<FormState>(emptyForm)\n const [submitting, setSubmitting] = useState(false)\n\n const isEdit = editingId !== null\n\n const columns = useMemo<ColumnDef[]>(\n () => [\n {\n data: 'carrierCode',\n title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code'),\n width: 150,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"font-medium\">{String(value ?? '')}</span>\n ),\n },\n {\n data: 'apiEndpoint',\n title: t('shipment_tracking.carrier_configs.fields.apiEndpoint', 'API Endpoint'),\n width: 280,\n readOnly: true,\n renderer: (value: unknown) => (\n <span className=\"text-sm truncate max-w-[260px] block\">{String(value || '-')}</span>\n ),\n },\n {\n data: 'rateLimit',\n title: t('shipment_tracking.carrier_configs.fields.rateLimit', 'Rate Limit'),\n width: 120,\n readOnly: true,\n renderer: (_value: unknown, rowData: Record<string, unknown>) => (\n <span className=\"text-sm\">\n {String(rowData.rateLimitRequests ?? 60)} / {String(rowData.rateLimitWindowSeconds ?? 60)}s\n </span>\n ),\n },\n {\n data: 'isActive',\n title: t('shipment_tracking.carrier_configs.fields.isActive', 'Active'),\n width: 80,\n readOnly: true,\n renderer: (value: unknown) => <BooleanIcon value={value === true || value === 'true'} />,\n },\n ],\n [t],\n )\n\n const openCreateDialog = useCallback(() => {\n setEditingId(null)\n setForm(emptyForm)\n setDialogOpen(true)\n }, [])\n\n const addButton = useMemo(\n () => (\n <ButtonV2 variant=\"primary\" size=\"sm\" icon={<Plus className=\"h-4 w-4\" />} onClick={openCreateDialog}>\n {t('shipment_tracking.carrier_configs.create', 'Add Carrier Config')}\n </ButtonV2>\n ),\n [openCreateDialog, t],\n )\n\n const table = useDynamicTablePage<CarrierConfigRow>({\n source: '/api/shipment_tracking/carrier-configs',\n // Saved views. Without this the perspective tab strip auto-hides\n // (hidePerspectiveTabs ?? !hasPerspectives), so users cannot save or\n // switch a view on this list at all.\n perspectives: 'shipment_tracking_carrier_configs',\n columns,\n tableName: t('shipment_tracking.carrier_configs.title', 'Carrier Configs'),\n defaultPageSize: 20,\n mapApiItem: mapItem,\n cellEdit: false,\n tableProps: {\n height: '100%',\n uiConfig: {\n readOnlyStyle: 'normal',\n hideAddRowButton: true,\n searchBarEnd: addButton,\n },\n },\n })\n\n const openEditDialog = useCallback(\n (id: string) => {\n const items = (table.query.data as { items?: Record<string, unknown>[] })?.items ?? []\n const mapped = items.map(mapItem).filter((x): x is CarrierConfigRow => x !== null)\n const row = mapped.find((r) => r.id === id)\n if (!row) return\n setEditingId(id)\n setForm(rowToForm(row))\n setDialogOpen(true)\n },\n [table.query.data],\n )\n\n const handleSubmit = useCallback(async () => {\n if (submitting) return\n\n if (!isEdit && !form.carrierCode.trim()) {\n flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier code is required'), 'error')\n return\n }\n\n if (form.authConfig.trim()) {\n try {\n JSON.parse(form.authConfig)\n } catch {\n flash(t('shipment_tracking.carrier_configs.validation.authConfigInvalidJson', 'Auth config must be valid JSON'), 'error')\n return\n }\n }\n\n if (form.apiEndpoint.trim()) {\n try {\n new URL(form.apiEndpoint.trim())\n } catch {\n flash(t('shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl', 'API endpoint must be a valid URL'), 'error')\n return\n }\n }\n\n setSubmitting(true)\n try {\n const authConfig = form.authConfig.trim() ? JSON.parse(form.authConfig) : undefined\n\n if (isEdit) {\n const body: Record<string, unknown> = { id: editingId }\n if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()\n else body.apiEndpoint = null\n body.authConfig = authConfig ?? null\n body.rateLimitRequests = form.rateLimitRequests\n body.rateLimitWindowSeconds = form.rateLimitWindowSeconds\n body.isActive = form.isActive\n\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'PUT',\n body: JSON.stringify(body),\n })\n flash(t('shipment_tracking.carrier_configs.flash.updated', 'Carrier config updated'), 'success')\n } else {\n const body: Record<string, unknown> = {\n carrierCode: form.carrierCode.trim(),\n rateLimitRequests: form.rateLimitRequests,\n rateLimitWindowSeconds: form.rateLimitWindowSeconds,\n isActive: form.isActive,\n }\n if (form.apiEndpoint.trim()) body.apiEndpoint = form.apiEndpoint.trim()\n if (authConfig) body.authConfig = authConfig\n\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'POST',\n body: JSON.stringify(body),\n })\n flash(t('shipment_tracking.carrier_configs.flash.created', 'Carrier config created'), 'success')\n }\n\n setDialogOpen(false)\n table.refresh()\n } catch {\n flash(isEdit ? t('shipment_tracking.carrier_configs.flash.updateFailed', 'Failed to update carrier config') : t('shipment_tracking.carrier_configs.flash.createFailed', 'Failed to create carrier config'), 'error')\n } finally {\n setSubmitting(false)\n }\n }, [form, isEdit, editingId, submitting, table.refresh, t])\n\n const handleDialogKeyDown = useCallback(\n (event: React.KeyboardEvent) => {\n if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {\n event.preventDefault()\n handleSubmit()\n }\n },\n [handleSubmit],\n )\n\n const handleDelete = useCallback(\n async (id: string) => {\n if (!confirm(t('shipment_tracking.carrier_configs.confirmDelete', 'Delete this carrier config?'))) return\n\n try {\n await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {\n method: 'DELETE',\n body: JSON.stringify({ id }),\n })\n flash(t('shipment_tracking.carrier_configs.flash.deleted', 'Carrier config deleted'), 'success')\n table.refresh()\n } catch {\n flash(t('shipment_tracking.carrier_configs.flash.deleteFailed', 'Failed to delete carrier config'), 'error')\n }\n },\n [table.refresh, t],\n )\n\n const rowActions = useCallback(\n (rowData: any): ContextMenuAction[] => {\n if (!rowData?.id) return []\n return [\n { id: 'edit', label: t('common.edit', 'Edit') },\n { id: 'delete', label: t('common.delete', 'Delete') },\n ]\n },\n [t],\n )\n\n const handleRowAction = useCallback(\n (actionId: string, rowData: any) => {\n const id = rowData?.id as string | undefined\n if (!id) return\n if (actionId === 'edit') openEditDialog(id)\n else if (actionId === 'delete') handleDelete(id)\n },\n [openEditDialog, handleDelete],\n )\n\n const tableHeight = useMemo(() => {\n const rowH = 40\n const headerH = 40\n const toolbarH = 50\n const minHeight = 200\n const maxHeight = 400\n const rowCount = (table.query.data as { total?: number })?.total ?? 0\n const contentHeight = toolbarH + headerH + rowCount * rowH + 20\n return Math.min(Math.max(contentHeight, minHeight), maxHeight)\n }, [table.query.data])\n\n if (table.isLoading) {\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n <BicConfigSection />\n <ShipsGoConfigSection />\n <Card>\n <CardHeader>\n <CardTitle>{t('shipment_tracking.carrier_configs.title', 'Carrier Configs')}</CardTitle>\n </CardHeader>\n <CardContent>\n <TableSkeleton rows={5} columns={4} />\n </CardContent>\n </Card>\n </PageBody>\n </Page>\n )\n }\n\n return (\n <Page>\n <PageBody className=\"space-y-6\">\n {/* BIC Config Section */}\n <BicConfigSection />\n\n {/* ShipsGo Aggregator Config Section */}\n <ShipsGoConfigSection />\n\n {/* Carrier Configs Section */}\n <Card>\n <CardHeader>\n <CardTitle>{t('shipment_tracking.carrier_configs.title', 'Carrier Configs')}</CardTitle>\n <CardDescription>\n {t(\n 'shipment_tracking.auth_config.description',\n 'Configure authentication for carrier tracking APIs and facility enrichment services.'\n )}\n </CardDescription>\n </CardHeader>\n <CardContent>\n <div style={{ height: tableHeight }}>\n <DynamicTable\n {...table.props}\n striped\n density=\"md\"\n rowActions={rowActions}\n onRowAction={handleRowAction}\n />\n </div>\n </CardContent>\n </Card>\n\n <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>\n <DialogContent className=\"sm:max-w-lg\" onKeyDown={handleDialogKeyDown}>\n <DialogHeader>\n <DialogTitle>{isEdit ? t('shipment_tracking.carrier_configs.edit', 'Edit Carrier Config') : t('shipment_tracking.carrier_configs.create', 'Add Carrier Config')}</DialogTitle>\n </DialogHeader>\n\n <div className=\"grid gap-4 py-2\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"carrierCode\">{t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier Code')}</Label>\n <Input\n id=\"carrierCode\"\n value={form.carrierCode}\n onChange={(event) => setForm((prev) => ({ ...prev, carrierCode: event.target.value }))}\n placeholder=\"e.g. maersk, msc, cma-cgm\"\n disabled={isEdit}\n autoFocus={!isEdit}\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"apiEndpoint\">{t('shipment_tracking.carrier_configs.fields.apiEndpoint', 'API Endpoint')}</Label>\n <Input\n id=\"apiEndpoint\"\n value={form.apiEndpoint}\n onChange={(event) => setForm((prev) => ({ ...prev, apiEndpoint: event.target.value }))}\n placeholder=\"https://api.example.com/tracking\"\n />\n </div>\n\n <div className=\"grid gap-2\">\n <Label htmlFor=\"authConfig\">{t('shipment_tracking.carrier_configs.fields.authConfig', 'Auth Config (JSON)')}</Label>\n <Textarea\n id=\"authConfig\"\n value={form.authConfig}\n onChange={(event) => setForm((prev) => ({ ...prev, authConfig: event.target.value }))}\n placeholder='{\"apiKey\": \"...\"}'\n rows={3}\n className=\"font-mono text-xs\"\n />\n </div>\n\n <div className=\"grid grid-cols-2 gap-4\">\n <div className=\"grid gap-2\">\n <Label htmlFor=\"rateLimitRequests\">{t('shipment_tracking.carrier_configs.fields.rateLimitRequests', 'Rate Limit (requests)')}</Label>\n <Input\n id=\"rateLimitRequests\"\n type=\"number\"\n min={1}\n max={10000}\n value={form.rateLimitRequests}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, rateLimitRequests: Number(event.target.value) || 60 }))\n }\n />\n </div>\n <div className=\"grid gap-2\">\n <Label htmlFor=\"rateLimitWindowSeconds\">{t('shipment_tracking.carrier_configs.fields.rateLimitWindowSeconds', 'Window (seconds)')}</Label>\n <Input\n id=\"rateLimitWindowSeconds\"\n type=\"number\"\n min={1}\n max={86400}\n value={form.rateLimitWindowSeconds}\n onChange={(event) =>\n setForm((prev) => ({ ...prev, rateLimitWindowSeconds: Number(event.target.value) || 60 }))\n }\n />\n </div>\n </div>\n\n <div className=\"flex items-center gap-2\">\n <Checkbox\n id=\"isActive\"\n checked={form.isActive}\n onCheckedChange={(checked) =>\n setForm((prev) => ({ ...prev, isActive: checked === true }))\n }\n />\n <Label htmlFor=\"isActive\" className=\"cursor-pointer\">\n {t('shipment_tracking.carrier_configs.fields.isActive', 'Active')}\n </Label>\n </div>\n </div>\n\n <DialogFooter>\n <Button variant=\"outline\" onClick={() => setDialogOpen(false)} disabled={submitting}>\n {t('common.cancel', 'Cancel')}\n </Button>\n <Button onClick={handleSubmit} disabled={submitting}>\n {submitting ? t('common.saving', 'Saving...') : isEdit ? t('common.save', 'Save') : t('common.create', 'Create')}\n </Button>\n </DialogFooter>\n </DialogContent>\n </Dialog>\n </PageBody>\n </Page>\n )\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["'use client'\n\nimport * as React from 'react'\nimport { useCallback, useMemo, useState } from 'react'\nimport { Page, PageBody } from '@freighttech/ui/backend/Page'\nimport { BooleanIcon } from '@freighttech/ui/backend/ValueIcons'\nimport {\n DynamicTable,\n TableSkeleton,\n useDynamicTablePage,\n} from '@freighttech/ui/backend/dynamic-table'\nimport type { ColumnDef, ContextMenuAction } from '@freighttech/ui/backend/dynamic-table'\nimport { apiCallOrThrow } from '@freighttech/ui/backend/utils/apiCall'\nimport { flash } from '@freighttech/ui/backend/FlashMessages'\nimport { useT } from '@open-mercato/shared/lib/i18n/context'\nimport {\n Dialog,\n DialogContent,\n DialogHeader,\n DialogTitle,\n DialogFooter,\n} from '@freighttech/ui/primitives/dialog'\nimport { Button } from '@freighttech/ui/primitives/button'\nimport { Button as ButtonV2 } from '@freighttech/ui/primitives-v2'\nimport { Input } from '@freighttech/ui/primitives/input'\nimport { Label } from '@freighttech/ui/primitives/label'\nimport { Checkbox } from '@freighttech/ui/primitives/checkbox'\nimport { 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": "
|
|
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
|
@@ -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,26 +158,35 @@ function mapItem(item: Record<string, unknown>): CarrierConfigRow | null {
|
|
|
63
158
|
type FormState = {
|
|
64
159
|
carrierCode: string
|
|
65
160
|
apiEndpoint: string
|
|
66
|
-
|
|
161
|
+
credentials: Record<string, string>
|
|
67
162
|
rateLimitRequests: number
|
|
68
163
|
rateLimitWindowSeconds: number
|
|
69
164
|
isActive: boolean
|
|
70
165
|
}
|
|
71
166
|
|
|
72
|
-
|
|
73
|
-
carrierCode
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
+
}
|
|
79
177
|
}
|
|
80
178
|
|
|
179
|
+
const emptyForm: FormState = formForCarrier(CARRIER_SPECS[0].value)
|
|
180
|
+
|
|
81
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)
|
|
82
186
|
return {
|
|
83
187
|
carrierCode: row.carrierCode,
|
|
84
|
-
apiEndpoint: row.apiEndpoint ?? '',
|
|
85
|
-
|
|
188
|
+
apiEndpoint: row.apiEndpoint ?? specFor(row.carrierCode)?.defaultEndpoint ?? '',
|
|
189
|
+
credentials,
|
|
86
190
|
rateLimitRequests: row.rateLimitRequests,
|
|
87
191
|
rateLimitWindowSeconds: row.rateLimitWindowSeconds,
|
|
88
192
|
isActive: row.isActive,
|
|
@@ -408,16 +512,28 @@ export default function TrackingAuthConfigPage() {
|
|
|
408
512
|
const [submitting, setSubmitting] = useState(false)
|
|
409
513
|
|
|
410
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)), [])
|
|
411
527
|
|
|
412
528
|
const columns = useMemo<ColumnDef[]>(
|
|
413
529
|
() => [
|
|
414
530
|
{
|
|
415
531
|
data: 'carrierCode',
|
|
416
|
-
title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier
|
|
532
|
+
title: t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier'),
|
|
417
533
|
width: 150,
|
|
418
534
|
readOnly: true,
|
|
419
535
|
renderer: (value: unknown) => (
|
|
420
|
-
<span className="font-medium">{String(value ?? '')}</span>
|
|
536
|
+
<span className="font-medium">{specFor(String(value))?.label ?? String(value ?? '')}</span>
|
|
421
537
|
),
|
|
422
538
|
},
|
|
423
539
|
{
|
|
@@ -504,37 +620,48 @@ export default function TrackingAuthConfigPage() {
|
|
|
504
620
|
if (submitting) return
|
|
505
621
|
|
|
506
622
|
if (!isEdit && !form.carrierCode.trim()) {
|
|
507
|
-
flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier
|
|
623
|
+
flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier is required'), 'error')
|
|
508
624
|
return
|
|
509
625
|
}
|
|
510
626
|
|
|
511
|
-
|
|
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) {
|
|
512
634
|
try {
|
|
513
|
-
|
|
635
|
+
new URL(endpoint)
|
|
514
636
|
} catch {
|
|
515
|
-
flash(t('shipment_tracking.carrier_configs.validation.
|
|
637
|
+
flash(t('shipment_tracking.carrier_configs.validation.apiEndpointInvalidUrl', 'API endpoint must be a valid URL'), 'error')
|
|
516
638
|
return
|
|
517
639
|
}
|
|
518
640
|
}
|
|
519
641
|
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
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
|
+
)
|
|
525
653
|
return
|
|
526
654
|
}
|
|
655
|
+
authConfig[f.key] = val
|
|
527
656
|
}
|
|
528
657
|
|
|
529
658
|
setSubmitting(true)
|
|
530
659
|
try {
|
|
531
|
-
const
|
|
532
|
-
|
|
660
|
+
const hasAuth = Object.keys(authConfig).length > 0
|
|
533
661
|
if (isEdit) {
|
|
534
662
|
const body: Record<string, unknown> = { id: editingId }
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
body.authConfig = authConfig ?? null
|
|
663
|
+
body.apiEndpoint = endpoint || null
|
|
664
|
+
body.authConfig = hasAuth ? authConfig : null
|
|
538
665
|
body.rateLimitRequests = form.rateLimitRequests
|
|
539
666
|
body.rateLimitWindowSeconds = form.rateLimitWindowSeconds
|
|
540
667
|
body.isActive = form.isActive
|
|
@@ -551,8 +678,8 @@ export default function TrackingAuthConfigPage() {
|
|
|
551
678
|
rateLimitWindowSeconds: form.rateLimitWindowSeconds,
|
|
552
679
|
isActive: form.isActive,
|
|
553
680
|
}
|
|
554
|
-
if (
|
|
555
|
-
if (
|
|
681
|
+
if (endpoint) body.apiEndpoint = endpoint
|
|
682
|
+
if (hasAuth) body.authConfig = authConfig
|
|
556
683
|
|
|
557
684
|
await apiCallOrThrow('/api/shipment_tracking/carrier-configs', {
|
|
558
685
|
method: 'POST',
|
|
@@ -690,15 +817,21 @@ export default function TrackingAuthConfigPage() {
|
|
|
690
817
|
|
|
691
818
|
<div className="grid gap-4 py-2">
|
|
692
819
|
<div className="grid gap-2">
|
|
693
|
-
<Label htmlFor="carrierCode">{t('shipment_tracking.carrier_configs.fields.carrierCode', 'Carrier
|
|
694
|
-
|
|
695
|
-
id="carrierCode"
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
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
|
+
)}
|
|
702
835
|
</div>
|
|
703
836
|
|
|
704
837
|
<div className="grid gap-2">
|
|
@@ -706,22 +839,33 @@ export default function TrackingAuthConfigPage() {
|
|
|
706
839
|
<Input
|
|
707
840
|
id="apiEndpoint"
|
|
708
841
|
value={form.apiEndpoint}
|
|
709
|
-
onChange={(event) =>
|
|
842
|
+
onChange={(event) => setField('apiEndpoint', event.target.value)}
|
|
710
843
|
placeholder="https://api.example.com/tracking"
|
|
711
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
|
+
)}
|
|
712
850
|
</div>
|
|
713
851
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
<
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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
|
+
))}
|
|
725
869
|
|
|
726
870
|
<div className="grid grid-cols-2 gap-4">
|
|
727
871
|
<div className="grid gap-2">
|
|
@@ -733,7 +877,7 @@ export default function TrackingAuthConfigPage() {
|
|
|
733
877
|
max={10000}
|
|
734
878
|
value={form.rateLimitRequests}
|
|
735
879
|
onChange={(event) =>
|
|
736
|
-
|
|
880
|
+
setField('rateLimitRequests', Number(event.target.value) || 60)
|
|
737
881
|
}
|
|
738
882
|
/>
|
|
739
883
|
</div>
|
|
@@ -746,7 +890,7 @@ export default function TrackingAuthConfigPage() {
|
|
|
746
890
|
max={86400}
|
|
747
891
|
value={form.rateLimitWindowSeconds}
|
|
748
892
|
onChange={(event) =>
|
|
749
|
-
|
|
893
|
+
setField('rateLimitWindowSeconds', Number(event.target.value) || 60)
|
|
750
894
|
}
|
|
751
895
|
/>
|
|
752
896
|
</div>
|
|
@@ -756,9 +900,7 @@ export default function TrackingAuthConfigPage() {
|
|
|
756
900
|
<Checkbox
|
|
757
901
|
id="isActive"
|
|
758
902
|
checked={form.isActive}
|
|
759
|
-
onCheckedChange={(checked) =>
|
|
760
|
-
setForm((prev) => ({ ...prev, isActive: checked === true }))
|
|
761
|
-
}
|
|
903
|
+
onCheckedChange={(checked) => setField('isActive', checked === true)}
|
|
762
904
|
/>
|
|
763
905
|
<Label htmlFor="isActive" className="cursor-pointer">
|
|
764
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": "
|
|
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",
|