@freighttech/terminal-tracking 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/generated/entities/terminal_config/index.js +53 -0
- package/dist/generated/entities/terminal_config/index.js.map +7 -0
- package/dist/generated/entities/terminal_event/index.js +59 -0
- package/dist/generated/entities/terminal_event/index.js.map +7 -0
- package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
- package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +26 -0
- package/dist/generated/entities.ids.generated.js.map +7 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/terminal_tracking/acl.js +14 -0
- package/dist/modules/terminal_tracking/acl.js.map +7 -0
- package/dist/modules/terminal_tracking/api/openapi.js +27 -0
- package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/utils.js +12 -0
- package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/ce.js +35 -0
- package/dist/modules/terminal_tracking/ce.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/index.js +4 -0
- package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
- package/dist/modules/terminal_tracking/data/entities.js +337 -0
- package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
- package/dist/modules/terminal_tracking/data/validators.js +98 -0
- package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
- package/dist/modules/terminal_tracking/di.js +39 -0
- package/dist/modules/terminal_tracking/di.js.map +7 -0
- package/dist/modules/terminal_tracking/encryption.js +14 -0
- package/dist/modules/terminal_tracking/encryption.js.map +7 -0
- package/dist/modules/terminal_tracking/events.js +45 -0
- package/dist/modules/terminal_tracking/events.js.map +7 -0
- package/dist/modules/terminal_tracking/i18n/de.json +175 -0
- package/dist/modules/terminal_tracking/i18n/en.json +178 -0
- package/dist/modules/terminal_tracking/i18n/es.json +175 -0
- package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
- package/dist/modules/terminal_tracking/index.js +15 -0
- package/dist/modules/terminal_tracking/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/availability.js +40 -0
- package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/holds.js +240 -0
- package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/logger.js +19 -0
- package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/stops.js +48 -0
- package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/totp.js +41 -0
- package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
- package/dist/modules/terminal_tracking/setup.js +23 -0
- package/dist/modules/terminal_tracking/setup.js.map +7 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
- package/generated/entities/terminal_config/index.ts +36 -0
- package/generated/entities/terminal_event/index.ts +39 -0
- package/generated/entities/terminal_tracking_job/index.ts +28 -0
- package/generated/entities/terminal_vessel_visit/index.ts +30 -0
- package/generated/entities.ids.generated.ts +52 -0
- package/package.json +80 -0
- package/src/index.ts +44 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
- package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
- package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
- package/src/modules/terminal_tracking/acl.ts +10 -0
- package/src/modules/terminal_tracking/api/openapi.ts +28 -0
- package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
- package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
- package/src/modules/terminal_tracking/api/utils.ts +10 -0
- package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
- package/src/modules/terminal_tracking/ce.ts +31 -0
- package/src/modules/terminal_tracking/commands/index.ts +3 -0
- package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
- package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
- package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
- package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
- package/src/modules/terminal_tracking/data/entities.ts +386 -0
- package/src/modules/terminal_tracking/data/validators.ts +113 -0
- package/src/modules/terminal_tracking/di.ts +46 -0
- package/src/modules/terminal_tracking/encryption.ts +12 -0
- package/src/modules/terminal_tracking/events.ts +48 -0
- package/src/modules/terminal_tracking/i18n/de.json +175 -0
- package/src/modules/terminal_tracking/i18n/en.json +178 -0
- package/src/modules/terminal_tracking/i18n/es.json +175 -0
- package/src/modules/terminal_tracking/i18n/pl.json +206 -0
- package/src/modules/terminal_tracking/index.ts +13 -0
- package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
- package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
- package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
- package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
- package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
- package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
- package/src/modules/terminal_tracking/lib/availability.ts +119 -0
- package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
- package/src/modules/terminal_tracking/lib/holds.ts +344 -0
- package/src/modules/terminal_tracking/lib/logger.ts +21 -0
- package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
- package/src/modules/terminal_tracking/lib/stops.ts +124 -0
- package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
- package/src/modules/terminal_tracking/lib/totp.ts +63 -0
- package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
- package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
- package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
- package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
- package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
- package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
- package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
- package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
- package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
- package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
- package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
- package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
- package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
- package/src/modules/terminal_tracking/setup.ts +24 -0
- package/src/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.ts +32 -0
|
@@ -0,0 +1,540 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import * as React from 'react'
|
|
4
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
5
|
+
import { Sheet, SheetContent } from '@freighttech/ui/primitives/sheet'
|
|
6
|
+
import { Badge } from '@freighttech/ui/primitives/badge'
|
|
7
|
+
import { SimpleTooltip, TooltipProvider } from '@freighttech/ui/primitives/tooltip'
|
|
8
|
+
import { apiCallOrThrow } from '@freighttech/ui/backend/utils/apiCall'
|
|
9
|
+
import { useT } from '@open-mercato/shared/lib/i18n/context'
|
|
10
|
+
import { resolveHolds, type HoldSeverity } from '../lib/holds'
|
|
11
|
+
import { parseTerminalStops, activeStopModes, type TerminalStopMode } from '../lib/stops'
|
|
12
|
+
import {
|
|
13
|
+
Loader2,
|
|
14
|
+
LogIn,
|
|
15
|
+
Warehouse,
|
|
16
|
+
PackageCheck,
|
|
17
|
+
Truck,
|
|
18
|
+
Ship,
|
|
19
|
+
Train,
|
|
20
|
+
Anchor,
|
|
21
|
+
Calendar,
|
|
22
|
+
AlertTriangle,
|
|
23
|
+
ShieldAlert,
|
|
24
|
+
Ban,
|
|
25
|
+
} from 'lucide-react'
|
|
26
|
+
|
|
27
|
+
type TerminalSeal = { number: string; type?: string | null; source?: string | null }
|
|
28
|
+
|
|
29
|
+
type TerminalEventData = {
|
|
30
|
+
id: string
|
|
31
|
+
ufvGkey: string
|
|
32
|
+
sourceEventId: string
|
|
33
|
+
eventType: string
|
|
34
|
+
eventCode: string
|
|
35
|
+
eventClassifierCode: 'ACT' | 'PLN' | 'EST' | null
|
|
36
|
+
eventDateTime: string | null
|
|
37
|
+
transitState: string | null
|
|
38
|
+
visitState: string | null
|
|
39
|
+
facilityCode: string | null
|
|
40
|
+
facilityCodeListProvider: string | null
|
|
41
|
+
unlocode: string | null
|
|
42
|
+
visitRefIn: string | null
|
|
43
|
+
visitRefOut: string | null
|
|
44
|
+
vesselName: string | null
|
|
45
|
+
voyageNumber: string | null
|
|
46
|
+
modeOfTransport: 'VESSEL' | 'RAIL' | 'TRUCK' | 'BARGE' | null
|
|
47
|
+
seals: TerminalSeal[] | null
|
|
48
|
+
vgmWeightKg: number | null
|
|
49
|
+
impediments: string[] | null
|
|
50
|
+
loadedAt: string | null
|
|
51
|
+
rawData: Record<string, unknown> | null
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
type JobData = {
|
|
55
|
+
id: string
|
|
56
|
+
terminalCode: string
|
|
57
|
+
containerNumber: string
|
|
58
|
+
status: string
|
|
59
|
+
lastPollAt: string | null
|
|
60
|
+
nextPollAt: string | null
|
|
61
|
+
retryCount: number
|
|
62
|
+
errorHistory: Array<{ date: string; message: string }>
|
|
63
|
+
createdAt: string | null
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
type VesselVisitData = {
|
|
67
|
+
visitRef: string
|
|
68
|
+
vesselName: string | null
|
|
69
|
+
ibVoyage: string | null
|
|
70
|
+
obVoyage: string | null
|
|
71
|
+
line: string | null
|
|
72
|
+
phase: string | null
|
|
73
|
+
eta: string | null
|
|
74
|
+
etd: string | null
|
|
75
|
+
ata: string | null
|
|
76
|
+
atd: string | null
|
|
77
|
+
beginReceive: string | null
|
|
78
|
+
dryCutoff: string | null
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export interface ContainerDetailsDrawerProps {
|
|
82
|
+
open: boolean
|
|
83
|
+
onOpenChange: (open: boolean) => void
|
|
84
|
+
jobId: string | null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function eventIcon(code: string): React.ComponentType<{ className?: string }> {
|
|
88
|
+
switch (code) {
|
|
89
|
+
case 'GTIN':
|
|
90
|
+
return LogIn
|
|
91
|
+
case 'DISC':
|
|
92
|
+
return Warehouse
|
|
93
|
+
case 'LOAD':
|
|
94
|
+
return PackageCheck
|
|
95
|
+
case 'DEPA':
|
|
96
|
+
return Anchor
|
|
97
|
+
default:
|
|
98
|
+
return PackageCheck
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function modeIcon(mode: string | null): React.ComponentType<{ className?: string }> | null {
|
|
103
|
+
switch (mode) {
|
|
104
|
+
case 'RAIL':
|
|
105
|
+
return Train
|
|
106
|
+
case 'TRUCK':
|
|
107
|
+
return Truck
|
|
108
|
+
case 'VESSEL':
|
|
109
|
+
case 'BARGE':
|
|
110
|
+
return Ship
|
|
111
|
+
default:
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function fmt(date: string | null): string {
|
|
117
|
+
if (!date) return '—'
|
|
118
|
+
try {
|
|
119
|
+
return new Date(date).toLocaleString()
|
|
120
|
+
} catch {
|
|
121
|
+
return date
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function val(v: unknown): string {
|
|
126
|
+
if (v == null || v === '') return '—'
|
|
127
|
+
if (Array.isArray(v)) return v.length ? v.join(', ') : '—'
|
|
128
|
+
if (typeof v === 'object') return JSON.stringify(v)
|
|
129
|
+
return String(v)
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function statusVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
|
|
133
|
+
if (status === 'failed') return 'destructive'
|
|
134
|
+
if (status === 'active') return 'default'
|
|
135
|
+
if (status === 'completed') return 'outline'
|
|
136
|
+
return 'secondary'
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Badge colours by hold severity: red = blocking, amber = routine, slate = info. */
|
|
140
|
+
function holdBadgeClass(severity: HoldSeverity): string {
|
|
141
|
+
switch (severity) {
|
|
142
|
+
case 'critical':
|
|
143
|
+
return 'bg-red-100 text-red-700'
|
|
144
|
+
case 'warning':
|
|
145
|
+
return 'bg-amber-100 text-amber-800'
|
|
146
|
+
case 'info':
|
|
147
|
+
return 'bg-slate-100 text-slate-600'
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function ContainerDetailsDrawer({ open, onOpenChange, jobId }: ContainerDetailsDrawerProps) {
|
|
152
|
+
const t = useT()
|
|
153
|
+
const [loading, setLoading] = useState(false)
|
|
154
|
+
const [job, setJob] = useState<JobData | null>(null)
|
|
155
|
+
const [events, setEvents] = useState<TerminalEventData[]>([])
|
|
156
|
+
const [vesselVisits, setVesselVisits] = useState<Record<string, VesselVisitData>>({})
|
|
157
|
+
const [error, setError] = useState<string | null>(null)
|
|
158
|
+
|
|
159
|
+
useEffect(() => {
|
|
160
|
+
if (!open || !jobId) return
|
|
161
|
+
let cancelled = false
|
|
162
|
+
setLoading(true)
|
|
163
|
+
setError(null)
|
|
164
|
+
apiCallOrThrow<{
|
|
165
|
+
job: JobData
|
|
166
|
+
events: TerminalEventData[]
|
|
167
|
+
vesselVisits?: Record<string, VesselVisitData>
|
|
168
|
+
}>(`/api/terminal_tracking/tracking-jobs/details?id=${encodeURIComponent(jobId)}`)
|
|
169
|
+
.then(({ result }) => {
|
|
170
|
+
if (cancelled) return
|
|
171
|
+
setJob(result?.job ?? null)
|
|
172
|
+
setEvents(result?.events ?? [])
|
|
173
|
+
setVesselVisits(result?.vesselVisits ?? {})
|
|
174
|
+
})
|
|
175
|
+
.catch((err) => {
|
|
176
|
+
if (cancelled) return
|
|
177
|
+
setError(err instanceof Error ? err.message : 'Failed to load container details')
|
|
178
|
+
})
|
|
179
|
+
.finally(() => {
|
|
180
|
+
if (!cancelled) setLoading(false)
|
|
181
|
+
})
|
|
182
|
+
return () => {
|
|
183
|
+
cancelled = true
|
|
184
|
+
}
|
|
185
|
+
}, [open, jobId])
|
|
186
|
+
|
|
187
|
+
const handleKeyDown = useCallback(
|
|
188
|
+
(e: React.KeyboardEvent) => {
|
|
189
|
+
if (e.key === 'Escape') onOpenChange(false)
|
|
190
|
+
},
|
|
191
|
+
[onOpenChange],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
const eventLabel = useCallback(
|
|
195
|
+
(code: string): string => {
|
|
196
|
+
switch (code) {
|
|
197
|
+
case 'GTIN':
|
|
198
|
+
return t('terminal_tracking.event.gate_in', 'Gate In')
|
|
199
|
+
case 'DISC':
|
|
200
|
+
return t('terminal_tracking.event.yard', 'In Yard / Discharged')
|
|
201
|
+
case 'LOAD':
|
|
202
|
+
return t('terminal_tracking.event.loaded', 'Loaded')
|
|
203
|
+
case 'DEPA':
|
|
204
|
+
return t('terminal_tracking.event.departed', 'Departed')
|
|
205
|
+
default:
|
|
206
|
+
return code
|
|
207
|
+
}
|
|
208
|
+
},
|
|
209
|
+
[t],
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
// Group events by Unit Facility Visit (import/export leg)
|
|
213
|
+
const legs = React.useMemo(() => {
|
|
214
|
+
const map = new Map<string, TerminalEventData[]>()
|
|
215
|
+
for (const ev of events) {
|
|
216
|
+
const arr = map.get(ev.ufvGkey) ?? []
|
|
217
|
+
arr.push(ev)
|
|
218
|
+
map.set(ev.ufvGkey, arr)
|
|
219
|
+
}
|
|
220
|
+
return Array.from(map.entries()).map(([ufvGkey, evs]) => ({ ufvGkey, events: evs }))
|
|
221
|
+
}, [events])
|
|
222
|
+
|
|
223
|
+
const latest = events.length ? events[events.length - 1] : null
|
|
224
|
+
const vgm = latest?.vgmWeightKg ?? null
|
|
225
|
+
const seals = latest?.seals ?? null
|
|
226
|
+
// The load timestamp of whichever leg reported one (last non-null wins).
|
|
227
|
+
const loadedAt = events.map((e) => e.loadedAt).filter(Boolean).pop() ?? null
|
|
228
|
+
// Per-mode load/pickup restrictions (Stop-Vsl/Road/Rail), read from the raw
|
|
229
|
+
// N4 columns on the latest snapshot; only active stops are surfaced.
|
|
230
|
+
const stopModes = activeStopModes(parseTerminalStops(latest?.rawData ?? null))
|
|
231
|
+
|
|
232
|
+
const stopLabel = useCallback(
|
|
233
|
+
(mode: TerminalStopMode): string => {
|
|
234
|
+
switch (mode) {
|
|
235
|
+
case 'vsl':
|
|
236
|
+
return t('terminal_tracking.stops.vsl', 'No vessel loading')
|
|
237
|
+
case 'road':
|
|
238
|
+
return t('terminal_tracking.stops.road', 'No road pickup')
|
|
239
|
+
case 'rail':
|
|
240
|
+
return t('terminal_tracking.stops.rail', 'No rail pickup')
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
[t],
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
return (
|
|
247
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
248
|
+
<SheetContent
|
|
249
|
+
className="flex flex-col p-0 w-full sm:max-w-xl md:max-w-2xl"
|
|
250
|
+
overlayClassName="backdrop-blur-none"
|
|
251
|
+
onKeyDown={handleKeyDown}
|
|
252
|
+
>
|
|
253
|
+
{/* Header */}
|
|
254
|
+
<div className="px-5 py-4 border-b border-border">
|
|
255
|
+
<div className="text-xs uppercase tracking-wide text-muted-foreground">
|
|
256
|
+
{t('terminal_tracking.tracking_job.details', 'Container Details')}
|
|
257
|
+
</div>
|
|
258
|
+
<div className="mt-1 flex items-center gap-3">
|
|
259
|
+
<span className="text-xl font-semibold font-mono">{job?.containerNumber ?? '—'}</span>
|
|
260
|
+
{job ? (
|
|
261
|
+
<Badge variant={statusVariant(job.status)}>
|
|
262
|
+
{t(`terminal_tracking.tracking_job.statuses.${job.status}`, job.status)}
|
|
263
|
+
</Badge>
|
|
264
|
+
) : null}
|
|
265
|
+
</div>
|
|
266
|
+
{job ? (
|
|
267
|
+
<div className="mt-1 text-sm text-muted-foreground">
|
|
268
|
+
{t('terminal_tracking.terminal_config.terminalCode', 'Terminal')}:{' '}
|
|
269
|
+
<span className="font-medium uppercase">{job.terminalCode}</span>
|
|
270
|
+
</div>
|
|
271
|
+
) : null}
|
|
272
|
+
</div>
|
|
273
|
+
|
|
274
|
+
{/* Body */}
|
|
275
|
+
<div className="flex-1 overflow-y-auto p-4 space-y-4">
|
|
276
|
+
{loading ? (
|
|
277
|
+
<div className="flex items-center justify-center py-12">
|
|
278
|
+
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
|
|
279
|
+
</div>
|
|
280
|
+
) : error ? (
|
|
281
|
+
<div className="flex items-center gap-2 rounded-md bg-red-50 p-3 text-sm text-red-700">
|
|
282
|
+
<AlertTriangle className="h-4 w-4" />
|
|
283
|
+
<span>{error}</span>
|
|
284
|
+
</div>
|
|
285
|
+
) : (
|
|
286
|
+
<>
|
|
287
|
+
{/* Holds */}
|
|
288
|
+
{latest?.impediments && latest.impediments.length > 0 ? (
|
|
289
|
+
<div className="rounded-md border border-red-200 bg-red-50 p-3">
|
|
290
|
+
<div className="flex items-center gap-2 text-sm font-medium text-red-700">
|
|
291
|
+
<ShieldAlert className="h-4 w-4" />
|
|
292
|
+
{t('terminal_tracking.event.impediments', 'Holds')}
|
|
293
|
+
</div>
|
|
294
|
+
<TooltipProvider>
|
|
295
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
296
|
+
{resolveHolds(latest.impediments).map((hold) => {
|
|
297
|
+
const label = hold.unknown
|
|
298
|
+
? hold.label
|
|
299
|
+
: t(`terminal_tracking.holds.${hold.i18nKey}.label`, hold.label)
|
|
300
|
+
const description = t(
|
|
301
|
+
`terminal_tracking.holds.${hold.i18nKey}.description`,
|
|
302
|
+
hold.description,
|
|
303
|
+
)
|
|
304
|
+
const resolution = t(
|
|
305
|
+
`terminal_tracking.holds.${hold.i18nKey}.resolution`,
|
|
306
|
+
hold.resolution,
|
|
307
|
+
)
|
|
308
|
+
return (
|
|
309
|
+
<SimpleTooltip
|
|
310
|
+
key={hold.rawCode}
|
|
311
|
+
content={
|
|
312
|
+
<div className="space-y-1">
|
|
313
|
+
<div className="font-medium">{label}</div>
|
|
314
|
+
<div>{description}</div>
|
|
315
|
+
<div className="text-slate-300">{resolution}</div>
|
|
316
|
+
</div>
|
|
317
|
+
}
|
|
318
|
+
>
|
|
319
|
+
<span
|
|
320
|
+
className={`cursor-help rounded px-1.5 py-0.5 text-xs ${holdBadgeClass(hold.severity)}`}
|
|
321
|
+
>
|
|
322
|
+
{label}
|
|
323
|
+
</span>
|
|
324
|
+
</SimpleTooltip>
|
|
325
|
+
)
|
|
326
|
+
})}
|
|
327
|
+
</div>
|
|
328
|
+
</TooltipProvider>
|
|
329
|
+
</div>
|
|
330
|
+
) : null}
|
|
331
|
+
|
|
332
|
+
{/* Loading restrictions (Stop-Vsl / Stop-Road / Stop-Rail) */}
|
|
333
|
+
{stopModes.length > 0 ? (
|
|
334
|
+
<div className="rounded-md border border-orange-200 bg-orange-50 p-3">
|
|
335
|
+
<div className="flex items-center gap-2 text-sm font-medium text-orange-700">
|
|
336
|
+
<Ban className="h-4 w-4" />
|
|
337
|
+
{t('terminal_tracking.stops.title', 'Loading restrictions')}
|
|
338
|
+
</div>
|
|
339
|
+
<div className="mt-2 flex flex-wrap gap-1.5">
|
|
340
|
+
{stopModes.map((mode) => {
|
|
341
|
+
const Icon = mode === 'vsl' ? Ship : mode === 'road' ? Truck : Train
|
|
342
|
+
return (
|
|
343
|
+
<span
|
|
344
|
+
key={mode}
|
|
345
|
+
className="inline-flex items-center gap-1 rounded bg-orange-100 px-1.5 py-0.5 text-xs text-orange-800"
|
|
346
|
+
>
|
|
347
|
+
<Icon className="h-3 w-3" />
|
|
348
|
+
{stopLabel(mode)}
|
|
349
|
+
</span>
|
|
350
|
+
)
|
|
351
|
+
})}
|
|
352
|
+
</div>
|
|
353
|
+
</div>
|
|
354
|
+
) : null}
|
|
355
|
+
|
|
356
|
+
{/* Summary */}
|
|
357
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-2">
|
|
358
|
+
<SummaryItem label={t('terminal_tracking.tracking_job.lastPollAt', 'Last polled')} value={fmt(job?.lastPollAt ?? null)} />
|
|
359
|
+
<SummaryItem label={t('terminal_tracking.event.vgm', 'VGM (kg)')} value={vgm != null ? String(vgm) : '—'} />
|
|
360
|
+
<SummaryItem
|
|
361
|
+
label={t('terminal_tracking.event.seals', 'Seals')}
|
|
362
|
+
value={seals && seals.length ? seals.map((s) => s.number).join(', ') : '—'}
|
|
363
|
+
/>
|
|
364
|
+
<SummaryItem label={t('terminal_tracking.event.loadedAt', 'Loaded at')} value={fmt(loadedAt)} />
|
|
365
|
+
<SummaryItem label={t('common.created', 'Created')} value={fmt(job?.createdAt ?? null)} />
|
|
366
|
+
</div>
|
|
367
|
+
|
|
368
|
+
{/* Events by leg */}
|
|
369
|
+
{legs.length === 0 ? (
|
|
370
|
+
<div className="rounded-md border border-dashed p-6 text-center text-sm text-muted-foreground">
|
|
371
|
+
{t('terminal_tracking.tracking_job.noEvents', 'No terminal events yet. Use "Poll now" to fetch.')}
|
|
372
|
+
</div>
|
|
373
|
+
) : (
|
|
374
|
+
legs.map(({ ufvGkey, events: legEvents }) => {
|
|
375
|
+
const last = legEvents[legEvents.length - 1]
|
|
376
|
+
const category = (last?.rawData?.['Category'] as string) ?? null
|
|
377
|
+
const ib = last?.visitRefIn
|
|
378
|
+
const ob = last?.visitRefOut
|
|
379
|
+
// The leg's vessel: whichever visit ref resolved to a vessel
|
|
380
|
+
// visit (import → I/B, export → O/B; land-side refs don't resolve).
|
|
381
|
+
const vesselRef = ib && vesselVisits[ib] ? ib : ob && vesselVisits[ob] ? ob : null
|
|
382
|
+
const vessel = vesselRef ? vesselVisits[vesselRef] : null
|
|
383
|
+
const voyage = vessel ? (vesselRef === ib ? vessel.ibVoyage : vessel.obVoyage) : null
|
|
384
|
+
return (
|
|
385
|
+
<div key={ufvGkey} className="rounded-lg border">
|
|
386
|
+
<div className="flex items-center justify-between border-b bg-muted/40 px-3 py-2 text-xs">
|
|
387
|
+
<span className="font-medium">
|
|
388
|
+
{category ?? t('terminal_tracking.tracking_job.leg', 'Facility visit')}
|
|
389
|
+
<span className="ml-2 font-normal text-muted-foreground">
|
|
390
|
+
{t('terminal_tracking.event.ufvGkey', 'Ufv_Gkey')} {ufvGkey}
|
|
391
|
+
</span>
|
|
392
|
+
</span>
|
|
393
|
+
<span className="text-muted-foreground">
|
|
394
|
+
{ib ? `I/B ${ib}` : ''} {ob ? `· O/B ${ob}` : ''}
|
|
395
|
+
</span>
|
|
396
|
+
</div>
|
|
397
|
+
|
|
398
|
+
{/* Vessel visit (ETA/ETD/ATA/ATD from /VESSEL) */}
|
|
399
|
+
{vessel ? (
|
|
400
|
+
<div className="border-b bg-sky-50/60 px-3 py-2">
|
|
401
|
+
<div className="flex flex-wrap items-center gap-2 text-xs font-medium">
|
|
402
|
+
<Ship className="h-3.5 w-3.5 text-sky-700" />
|
|
403
|
+
<span>{vessel.vesselName ?? vessel.visitRef}</span>
|
|
404
|
+
{voyage ? <span className="text-muted-foreground">{voyage}</span> : null}
|
|
405
|
+
{vessel.line ? <span className="text-muted-foreground">· {vessel.line}</span> : null}
|
|
406
|
+
{vessel.phase ? (
|
|
407
|
+
<span className="rounded bg-sky-100 px-1.5 py-0.5 text-[10px] text-sky-700">
|
|
408
|
+
{vessel.phase}
|
|
409
|
+
</span>
|
|
410
|
+
) : null}
|
|
411
|
+
</div>
|
|
412
|
+
<div className="mt-1.5 grid grid-cols-2 gap-x-4 gap-y-1 sm:grid-cols-4">
|
|
413
|
+
<VesselTime label={t('terminal_tracking.event.eta', 'ETA')} value={vessel.eta} />
|
|
414
|
+
<VesselTime label={t('terminal_tracking.event.ata', 'ATA')} value={vessel.ata} actual />
|
|
415
|
+
<VesselTime label={t('terminal_tracking.event.etd', 'ETD')} value={vessel.etd} />
|
|
416
|
+
<VesselTime label={t('terminal_tracking.event.atd', 'ATD')} value={vessel.atd} actual />
|
|
417
|
+
</div>
|
|
418
|
+
</div>
|
|
419
|
+
) : null}
|
|
420
|
+
|
|
421
|
+
{/* Milestone timeline */}
|
|
422
|
+
<div className="space-y-2 p-2.5">
|
|
423
|
+
{legEvents.map((ev) => {
|
|
424
|
+
const Icon = eventIcon(ev.eventCode)
|
|
425
|
+
const Mode = modeIcon(ev.modeOfTransport)
|
|
426
|
+
return (
|
|
427
|
+
<div key={ev.id} className="flex items-start gap-3">
|
|
428
|
+
<div className="mt-0.5 rounded-full border-2 border-green-200 bg-card p-1.5">
|
|
429
|
+
<Icon className="h-4 w-4 text-green-600" />
|
|
430
|
+
</div>
|
|
431
|
+
<div className="flex-1">
|
|
432
|
+
<div className="flex items-center gap-2">
|
|
433
|
+
<span className="font-medium">{eventLabel(ev.eventCode)}</span>
|
|
434
|
+
{ev.eventClassifierCode ? (
|
|
435
|
+
<span className="rounded bg-green-100 px-1.5 py-0.5 text-[10px] font-medium text-green-700">
|
|
436
|
+
{ev.eventClassifierCode}
|
|
437
|
+
</span>
|
|
438
|
+
) : null}
|
|
439
|
+
{ev.transitState ? (
|
|
440
|
+
<span className="text-[10px] text-muted-foreground">{ev.transitState}</span>
|
|
441
|
+
) : null}
|
|
442
|
+
</div>
|
|
443
|
+
<div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5 text-xs text-muted-foreground">
|
|
444
|
+
<span className="inline-flex items-center gap-1">
|
|
445
|
+
<Calendar className="h-3 w-3" /> {fmt(ev.eventDateTime)}
|
|
446
|
+
</span>
|
|
447
|
+
{ev.vesselName ? (
|
|
448
|
+
<span className="inline-flex items-center gap-1">
|
|
449
|
+
<Ship className="h-3 w-3" /> {ev.vesselName}
|
|
450
|
+
{ev.voyageNumber ? ` ${ev.voyageNumber}` : ''}
|
|
451
|
+
</span>
|
|
452
|
+
) : null}
|
|
453
|
+
{Mode ? (
|
|
454
|
+
<span className="inline-flex items-center gap-1">
|
|
455
|
+
<Mode className="h-3 w-3" /> {ev.modeOfTransport}
|
|
456
|
+
</span>
|
|
457
|
+
) : null}
|
|
458
|
+
{ev.unlocode ? <span>{ev.unlocode}</span> : null}
|
|
459
|
+
{ev.facilityCode ? (
|
|
460
|
+
<span>
|
|
461
|
+
{ev.facilityCode}
|
|
462
|
+
{ev.facilityCodeListProvider ? ` (${ev.facilityCodeListProvider})` : ''}
|
|
463
|
+
</span>
|
|
464
|
+
) : null}
|
|
465
|
+
</div>
|
|
466
|
+
</div>
|
|
467
|
+
</div>
|
|
468
|
+
)
|
|
469
|
+
})}
|
|
470
|
+
</div>
|
|
471
|
+
|
|
472
|
+
{/* Full raw N4 row (non-empty fields), 2-column field grid */}
|
|
473
|
+
{(() => {
|
|
474
|
+
const rawRows = Object.entries(last?.rawData ?? {})
|
|
475
|
+
.map(([k, v]) => [k, val(v)] as [string, string])
|
|
476
|
+
.filter(([, v]) => v !== '—')
|
|
477
|
+
if (rawRows.length === 0) return null
|
|
478
|
+
return (
|
|
479
|
+
<div className="border-t px-2.5 py-2.5">
|
|
480
|
+
<div className="mb-1.5 text-[11px] font-medium uppercase tracking-wide text-muted-foreground">
|
|
481
|
+
{t('terminal_tracking.event.rawData', 'Raw terminal data')}
|
|
482
|
+
</div>
|
|
483
|
+
<div className="grid grid-cols-2 gap-x-4 gap-y-1.5">
|
|
484
|
+
{rawRows.map(([k, v], i) => (
|
|
485
|
+
<SummaryItem key={`${k}-${i}`} label={k} value={v} />
|
|
486
|
+
))}
|
|
487
|
+
</div>
|
|
488
|
+
</div>
|
|
489
|
+
)
|
|
490
|
+
})()}
|
|
491
|
+
</div>
|
|
492
|
+
)
|
|
493
|
+
})
|
|
494
|
+
)}
|
|
495
|
+
|
|
496
|
+
{/* Errors */}
|
|
497
|
+
{job?.errorHistory && job.errorHistory.length > 0 ? (
|
|
498
|
+
<div className="rounded-md border border-amber-200 bg-amber-50 p-3">
|
|
499
|
+
<div className="text-sm font-medium text-amber-800">
|
|
500
|
+
{t('terminal_tracking.tracking_job.recentErrors', 'Recent poll errors')}
|
|
501
|
+
</div>
|
|
502
|
+
<ul className="mt-2 space-y-1 text-xs text-amber-700">
|
|
503
|
+
{job.errorHistory.slice(0, 5).map((e, i) => (
|
|
504
|
+
<li key={i}>
|
|
505
|
+
<span className="text-amber-500">{fmt(e.date)}</span> — {e.message}
|
|
506
|
+
</li>
|
|
507
|
+
))}
|
|
508
|
+
</ul>
|
|
509
|
+
</div>
|
|
510
|
+
) : null}
|
|
511
|
+
</>
|
|
512
|
+
)}
|
|
513
|
+
</div>
|
|
514
|
+
</SheetContent>
|
|
515
|
+
</Sheet>
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function SummaryItem({ label, value }: { label: string; value: string }) {
|
|
520
|
+
return (
|
|
521
|
+
<div className="min-w-0">
|
|
522
|
+
<div className="truncate text-xs text-muted-foreground">{label}</div>
|
|
523
|
+
<div className="break-words text-sm font-medium">{value}</div>
|
|
524
|
+
</div>
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** One vessel timestamp cell; actual (ATA/ATD) values render emphasized. */
|
|
529
|
+
function VesselTime({ label, value, actual = false }: { label: string; value: string | null; actual?: boolean }) {
|
|
530
|
+
return (
|
|
531
|
+
<div className="min-w-0">
|
|
532
|
+
<div className="text-[10px] uppercase tracking-wide text-muted-foreground">{label}</div>
|
|
533
|
+
<div className={`text-xs ${actual && value ? 'font-semibold text-sky-800' : 'font-medium'}`}>
|
|
534
|
+
{fmt(value)}
|
|
535
|
+
</div>
|
|
536
|
+
</div>
|
|
537
|
+
)
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
export default ContainerDetailsDrawer
|