@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.
Files changed (236) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +121 -0
  3. package/dist/generated/entities/terminal_config/index.js +53 -0
  4. package/dist/generated/entities/terminal_config/index.js.map +7 -0
  5. package/dist/generated/entities/terminal_event/index.js +59 -0
  6. package/dist/generated/entities/terminal_event/index.js.map +7 -0
  7. package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
  8. package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
  9. package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
  10. package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
  11. package/dist/generated/entities.ids.generated.js +26 -0
  12. package/dist/generated/entities.ids.generated.js.map +7 -0
  13. package/dist/index.js +20 -0
  14. package/dist/index.js.map +7 -0
  15. package/dist/modules/terminal_tracking/acl.js +14 -0
  16. package/dist/modules/terminal_tracking/acl.js.map +7 -0
  17. package/dist/modules/terminal_tracking/api/openapi.js +27 -0
  18. package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
  19. package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
  20. package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
  21. package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
  22. package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
  23. package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
  24. package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
  25. package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
  26. package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
  27. package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
  28. package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
  29. package/dist/modules/terminal_tracking/api/utils.js +12 -0
  30. package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
  31. package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
  32. package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
  33. package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
  34. package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
  35. package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
  36. package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
  37. package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
  38. package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
  39. package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
  40. package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
  41. package/dist/modules/terminal_tracking/ce.js +35 -0
  42. package/dist/modules/terminal_tracking/ce.js.map +7 -0
  43. package/dist/modules/terminal_tracking/commands/index.js +4 -0
  44. package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
  45. package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
  46. package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
  47. package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
  48. package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
  49. package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
  50. package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
  51. package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
  52. package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
  53. package/dist/modules/terminal_tracking/data/entities.js +337 -0
  54. package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
  55. package/dist/modules/terminal_tracking/data/validators.js +98 -0
  56. package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
  57. package/dist/modules/terminal_tracking/di.js +39 -0
  58. package/dist/modules/terminal_tracking/di.js.map +7 -0
  59. package/dist/modules/terminal_tracking/encryption.js +14 -0
  60. package/dist/modules/terminal_tracking/encryption.js.map +7 -0
  61. package/dist/modules/terminal_tracking/events.js +45 -0
  62. package/dist/modules/terminal_tracking/events.js.map +7 -0
  63. package/dist/modules/terminal_tracking/i18n/de.json +175 -0
  64. package/dist/modules/terminal_tracking/i18n/en.json +178 -0
  65. package/dist/modules/terminal_tracking/i18n/es.json +175 -0
  66. package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
  67. package/dist/modules/terminal_tracking/index.js +15 -0
  68. package/dist/modules/terminal_tracking/index.js.map +7 -0
  69. package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
  70. package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
  71. package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
  72. package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
  73. package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
  74. package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
  75. package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
  76. package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
  77. package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
  78. package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
  79. package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
  80. package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
  81. package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
  82. package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
  83. package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
  84. package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
  85. package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
  86. package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
  87. package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
  88. package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
  89. package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
  90. package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
  91. package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
  92. package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
  93. package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
  94. package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
  95. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
  96. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
  97. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
  98. package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
  99. package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
  100. package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
  101. package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
  102. package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
  103. package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
  104. package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
  105. package/dist/modules/terminal_tracking/lib/availability.js +40 -0
  106. package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
  107. package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
  108. package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
  109. package/dist/modules/terminal_tracking/lib/holds.js +240 -0
  110. package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
  111. package/dist/modules/terminal_tracking/lib/logger.js +19 -0
  112. package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
  113. package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
  114. package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
  115. package/dist/modules/terminal_tracking/lib/stops.js +48 -0
  116. package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
  117. package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
  118. package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
  119. package/dist/modules/terminal_tracking/lib/totp.js +41 -0
  120. package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
  121. package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
  122. package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
  123. package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
  124. package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
  125. package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
  126. package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
  127. package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
  128. package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
  129. package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
  130. package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
  131. package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
  132. package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
  133. package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
  134. package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
  135. package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
  136. package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
  137. package/dist/modules/terminal_tracking/setup.js +23 -0
  138. package/dist/modules/terminal_tracking/setup.js.map +7 -0
  139. package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
  140. package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
  141. package/generated/entities/terminal_config/index.ts +36 -0
  142. package/generated/entities/terminal_event/index.ts +39 -0
  143. package/generated/entities/terminal_tracking_job/index.ts +28 -0
  144. package/generated/entities/terminal_vessel_visit/index.ts +30 -0
  145. package/generated/entities.ids.generated.ts +52 -0
  146. package/package.json +80 -0
  147. package/src/index.ts +44 -0
  148. package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
  149. package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
  150. package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
  151. package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
  152. package/src/modules/terminal_tracking/acl.ts +10 -0
  153. package/src/modules/terminal_tracking/api/openapi.ts +28 -0
  154. package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
  155. package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
  156. package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
  157. package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
  158. package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
  159. package/src/modules/terminal_tracking/api/utils.ts +10 -0
  160. package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
  161. package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
  162. package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
  163. package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
  164. package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
  165. package/src/modules/terminal_tracking/ce.ts +31 -0
  166. package/src/modules/terminal_tracking/commands/index.ts +3 -0
  167. package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
  168. package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
  169. package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
  170. package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
  171. package/src/modules/terminal_tracking/data/entities.ts +386 -0
  172. package/src/modules/terminal_tracking/data/validators.ts +113 -0
  173. package/src/modules/terminal_tracking/di.ts +46 -0
  174. package/src/modules/terminal_tracking/encryption.ts +12 -0
  175. package/src/modules/terminal_tracking/events.ts +48 -0
  176. package/src/modules/terminal_tracking/i18n/de.json +175 -0
  177. package/src/modules/terminal_tracking/i18n/en.json +178 -0
  178. package/src/modules/terminal_tracking/i18n/es.json +175 -0
  179. package/src/modules/terminal_tracking/i18n/pl.json +206 -0
  180. package/src/modules/terminal_tracking/index.ts +13 -0
  181. package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
  182. package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
  183. package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
  184. package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
  185. package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
  186. package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
  187. package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
  188. package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
  189. package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
  190. package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
  191. package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
  192. package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
  193. package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
  194. package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
  195. package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
  196. package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
  197. package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
  198. package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
  199. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
  200. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
  201. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
  202. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
  203. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
  204. package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
  205. package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
  206. package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
  207. package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
  208. package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
  209. package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
  210. package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
  211. package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
  212. package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
  213. package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
  214. package/src/modules/terminal_tracking/lib/availability.ts +119 -0
  215. package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
  216. package/src/modules/terminal_tracking/lib/holds.ts +344 -0
  217. package/src/modules/terminal_tracking/lib/logger.ts +21 -0
  218. package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
  219. package/src/modules/terminal_tracking/lib/stops.ts +124 -0
  220. package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
  221. package/src/modules/terminal_tracking/lib/totp.ts +63 -0
  222. package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
  223. package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
  224. package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
  225. package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
  226. package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
  227. package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
  228. package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
  229. package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
  230. package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
  231. package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
  232. package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
  233. package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
  234. package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
  235. package/src/modules/terminal_tracking/setup.ts +24 -0
  236. 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