@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.
- package/dist/modules/shipment_tracking/backend/carrier-configs/page.js +154 -175
- 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 +199 -193
- 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,70 +111,31 @@ function mapItem(item) {
|
|
|
43
111
|
createdAt: item.createdAt ?? null
|
|
44
112
|
};
|
|
45
113
|
}
|
|
46
|
-
|
|
47
|
-
carrierCode
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (
|
|
477
|
+
if (endpoint) {
|
|
432
478
|
try {
|
|
433
|
-
new URL(
|
|
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
|
|
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
|
-
|
|
446
|
-
|
|
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 (
|
|
464
|
-
if (
|
|
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
|
|
570
|
-
/* @__PURE__ */ jsx(
|
|
571
|
-
|
|
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) =>
|
|
576
|
-
|
|
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) =>
|
|
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__ */
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
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
|
-
|
|
626
|
-
|
|
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) =>
|
|
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) =>
|
|
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) =>
|
|
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": "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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,93 +158,41 @@ function mapItem(item: Record<string, unknown>): CarrierConfigRow | null {
|
|
|
63
158
|
type FormState = {
|
|
64
159
|
carrierCode: string
|
|
65
160
|
apiEndpoint: string
|
|
66
|
-
|
|
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
|
-
|
|
74
|
-
carrierCode
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
623
|
+
flash(t('shipment_tracking.carrier_configs.validation.carrierCodeRequired', 'Carrier is required'), 'error')
|
|
569
624
|
return
|
|
570
625
|
}
|
|
571
626
|
|
|
572
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
589
|
-
|
|
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 (
|
|
608
|
-
if (
|
|
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
|
|
747
|
-
|
|
748
|
-
id="carrierCode"
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
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) =>
|
|
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
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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",
|