@freighttech/shipment-tracking 0.6.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 (456) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +25 -0
  3. package/dist/generated/entities/bic_config/index.js +21 -0
  4. package/dist/generated/entities/bic_config/index.js.map +7 -0
  5. package/dist/generated/entities/carrier_config/index.js +27 -0
  6. package/dist/generated/entities/carrier_config/index.js.map +7 -0
  7. package/dist/generated/entities/location_override/index.js +33 -0
  8. package/dist/generated/entities/location_override/index.js.map +7 -0
  9. package/dist/generated/entities/shipment/index.js +73 -0
  10. package/dist/generated/entities/shipment/index.js.map +7 -0
  11. package/dist/generated/entities/ships_go_config/index.js +31 -0
  12. package/dist/generated/entities/ships_go_config/index.js.map +7 -0
  13. package/dist/generated/entities/tracking_event/index.js +101 -0
  14. package/dist/generated/entities/tracking_event/index.js.map +7 -0
  15. package/dist/generated/entities/tracking_job/index.js +47 -0
  16. package/dist/generated/entities/tracking_job/index.js.map +7 -0
  17. package/dist/generated/entities/webhook/index.js +25 -0
  18. package/dist/generated/entities/webhook/index.js.map +7 -0
  19. package/dist/generated/entities/webhook_delivery/index.js +25 -0
  20. package/dist/generated/entities/webhook_delivery/index.js.map +7 -0
  21. package/dist/generated/entities.ids.generated.js +21 -0
  22. package/dist/generated/entities.ids.generated.js.map +7 -0
  23. package/dist/index.js +41 -0
  24. package/dist/index.js.map +7 -0
  25. package/dist/modules/shipment_tracking/acl.js +22 -0
  26. package/dist/modules/shipment_tracking/acl.js.map +7 -0
  27. package/dist/modules/shipment_tracking/api/bic-configs/route.js +176 -0
  28. package/dist/modules/shipment_tracking/api/bic-configs/route.js.map +7 -0
  29. package/dist/modules/shipment_tracking/api/bic-configs/test/route.js +95 -0
  30. package/dist/modules/shipment_tracking/api/bic-configs/test/route.js.map +7 -0
  31. package/dist/modules/shipment_tracking/api/cargo-events/route.js +93 -0
  32. package/dist/modules/shipment_tracking/api/cargo-events/route.js.map +7 -0
  33. package/dist/modules/shipment_tracking/api/carrier-configs/route.js +139 -0
  34. package/dist/modules/shipment_tracking/api/carrier-configs/route.js.map +7 -0
  35. package/dist/modules/shipment_tracking/api/dashboard/ships-map/route.js +177 -0
  36. package/dist/modules/shipment_tracking/api/dashboard/ships-map/route.js.map +7 -0
  37. package/dist/modules/shipment_tracking/api/location-overrides/route.js +179 -0
  38. package/dist/modules/shipment_tracking/api/location-overrides/route.js.map +7 -0
  39. package/dist/modules/shipment_tracking/api/openapi.js +27 -0
  40. package/dist/modules/shipment_tracking/api/openapi.js.map +7 -0
  41. package/dist/modules/shipment_tracking/api/shipments/route.js +192 -0
  42. package/dist/modules/shipment_tracking/api/shipments/route.js.map +7 -0
  43. package/dist/modules/shipment_tracking/api/shipsgo-configs/route.js +185 -0
  44. package/dist/modules/shipment_tracking/api/shipsgo-configs/route.js.map +7 -0
  45. package/dist/modules/shipment_tracking/api/tracking-jobs/route.js +144 -0
  46. package/dist/modules/shipment_tracking/api/tracking-jobs/route.js.map +7 -0
  47. package/dist/modules/shipment_tracking/api/tracking-jobs/sync/route.js +107 -0
  48. package/dist/modules/shipment_tracking/api/tracking-jobs/sync/route.js.map +7 -0
  49. package/dist/modules/shipment_tracking/api/utils.js +12 -0
  50. package/dist/modules/shipment_tracking/api/utils.js.map +7 -0
  51. package/dist/modules/shipment_tracking/api/webhooks/deliveries/route.js +76 -0
  52. package/dist/modules/shipment_tracking/api/webhooks/deliveries/route.js.map +7 -0
  53. package/dist/modules/shipment_tracking/api/webhooks/route.js +135 -0
  54. package/dist/modules/shipment_tracking/api/webhooks/route.js.map +7 -0
  55. package/dist/modules/shipment_tracking/api/webhooks/test/route.js +71 -0
  56. package/dist/modules/shipment_tracking/api/webhooks/test/route.js.map +7 -0
  57. package/dist/modules/shipment_tracking/assets/carriers/cma_cgm.png +0 -0
  58. package/dist/modules/shipment_tracking/assets/carriers/cosco.png +0 -0
  59. package/dist/modules/shipment_tracking/assets/carriers/evergreen.png +0 -0
  60. package/dist/modules/shipment_tracking/assets/carriers/hapag_lloyd.png +0 -0
  61. package/dist/modules/shipment_tracking/assets/carriers/maersk.png +0 -0
  62. package/dist/modules/shipment_tracking/assets/carriers/msc.png +0 -0
  63. package/dist/modules/shipment_tracking/assets/carriers/one.png +0 -0
  64. package/dist/modules/shipment_tracking/assets/carriers/oocl.png +0 -0
  65. package/dist/modules/shipment_tracking/assets/carriers/yang_ming.png +0 -0
  66. package/dist/modules/shipment_tracking/assets/carriers/zim.png +0 -0
  67. package/dist/modules/shipment_tracking/assets/index.js +52 -0
  68. package/dist/modules/shipment_tracking/assets/index.js.map +7 -0
  69. package/dist/modules/shipment_tracking/backend/carrier-configs/page.js +621 -0
  70. package/dist/modules/shipment_tracking/backend/carrier-configs/page.js.map +7 -0
  71. package/dist/modules/shipment_tracking/backend/carrier-configs/page.meta.js +19 -0
  72. package/dist/modules/shipment_tracking/backend/carrier-configs/page.meta.js.map +7 -0
  73. package/dist/modules/shipment_tracking/backend/location-overrides/page.js +198 -0
  74. package/dist/modules/shipment_tracking/backend/location-overrides/page.js.map +7 -0
  75. package/dist/modules/shipment_tracking/backend/location-overrides/page.meta.js +19 -0
  76. package/dist/modules/shipment_tracking/backend/location-overrides/page.meta.js.map +7 -0
  77. package/dist/modules/shipment_tracking/backend/shipment-tracking/page.js +279 -0
  78. package/dist/modules/shipment_tracking/backend/shipment-tracking/page.js.map +7 -0
  79. package/dist/modules/shipment_tracking/backend/shipment-tracking/page.meta.js +19 -0
  80. package/dist/modules/shipment_tracking/backend/shipment-tracking/page.meta.js.map +7 -0
  81. package/dist/modules/shipment_tracking/backend/tracking-jobs/page.js +10 -0
  82. package/dist/modules/shipment_tracking/backend/tracking-jobs/page.js.map +7 -0
  83. package/dist/modules/shipment_tracking/backend/tracking-jobs/page.meta.js +19 -0
  84. package/dist/modules/shipment_tracking/backend/tracking-jobs/page.meta.js.map +7 -0
  85. package/dist/modules/shipment_tracking/backend/webhooks/page.js +198 -0
  86. package/dist/modules/shipment_tracking/backend/webhooks/page.js.map +7 -0
  87. package/dist/modules/shipment_tracking/backend/webhooks/page.meta.js +19 -0
  88. package/dist/modules/shipment_tracking/backend/webhooks/page.meta.js.map +7 -0
  89. package/dist/modules/shipment_tracking/ce.js +52 -0
  90. package/dist/modules/shipment_tracking/ce.js.map +7 -0
  91. package/dist/modules/shipment_tracking/cli.js +229 -0
  92. package/dist/modules/shipment_tracking/cli.js.map +7 -0
  93. package/dist/modules/shipment_tracking/commands/bic-configs.js +66 -0
  94. package/dist/modules/shipment_tracking/commands/bic-configs.js.map +7 -0
  95. package/dist/modules/shipment_tracking/commands/carrier-configs.js +65 -0
  96. package/dist/modules/shipment_tracking/commands/carrier-configs.js.map +7 -0
  97. package/dist/modules/shipment_tracking/commands/index.js +9 -0
  98. package/dist/modules/shipment_tracking/commands/index.js.map +7 -0
  99. package/dist/modules/shipment_tracking/commands/location-overrides.js +93 -0
  100. package/dist/modules/shipment_tracking/commands/location-overrides.js.map +7 -0
  101. package/dist/modules/shipment_tracking/commands/shipments.js +200 -0
  102. package/dist/modules/shipment_tracking/commands/shipments.js.map +7 -0
  103. package/dist/modules/shipment_tracking/commands/shipsgo-configs.js +75 -0
  104. package/dist/modules/shipment_tracking/commands/shipsgo-configs.js.map +7 -0
  105. package/dist/modules/shipment_tracking/commands/tracking-jobs.js +120 -0
  106. package/dist/modules/shipment_tracking/commands/tracking-jobs.js.map +7 -0
  107. package/dist/modules/shipment_tracking/commands/tracking.js +140 -0
  108. package/dist/modules/shipment_tracking/commands/tracking.js.map +7 -0
  109. package/dist/modules/shipment_tracking/commands/webhooks.js +115 -0
  110. package/dist/modules/shipment_tracking/commands/webhooks.js.map +7 -0
  111. package/dist/modules/shipment_tracking/components/CombinedTimestampCell.js +168 -0
  112. package/dist/modules/shipment_tracking/components/CombinedTimestampCell.js.map +7 -0
  113. package/dist/modules/shipment_tracking/components/FacilityLocationInput.js +202 -0
  114. package/dist/modules/shipment_tracking/components/FacilityLocationInput.js.map +7 -0
  115. package/dist/modules/shipment_tracking/components/LocationOverrideDrawer.js +501 -0
  116. package/dist/modules/shipment_tracking/components/LocationOverrideDrawer.js.map +7 -0
  117. package/dist/modules/shipment_tracking/components/ShipmentDetailsDrawer.js +818 -0
  118. package/dist/modules/shipment_tracking/components/ShipmentDetailsDrawer.js.map +7 -0
  119. package/dist/modules/shipment_tracking/components/ShipmentDrawer.js +404 -0
  120. package/dist/modules/shipment_tracking/components/ShipmentDrawer.js.map +7 -0
  121. package/dist/modules/shipment_tracking/components/ShipsGoConfigSection.js +200 -0
  122. package/dist/modules/shipment_tracking/components/ShipsGoConfigSection.js.map +7 -0
  123. package/dist/modules/shipment_tracking/components/TimestampCell.js +108 -0
  124. package/dist/modules/shipment_tracking/components/TimestampCell.js.map +7 -0
  125. package/dist/modules/shipment_tracking/components/TrackingJobDrawer.js +343 -0
  126. package/dist/modules/shipment_tracking/components/TrackingJobDrawer.js.map +7 -0
  127. package/dist/modules/shipment_tracking/components/VesselTrackingMap.js +496 -0
  128. package/dist/modules/shipment_tracking/components/VesselTrackingMap.js.map +7 -0
  129. package/dist/modules/shipment_tracking/components/WebhookDrawer.js +332 -0
  130. package/dist/modules/shipment_tracking/components/WebhookDrawer.js.map +7 -0
  131. package/dist/modules/shipment_tracking/data/entities.js +686 -0
  132. package/dist/modules/shipment_tracking/data/entities.js.map +7 -0
  133. package/dist/modules/shipment_tracking/data/validators.js +434 -0
  134. package/dist/modules/shipment_tracking/data/validators.js.map +7 -0
  135. package/dist/modules/shipment_tracking/di.js +60 -0
  136. package/dist/modules/shipment_tracking/di.js.map +7 -0
  137. package/dist/modules/shipment_tracking/encryption.js +12 -0
  138. package/dist/modules/shipment_tracking/encryption.js.map +7 -0
  139. package/dist/modules/shipment_tracking/events.js +87 -0
  140. package/dist/modules/shipment_tracking/events.js.map +7 -0
  141. package/dist/modules/shipment_tracking/hooks/useVesselTracking.js +137 -0
  142. package/dist/modules/shipment_tracking/hooks/useVesselTracking.js.map +7 -0
  143. package/dist/modules/shipment_tracking/i18n/de.json +327 -0
  144. package/dist/modules/shipment_tracking/i18n/en.json +327 -0
  145. package/dist/modules/shipment_tracking/i18n/es.json +327 -0
  146. package/dist/modules/shipment_tracking/i18n/pl.json +327 -0
  147. package/dist/modules/shipment_tracking/index.js +15 -0
  148. package/dist/modules/shipment_tracking/index.js.map +7 -0
  149. package/dist/modules/shipment_tracking/lib/adapters/cma-cgm.js +66 -0
  150. package/dist/modules/shipment_tracking/lib/adapters/cma-cgm.js.map +7 -0
  151. package/dist/modules/shipment_tracking/lib/adapters/cosco-types.js +1 -0
  152. package/dist/modules/shipment_tracking/lib/adapters/cosco-types.js.map +7 -0
  153. package/dist/modules/shipment_tracking/lib/adapters/cosco.js +236 -0
  154. package/dist/modules/shipment_tracking/lib/adapters/cosco.js.map +7 -0
  155. package/dist/modules/shipment_tracking/lib/adapters/evergreen.js +87 -0
  156. package/dist/modules/shipment_tracking/lib/adapters/evergreen.js.map +7 -0
  157. package/dist/modules/shipment_tracking/lib/adapters/hapag-lloyd.js +68 -0
  158. package/dist/modules/shipment_tracking/lib/adapters/hapag-lloyd.js.map +7 -0
  159. package/dist/modules/shipment_tracking/lib/adapters/index.js +20 -0
  160. package/dist/modules/shipment_tracking/lib/adapters/index.js.map +7 -0
  161. package/dist/modules/shipment_tracking/lib/adapters/maersk.js +84 -0
  162. package/dist/modules/shipment_tracking/lib/adapters/maersk.js.map +7 -0
  163. package/dist/modules/shipment_tracking/lib/adapters/msc.js +170 -0
  164. package/dist/modules/shipment_tracking/lib/adapters/msc.js.map +7 -0
  165. package/dist/modules/shipment_tracking/lib/adapters/zim.js +85 -0
  166. package/dist/modules/shipment_tracking/lib/adapters/zim.js.map +7 -0
  167. package/dist/modules/shipment_tracking/lib/audit-helpers.js +20 -0
  168. package/dist/modules/shipment_tracking/lib/audit-helpers.js.map +7 -0
  169. package/dist/modules/shipment_tracking/lib/auth/base64url.js +8 -0
  170. package/dist/modules/shipment_tracking/lib/auth/base64url.js.map +7 -0
  171. package/dist/modules/shipment_tracking/lib/auth/oauth-client.js +38 -0
  172. package/dist/modules/shipment_tracking/lib/auth/oauth-client.js.map +7 -0
  173. package/dist/modules/shipment_tracking/lib/background.js +15 -0
  174. package/dist/modules/shipment_tracking/lib/background.js.map +7 -0
  175. package/dist/modules/shipment_tracking/lib/bic-api-client.js +106 -0
  176. package/dist/modules/shipment_tracking/lib/bic-api-client.js.map +7 -0
  177. package/dist/modules/shipment_tracking/lib/carrier-adapter.js +17 -0
  178. package/dist/modules/shipment_tracking/lib/carrier-adapter.js.map +7 -0
  179. package/dist/modules/shipment_tracking/lib/current-vessel.js +149 -0
  180. package/dist/modules/shipment_tracking/lib/current-vessel.js.map +7 -0
  181. package/dist/modules/shipment_tracking/lib/dcsa-event-mapping.js +124 -0
  182. package/dist/modules/shipment_tracking/lib/dcsa-event-mapping.js.map +7 -0
  183. package/dist/modules/shipment_tracking/lib/dcsa-event-parser.js +112 -0
  184. package/dist/modules/shipment_tracking/lib/dcsa-event-parser.js.map +7 -0
  185. package/dist/modules/shipment_tracking/lib/dcsa-params.js +16 -0
  186. package/dist/modules/shipment_tracking/lib/dcsa-params.js.map +7 -0
  187. package/dist/modules/shipment_tracking/lib/empty-return.js +48 -0
  188. package/dist/modules/shipment_tracking/lib/empty-return.js.map +7 -0
  189. package/dist/modules/shipment_tracking/lib/location-overrides.js +85 -0
  190. package/dist/modules/shipment_tracking/lib/location-overrides.js.map +7 -0
  191. package/dist/modules/shipment_tracking/lib/location-types.js +59 -0
  192. package/dist/modules/shipment_tracking/lib/location-types.js.map +7 -0
  193. package/dist/modules/shipment_tracking/lib/logger.js +113 -0
  194. package/dist/modules/shipment_tracking/lib/logger.js.map +7 -0
  195. package/dist/modules/shipment_tracking/lib/poi-event-processor.js +183 -0
  196. package/dist/modules/shipment_tracking/lib/poi-event-processor.js.map +7 -0
  197. package/dist/modules/shipment_tracking/lib/poi-nats-config.js +52 -0
  198. package/dist/modules/shipment_tracking/lib/poi-nats-config.js.map +7 -0
  199. package/dist/modules/shipment_tracking/lib/poi-types.js +43 -0
  200. package/dist/modules/shipment_tracking/lib/poi-types.js.map +7 -0
  201. package/dist/modules/shipment_tracking/lib/rate-limiter.js +31 -0
  202. package/dist/modules/shipment_tracking/lib/rate-limiter.js.map +7 -0
  203. package/dist/modules/shipment_tracking/lib/route-extraction.js +187 -0
  204. package/dist/modules/shipment_tracking/lib/route-extraction.js.map +7 -0
  205. package/dist/modules/shipment_tracking/lib/route-inference.js +106 -0
  206. package/dist/modules/shipment_tracking/lib/route-inference.js.map +7 -0
  207. package/dist/modules/shipment_tracking/lib/schedule-generator.js +63 -0
  208. package/dist/modules/shipment_tracking/lib/schedule-generator.js.map +7 -0
  209. package/dist/modules/shipment_tracking/lib/shipsgo-client.js +143 -0
  210. package/dist/modules/shipment_tracking/lib/shipsgo-client.js.map +7 -0
  211. package/dist/modules/shipment_tracking/lib/shipsgo-mapper.js +204 -0
  212. package/dist/modules/shipment_tracking/lib/shipsgo-mapper.js.map +7 -0
  213. package/dist/modules/shipment_tracking/lib/status-machine.js +131 -0
  214. package/dist/modules/shipment_tracking/lib/status-machine.js.map +7 -0
  215. package/dist/modules/shipment_tracking/lib/time-extraction.js +41 -0
  216. package/dist/modules/shipment_tracking/lib/time-extraction.js.map +7 -0
  217. package/dist/modules/shipment_tracking/lib/timestamp-utils.js +100 -0
  218. package/dist/modules/shipment_tracking/lib/timestamp-utils.js.map +7 -0
  219. package/dist/modules/shipment_tracking/lib/vessel-api.js +111 -0
  220. package/dist/modules/shipment_tracking/lib/vessel-api.js.map +7 -0
  221. package/dist/modules/shipment_tracking/lib/vessel-cache.js +50 -0
  222. package/dist/modules/shipment_tracking/lib/vessel-cache.js.map +7 -0
  223. package/dist/modules/shipment_tracking/lib/vessel-extraction.js +13 -0
  224. package/dist/modules/shipment_tracking/lib/vessel-extraction.js.map +7 -0
  225. package/dist/modules/shipment_tracking/lib/webhook-dispatcher.js +62 -0
  226. package/dist/modules/shipment_tracking/lib/webhook-dispatcher.js.map +7 -0
  227. package/dist/modules/shipment_tracking/migrations/Migration20260422161208_shipment_tracking.js +48 -0
  228. package/dist/modules/shipment_tracking/migrations/Migration20260422161208_shipment_tracking.js.map +7 -0
  229. package/dist/modules/shipment_tracking/migrations/Migration20260529120000_shipment_tracking_poi_region_names.js +15 -0
  230. package/dist/modules/shipment_tracking/migrations/Migration20260529120000_shipment_tracking_poi_region_names.js.map +7 -0
  231. package/dist/modules/shipment_tracking/migrations/Migration20260821120000_shipment_tracking_shipsgo.js +47 -0
  232. package/dist/modules/shipment_tracking/migrations/Migration20260821120000_shipment_tracking_shipsgo.js.map +7 -0
  233. package/dist/modules/shipment_tracking/notifications.js +145 -0
  234. package/dist/modules/shipment_tracking/notifications.js.map +7 -0
  235. package/dist/modules/shipment_tracking/services/carrierRegistry.js +24 -0
  236. package/dist/modules/shipment_tracking/services/carrierRegistry.js.map +7 -0
  237. package/dist/modules/shipment_tracking/services/poiNatsService.js +397 -0
  238. package/dist/modules/shipment_tracking/services/poiNatsService.js.map +7 -0
  239. package/dist/modules/shipment_tracking/services/shipsGoProvider.js +163 -0
  240. package/dist/modules/shipment_tracking/services/shipsGoProvider.js.map +7 -0
  241. package/dist/modules/shipment_tracking/services/trackingService.js +1283 -0
  242. package/dist/modules/shipment_tracking/services/trackingService.js.map +7 -0
  243. package/dist/modules/shipment_tracking/services/webhookService.js +263 -0
  244. package/dist/modules/shipment_tracking/services/webhookService.js.map +7 -0
  245. package/dist/modules/shipment_tracking/setup.js +72 -0
  246. package/dist/modules/shipment_tracking/setup.js.map +7 -0
  247. package/dist/modules/shipment_tracking/subscribers/cargo-event-created.js +60 -0
  248. package/dist/modules/shipment_tracking/subscribers/cargo-event-created.js.map +7 -0
  249. package/dist/modules/shipment_tracking/subscribers/dcsa-event-webhook-dispatch.js +58 -0
  250. package/dist/modules/shipment_tracking/subscribers/dcsa-event-webhook-dispatch.js.map +7 -0
  251. package/dist/modules/shipment_tracking/subscribers/poi-event-notification.js +82 -0
  252. package/dist/modules/shipment_tracking/subscribers/poi-event-notification.js.map +7 -0
  253. package/dist/modules/shipment_tracking/subscribers/poi-event-webhook-dispatch.js +53 -0
  254. package/dist/modules/shipment_tracking/subscribers/poi-event-webhook-dispatch.js.map +7 -0
  255. package/dist/modules/shipment_tracking/subscribers/shipment-status-changed.js +28 -0
  256. package/dist/modules/shipment_tracking/subscribers/shipment-status-changed.js.map +7 -0
  257. package/dist/modules/shipment_tracking/subscribers/terminal-event-ingest.js +67 -0
  258. package/dist/modules/shipment_tracking/subscribers/terminal-event-ingest.js.map +7 -0
  259. package/dist/modules/shipment_tracking/subscribers/tracking-job-initial-poll.js +30 -0
  260. package/dist/modules/shipment_tracking/subscribers/tracking-job-initial-poll.js.map +7 -0
  261. package/dist/modules/shipment_tracking/subscribers/tracking-job-webhook-dispatch.js +19 -0
  262. package/dist/modules/shipment_tracking/subscribers/tracking-job-webhook-dispatch.js.map +7 -0
  263. package/dist/modules/shipment_tracking/tables/index.js +6 -0
  264. package/dist/modules/shipment_tracking/tables/index.js.map +7 -0
  265. package/dist/modules/shipment_tracking/tables/tracking_job/table.client.js +284 -0
  266. package/dist/modules/shipment_tracking/tables/tracking_job/table.client.js.map +7 -0
  267. package/dist/modules/shipment_tracking/tables/tracking_job/table.js +36 -0
  268. package/dist/modules/shipment_tracking/tables/tracking_job/table.js.map +7 -0
  269. package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/config.js +25 -0
  270. package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/config.js.map +7 -0
  271. package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.client.js +355 -0
  272. package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.client.js.map +7 -0
  273. package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.js +26 -0
  274. package/dist/modules/shipment_tracking/widgets/dashboard/ships-map/widget.js.map +7 -0
  275. package/dist/types/assets.d.js +1 -0
  276. package/dist/types/assets.d.js.map +7 -0
  277. package/generated/entities/bic_config/index.ts +20 -0
  278. package/generated/entities/carrier_config/index.ts +23 -0
  279. package/generated/entities/location_override/index.ts +26 -0
  280. package/generated/entities/shipment/index.ts +46 -0
  281. package/generated/entities/ships_go_config/index.ts +25 -0
  282. package/generated/entities/tracking_event/index.ts +60 -0
  283. package/generated/entities/tracking_job/index.ts +33 -0
  284. package/generated/entities/webhook/index.ts +22 -0
  285. package/generated/entities/webhook_delivery/index.ts +22 -0
  286. package/generated/entities.ids.generated.ts +47 -0
  287. package/package.json +81 -0
  288. package/src/index.ts +39 -0
  289. package/src/modules/shipment_tracking/__integration__/TC-TRACK-410-shipsgo-config-api.spec.ts +133 -0
  290. package/src/modules/shipment_tracking/__integration__/meta.ts +4 -0
  291. package/src/modules/shipment_tracking/__tests__/empty-return-stop.test.ts +231 -0
  292. package/src/modules/shipment_tracking/__tests__/fixtures/hapag-lloyd-transship-container.json +702 -0
  293. package/src/modules/shipment_tracking/__tests__/fixtures/index.ts +434 -0
  294. package/src/modules/shipment_tracking/__tests__/fixtures/maersk-multi-transship-completed.json +1518 -0
  295. package/src/modules/shipment_tracking/__tests__/fixtures/maersk-transship-intransit.json +558 -0
  296. package/src/modules/shipment_tracking/__tests__/fixtures/msc-direct.json +222 -0
  297. package/src/modules/shipment_tracking/__tests__/fixtures/msc-multi-transship-completed.json +974 -0
  298. package/src/modules/shipment_tracking/__tests__/fixtures/msc-multi.json +4846 -0
  299. package/src/modules/shipment_tracking/__tests__/fixtures/msc-transship.json +409 -0
  300. package/src/modules/shipment_tracking/__tests__/fixtures/poi-events.json +240 -0
  301. package/src/modules/shipment_tracking/__tests__/fixtures/synthetic/index.ts +138 -0
  302. package/src/modules/shipment_tracking/__tests__/fixtures/synthetic-eta-updates.json +288 -0
  303. package/src/modules/shipment_tracking/__tests__/fixtures/synthetic-route-inference.json +201 -0
  304. package/src/modules/shipment_tracking/__tests__/fixtures/synthetic-voyage-progression.json +410 -0
  305. package/src/modules/shipment_tracking/__tests__/shipsgo-air-mapper.test.ts +157 -0
  306. package/src/modules/shipment_tracking/__tests__/shipsgo-client.test.ts +155 -0
  307. package/src/modules/shipment_tracking/__tests__/shipsgo-mapper.test.ts +201 -0
  308. package/src/modules/shipment_tracking/__tests__/shipsgo-poll.test.ts +357 -0
  309. package/src/modules/shipment_tracking/__tests__/shipsgo-provider.test.ts +364 -0
  310. package/src/modules/shipment_tracking/__tests__/trackingService.integration.test.ts +2248 -0
  311. package/src/modules/shipment_tracking/__tests__/webhookPayload.test.ts +151 -0
  312. package/src/modules/shipment_tracking/acl.ts +18 -0
  313. package/src/modules/shipment_tracking/api/bic-configs/route.ts +206 -0
  314. package/src/modules/shipment_tracking/api/bic-configs/test/route.ts +108 -0
  315. package/src/modules/shipment_tracking/api/cargo-events/route.ts +101 -0
  316. package/src/modules/shipment_tracking/api/carrier-configs/route.ts +142 -0
  317. package/src/modules/shipment_tracking/api/dashboard/ships-map/route.ts +246 -0
  318. package/src/modules/shipment_tracking/api/location-overrides/route.ts +185 -0
  319. package/src/modules/shipment_tracking/api/openapi.ts +28 -0
  320. package/src/modules/shipment_tracking/api/shipments/route.ts +197 -0
  321. package/src/modules/shipment_tracking/api/shipsgo-configs/route.ts +215 -0
  322. package/src/modules/shipment_tracking/api/tracking-jobs/route.ts +149 -0
  323. package/src/modules/shipment_tracking/api/tracking-jobs/sync/route.ts +116 -0
  324. package/src/modules/shipment_tracking/api/utils.ts +10 -0
  325. package/src/modules/shipment_tracking/api/webhooks/deliveries/route.ts +82 -0
  326. package/src/modules/shipment_tracking/api/webhooks/route.ts +138 -0
  327. package/src/modules/shipment_tracking/api/webhooks/test/route.ts +84 -0
  328. package/src/modules/shipment_tracking/assets/carriers/cma_cgm.png +0 -0
  329. package/src/modules/shipment_tracking/assets/carriers/cosco.png +0 -0
  330. package/src/modules/shipment_tracking/assets/carriers/evergreen.png +0 -0
  331. package/src/modules/shipment_tracking/assets/carriers/hapag_lloyd.png +0 -0
  332. package/src/modules/shipment_tracking/assets/carriers/maersk.png +0 -0
  333. package/src/modules/shipment_tracking/assets/carriers/msc.png +0 -0
  334. package/src/modules/shipment_tracking/assets/carriers/one.png +0 -0
  335. package/src/modules/shipment_tracking/assets/carriers/oocl.png +0 -0
  336. package/src/modules/shipment_tracking/assets/carriers/yang_ming.png +0 -0
  337. package/src/modules/shipment_tracking/assets/carriers/zim.png +0 -0
  338. package/src/modules/shipment_tracking/assets/index.ts +66 -0
  339. package/src/modules/shipment_tracking/backend/carrier-configs/page.meta.ts +16 -0
  340. package/src/modules/shipment_tracking/backend/carrier-configs/page.tsx +782 -0
  341. package/src/modules/shipment_tracking/backend/location-overrides/page.meta.ts +16 -0
  342. package/src/modules/shipment_tracking/backend/location-overrides/page.tsx +241 -0
  343. package/src/modules/shipment_tracking/backend/shipment-tracking/page.meta.ts +16 -0
  344. package/src/modules/shipment_tracking/backend/shipment-tracking/page.tsx +340 -0
  345. package/src/modules/shipment_tracking/backend/tracking-jobs/page.meta.ts +16 -0
  346. package/src/modules/shipment_tracking/backend/tracking-jobs/page.tsx +16 -0
  347. package/src/modules/shipment_tracking/backend/webhooks/page.meta.ts +16 -0
  348. package/src/modules/shipment_tracking/backend/webhooks/page.tsx +228 -0
  349. package/src/modules/shipment_tracking/ce.ts +48 -0
  350. package/src/modules/shipment_tracking/cli.ts +321 -0
  351. package/src/modules/shipment_tracking/commands/bic-configs.ts +94 -0
  352. package/src/modules/shipment_tracking/commands/carrier-configs.ts +89 -0
  353. package/src/modules/shipment_tracking/commands/index.ts +8 -0
  354. package/src/modules/shipment_tracking/commands/location-overrides.ts +119 -0
  355. package/src/modules/shipment_tracking/commands/shipments.ts +247 -0
  356. package/src/modules/shipment_tracking/commands/shipsgo-configs.ts +101 -0
  357. package/src/modules/shipment_tracking/commands/tracking-jobs.ts +158 -0
  358. package/src/modules/shipment_tracking/commands/tracking.ts +230 -0
  359. package/src/modules/shipment_tracking/commands/webhooks.ts +159 -0
  360. package/src/modules/shipment_tracking/components/CombinedTimestampCell.tsx +236 -0
  361. package/src/modules/shipment_tracking/components/FacilityLocationInput.tsx +235 -0
  362. package/src/modules/shipment_tracking/components/LocationOverrideDrawer.tsx +586 -0
  363. package/src/modules/shipment_tracking/components/ShipmentDetailsDrawer.tsx +1364 -0
  364. package/src/modules/shipment_tracking/components/ShipmentDrawer.tsx +498 -0
  365. package/src/modules/shipment_tracking/components/ShipsGoConfigSection.tsx +266 -0
  366. package/src/modules/shipment_tracking/components/TimestampCell.tsx +146 -0
  367. package/src/modules/shipment_tracking/components/TrackingJobDrawer.tsx +467 -0
  368. package/src/modules/shipment_tracking/components/VesselTrackingMap.tsx +761 -0
  369. package/src/modules/shipment_tracking/components/WebhookDrawer.tsx +434 -0
  370. package/src/modules/shipment_tracking/data/entities.ts +748 -0
  371. package/src/modules/shipment_tracking/data/validators.ts +523 -0
  372. package/src/modules/shipment_tracking/di.ts +73 -0
  373. package/src/modules/shipment_tracking/encryption.ts +17 -0
  374. package/src/modules/shipment_tracking/events.ts +122 -0
  375. package/src/modules/shipment_tracking/hooks/useVesselTracking.ts +297 -0
  376. package/src/modules/shipment_tracking/i18n/de.json +327 -0
  377. package/src/modules/shipment_tracking/i18n/en.json +327 -0
  378. package/src/modules/shipment_tracking/i18n/es.json +327 -0
  379. package/src/modules/shipment_tracking/i18n/pl.json +327 -0
  380. package/src/modules/shipment_tracking/index.ts +13 -0
  381. package/src/modules/shipment_tracking/lib/__tests__/dcsa-event-mapping.test.ts +385 -0
  382. package/src/modules/shipment_tracking/lib/__tests__/dcsa-event-parser.test.ts +398 -0
  383. package/src/modules/shipment_tracking/lib/__tests__/empty-return.test.ts +174 -0
  384. package/src/modules/shipment_tracking/lib/__tests__/poi-event-processor.test.ts +699 -0
  385. package/src/modules/shipment_tracking/lib/__tests__/route-inference.test.ts +402 -0
  386. package/src/modules/shipment_tracking/lib/__tests__/status-machine.test.ts +447 -0
  387. package/src/modules/shipment_tracking/lib/__tests__/time-extraction.test.ts +331 -0
  388. package/src/modules/shipment_tracking/lib/__tests__/timestamp-utils.test.ts +260 -0
  389. package/src/modules/shipment_tracking/lib/__tests__/vessel-api.test.ts +125 -0
  390. package/src/modules/shipment_tracking/lib/__tests__/vessel-extraction.test.ts +108 -0
  391. package/src/modules/shipment_tracking/lib/adapters/cma-cgm.ts +84 -0
  392. package/src/modules/shipment_tracking/lib/adapters/cosco-types.ts +145 -0
  393. package/src/modules/shipment_tracking/lib/adapters/cosco.ts +314 -0
  394. package/src/modules/shipment_tracking/lib/adapters/evergreen.ts +107 -0
  395. package/src/modules/shipment_tracking/lib/adapters/hapag-lloyd.ts +88 -0
  396. package/src/modules/shipment_tracking/lib/adapters/index.ts +18 -0
  397. package/src/modules/shipment_tracking/lib/adapters/maersk.ts +104 -0
  398. package/src/modules/shipment_tracking/lib/adapters/msc.ts +223 -0
  399. package/src/modules/shipment_tracking/lib/adapters/zim.ts +106 -0
  400. package/src/modules/shipment_tracking/lib/audit-helpers.ts +29 -0
  401. package/src/modules/shipment_tracking/lib/auth/base64url.ts +4 -0
  402. package/src/modules/shipment_tracking/lib/auth/oauth-client.ts +53 -0
  403. package/src/modules/shipment_tracking/lib/background.ts +24 -0
  404. package/src/modules/shipment_tracking/lib/bic-api-client.ts +199 -0
  405. package/src/modules/shipment_tracking/lib/carrier-adapter.ts +136 -0
  406. package/src/modules/shipment_tracking/lib/current-vessel.ts +274 -0
  407. package/src/modules/shipment_tracking/lib/dcsa-event-mapping.ts +202 -0
  408. package/src/modules/shipment_tracking/lib/dcsa-event-parser.ts +163 -0
  409. package/src/modules/shipment_tracking/lib/dcsa-params.ts +14 -0
  410. package/src/modules/shipment_tracking/lib/empty-return.ts +122 -0
  411. package/src/modules/shipment_tracking/lib/location-overrides.ts +150 -0
  412. package/src/modules/shipment_tracking/lib/location-types.ts +154 -0
  413. package/src/modules/shipment_tracking/lib/logger.ts +195 -0
  414. package/src/modules/shipment_tracking/lib/poi-event-processor.ts +302 -0
  415. package/src/modules/shipment_tracking/lib/poi-nats-config.ts +134 -0
  416. package/src/modules/shipment_tracking/lib/poi-types.ts +198 -0
  417. package/src/modules/shipment_tracking/lib/rate-limiter.ts +58 -0
  418. package/src/modules/shipment_tracking/lib/route-extraction.ts +439 -0
  419. package/src/modules/shipment_tracking/lib/route-inference.ts +238 -0
  420. package/src/modules/shipment_tracking/lib/schedule-generator.ts +92 -0
  421. package/src/modules/shipment_tracking/lib/shipsgo-client.ts +350 -0
  422. package/src/modules/shipment_tracking/lib/shipsgo-mapper.ts +296 -0
  423. package/src/modules/shipment_tracking/lib/status-machine.ts +230 -0
  424. package/src/modules/shipment_tracking/lib/time-extraction.ts +85 -0
  425. package/src/modules/shipment_tracking/lib/timestamp-utils.ts +244 -0
  426. package/src/modules/shipment_tracking/lib/vessel-api.ts +299 -0
  427. package/src/modules/shipment_tracking/lib/vessel-cache.ts +102 -0
  428. package/src/modules/shipment_tracking/lib/vessel-extraction.ts +39 -0
  429. package/src/modules/shipment_tracking/lib/webhook-dispatcher.ts +93 -0
  430. package/src/modules/shipment_tracking/migrations/.snapshot-open-mercato.json +2914 -0
  431. package/src/modules/shipment_tracking/migrations/Migration20260422161208_shipment_tracking.ts +60 -0
  432. package/src/modules/shipment_tracking/migrations/Migration20260529120000_shipment_tracking_poi_region_names.ts +22 -0
  433. package/src/modules/shipment_tracking/migrations/Migration20260821120000_shipment_tracking_shipsgo.ts +65 -0
  434. package/src/modules/shipment_tracking/notifications.ts +151 -0
  435. package/src/modules/shipment_tracking/services/carrierRegistry.ts +29 -0
  436. package/src/modules/shipment_tracking/services/poiNatsService.ts +526 -0
  437. package/src/modules/shipment_tracking/services/shipsGoProvider.ts +231 -0
  438. package/src/modules/shipment_tracking/services/trackingService.ts +1777 -0
  439. package/src/modules/shipment_tracking/services/webhookService.ts +347 -0
  440. package/src/modules/shipment_tracking/setup.ts +99 -0
  441. package/src/modules/shipment_tracking/subscribers/__tests__/terminal-event-ingest.test.ts +141 -0
  442. package/src/modules/shipment_tracking/subscribers/cargo-event-created.ts +115 -0
  443. package/src/modules/shipment_tracking/subscribers/dcsa-event-webhook-dispatch.ts +115 -0
  444. package/src/modules/shipment_tracking/subscribers/poi-event-notification.ts +144 -0
  445. package/src/modules/shipment_tracking/subscribers/poi-event-webhook-dispatch.ts +94 -0
  446. package/src/modules/shipment_tracking/subscribers/shipment-status-changed.ts +41 -0
  447. package/src/modules/shipment_tracking/subscribers/terminal-event-ingest.ts +109 -0
  448. package/src/modules/shipment_tracking/subscribers/tracking-job-initial-poll.ts +53 -0
  449. package/src/modules/shipment_tracking/subscribers/tracking-job-webhook-dispatch.ts +50 -0
  450. package/src/modules/shipment_tracking/tables/index.ts +4 -0
  451. package/src/modules/shipment_tracking/tables/tracking_job/table.client.tsx +337 -0
  452. package/src/modules/shipment_tracking/tables/tracking_job/table.ts +34 -0
  453. package/src/modules/shipment_tracking/widgets/dashboard/ships-map/config.ts +32 -0
  454. package/src/modules/shipment_tracking/widgets/dashboard/ships-map/widget.client.tsx +448 -0
  455. package/src/modules/shipment_tracking/widgets/dashboard/ships-map/widget.ts +25 -0
  456. package/src/types/assets.d.ts +5 -0
@@ -0,0 +1,1364 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useState, useMemo, useCallback } from 'react'
5
+ import { useQuery } from '@tanstack/react-query'
6
+ import Image from 'next/image'
7
+ import {
8
+ Ship,
9
+ MapPin,
10
+ Anchor,
11
+ Calendar,
12
+ Package,
13
+ ChevronDown,
14
+ ChevronUp,
15
+ Clock,
16
+ ArrowRight,
17
+ ExternalLink,
18
+ Navigation,
19
+ Radio,
20
+ Truck,
21
+ Warehouse,
22
+ } from 'lucide-react'
23
+ import {
24
+ Sheet,
25
+ SheetContent,
26
+ } from '@freighttech/ui/primitives/sheet'
27
+ import { Button } from '@freighttech/ui/primitives/button'
28
+ import { Badge } from '@freighttech/ui/primitives/badge'
29
+ import { Spinner } from '@freighttech/ui/primitives/spinner'
30
+ import { apiCall } from '@freighttech/ui/backend/utils/apiCall'
31
+ import { useT, useLocale } from '@open-mercato/shared/lib/i18n/context'
32
+ import type { TimestampEntry } from './CombinedTimestampCell'
33
+ import { getCarrierLogo } from '../assets'
34
+ import { VesselTrackingMap } from './VesselTrackingMap'
35
+ import { getCurrentVessel, type CurrentVesselInfo } from '../lib/current-vessel'
36
+ import type { RouteStopEntry, CargoEventEntry } from '../lib/route-extraction'
37
+
38
+ // ─── Types ───────────────────────────────────────────────────
39
+
40
+ interface Coords {
41
+ latitude: number
42
+ longitude: number
43
+ }
44
+
45
+ interface FacilityLocation {
46
+ name: string
47
+ unlocode: string | null
48
+ countryCode: string | null
49
+ facilityCode: string | null
50
+ facilityCodeListProvider: 'BIC' | 'SMDG' | null
51
+ facilityTypeCode: string | null
52
+ address: string | null
53
+ coords: Coords | null
54
+ operatorName: string | null
55
+ source: 'dcsa' | 'bic' | 'manual'
56
+ }
57
+
58
+ interface TrackingEventData {
59
+ id: string
60
+ eventType: string
61
+ eventCode: string
62
+ eventClassifierCode: 'ACT' | 'PLN' | 'EST' | null
63
+ eventDateTime: string
64
+ description?: string | null
65
+ locationName?: string | null
66
+ locationUnlocode?: string | null
67
+ // Localized region names for AIS POI events (e.g. open-water waypoints without a UN/LOCODE)
68
+ regionNamePl?: string | null
69
+ regionNameEn?: string | null
70
+ vesselName?: string | null
71
+ vesselImo?: string | null
72
+ voyageNumber?: string | null
73
+ equipmentReference?: string | null
74
+ isTransshipmentMove?: boolean | null
75
+ // Facility/terminal details
76
+ facilityCode?: string | null
77
+ facilityCodeListProvider?: 'BIC' | 'SMDG' | null
78
+ facilityTypeCode?: string | null
79
+ facilityAddress?: string | null
80
+ latitude?: number | null
81
+ longitude?: number | null
82
+ }
83
+
84
+ interface SealInfo {
85
+ number: string
86
+ source?: string | null
87
+ type?: string | null
88
+ }
89
+
90
+ interface ShipmentDetailsData {
91
+ id: string
92
+ status: string
93
+ carrierCode?: string | null
94
+ containerNumber?: string | null
95
+ bookingNumber?: string | null
96
+ isoEquipmentCode?: string | null
97
+ bolNumber?: string | null
98
+ etdTimestamps?: TimestampEntry[] | null
99
+ etaTimestamps?: TimestampEntry[] | null
100
+ atdTimestamps?: TimestampEntry[] | null
101
+ ataTimestamps?: TimestampEntry[] | null
102
+ // Location data (JSONB)
103
+ originLocation?: FacilityLocation | null
104
+ destinationLocation?: FacilityLocation | null
105
+ vesselName?: string | null
106
+ vesselImo?: string | null
107
+ voyageNumber?: string | null
108
+ // Denormalized route and events (no longer need separate API call)
109
+ routeStops?: RouteStop[] | null
110
+ cargoEvents?: TrackingEventData[] | null
111
+ // Aggregated seals from tracking events
112
+ seals?: SealInfo[] | null
113
+ // Kept for backward compatibility
114
+ trackingJob?: { id: string } | null
115
+ }
116
+
117
+ interface RouteStop {
118
+ location: string
119
+ unlocode?: string
120
+ type: 'origin' | 'transshipment' | 'port_call' | 'destination'
121
+ vesselName?: string
122
+ vesselImo?: string | null
123
+ ata?: string | null
124
+ atd?: string | null
125
+ eta?: string | null
126
+ etd?: string | null
127
+ // Facility/terminal details
128
+ facilityCode?: string | null
129
+ facilityCodeListProvider?: 'BIC' | 'SMDG' | null
130
+ facilityTypeCode?: string | null
131
+ facilityAddress?: string | null
132
+ coords?: Coords | null
133
+ }
134
+
135
+ // ─── Helpers ─────────────────────────────────────────────────
136
+
137
+ function getPrimaryTimestamp(timestamps: TimestampEntry[] | null | undefined): string | null {
138
+ if (!timestamps || timestamps.length === 0) return null
139
+ const latest = timestamps.reduce((best, entry) =>
140
+ entry.updatedAt > best.updatedAt ? entry : best
141
+ )
142
+ return latest.value
143
+ }
144
+
145
+ function formatShortDate(dateString: string | null | undefined): string {
146
+ if (!dateString) return '-'
147
+ const date = new Date(dateString)
148
+ if (Number.isNaN(date.getTime())) return '-'
149
+ return date.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
150
+ }
151
+
152
+ function formatDateOnly(dateString: string | null | undefined): string {
153
+ if (!dateString) return '-'
154
+ const date = new Date(dateString)
155
+ if (Number.isNaN(date.getTime())) return '-'
156
+ return date.toLocaleDateString(undefined, { day: '2-digit', month: '2-digit' })
157
+ }
158
+
159
+ /**
160
+ * Pick the human-friendly POI region name for the active locale, falling back across
161
+ * locales (the producer only emits pl + en). Returns null when no region name is set,
162
+ * so callers can fall back to locationName / POI code.
163
+ */
164
+ function pickRegionName(
165
+ event: Pick<TrackingEventData, 'regionNamePl' | 'regionNameEn'>,
166
+ locale: string
167
+ ): string | null {
168
+ if (locale === 'pl') return event.regionNamePl || event.regionNameEn || null
169
+ return event.regionNameEn || event.regionNamePl || null
170
+ }
171
+
172
+ /**
173
+ * Calculate transit time in days between departure and arrival.
174
+ * Uses ATD as start, and ATA (if arrived) or ETA (if in transit) as end.
175
+ */
176
+ function calculateTransitDays(
177
+ atd: string | null | undefined,
178
+ ata: string | null | undefined,
179
+ eta: string | null | undefined
180
+ ): number | null {
181
+ const startDate = atd ? new Date(atd) : null
182
+ const endDate = ata ? new Date(ata) : (eta ? new Date(eta) : null)
183
+ if (!startDate || !endDate) return null
184
+ if (Number.isNaN(startDate.getTime()) || Number.isNaN(endDate.getTime())) return null
185
+ const diffMs = endDate.getTime() - startDate.getTime()
186
+ return Math.ceil(diffMs / (1000 * 60 * 60 * 24))
187
+ }
188
+
189
+ /**
190
+ * Calculate delay in days by comparing the oldest timestamp to the newest.
191
+ * This shows drift over time as estimates get updated.
192
+ * Returns positive for delays (late), negative for ahead of schedule.
193
+ */
194
+ function calculateDelayFromHistory(timestamps: TimestampEntry[] | null | undefined): number | null {
195
+ if (!timestamps || timestamps.length < 2) return null
196
+ // Sort by updatedAt to get oldest (original estimate) and newest (latest update)
197
+ const sorted = [...timestamps].sort((a, b) =>
198
+ new Date(a.updatedAt).getTime() - new Date(b.updatedAt).getTime()
199
+ )
200
+ const oldest = new Date(sorted[0].value)
201
+ const newest = new Date(sorted[sorted.length - 1].value)
202
+ if (Number.isNaN(oldest.getTime()) || Number.isNaN(newest.getTime())) return null
203
+ const diffMs = newest.getTime() - oldest.getTime()
204
+ const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24))
205
+ return diffDays !== 0 ? diffDays : null
206
+ }
207
+
208
+ /**
209
+ * Calculate delay by comparing planned (ETD/ETA) to actual (ATD/ATA).
210
+ * Returns positive for delays (late), negative for ahead of schedule.
211
+ */
212
+ function calculateDelayFromPlanned(
213
+ planned: string | null | undefined,
214
+ actual: string | null | undefined
215
+ ): number | null {
216
+ if (!planned || !actual) return null
217
+ const plannedDate = new Date(planned)
218
+ const actualDate = new Date(actual)
219
+ if (Number.isNaN(plannedDate.getTime()) || Number.isNaN(actualDate.getTime())) return null
220
+ const diffMs = actualDate.getTime() - plannedDate.getTime()
221
+ const diffDays = Math.round(diffMs / (1000 * 60 * 60 * 24))
222
+ return diffDays !== 0 ? diffDays : null
223
+ }
224
+
225
+ // NOTE: getCarrierLogo() is now imported from '../assets' and returns StaticImageData
226
+ // for use with next/image component
227
+
228
+ /**
229
+ * Build carrier tracking URL with optional reference number.
230
+ * Some carriers support direct deep-linking to a specific shipment.
231
+ */
232
+ function getCarrierTrackingUrl(
233
+ carrierCode: string | null | undefined,
234
+ reference?: string | null
235
+ ): string | null {
236
+ if (!carrierCode) return null
237
+ const code = carrierCode.toLowerCase()
238
+ const ref = reference?.trim()
239
+
240
+ // Carriers that support direct deep-linking with reference in URL path
241
+ if ((code === 'maeu' || code === 'maersk') && ref) {
242
+ return `https://www.maersk.com/tracking/${encodeURIComponent(ref)}`
243
+ }
244
+
245
+ // Fallback to generic tracking pages
246
+ const urls: Record<string, string> = {
247
+ maeu: 'https://www.maersk.com/tracking',
248
+ maersk: 'https://www.maersk.com/tracking',
249
+ mscu: 'https://www.msc.com/track-a-shipment',
250
+ msc: 'https://www.msc.com/track-a-shipment',
251
+ cmdu: 'https://www.cma-cgm.com/ebusiness/tracking',
252
+ 'cma-cgm': 'https://www.cma-cgm.com/ebusiness/tracking',
253
+ hlcu: 'https://www.hapag-lloyd.com/en/online-business/track/track-by-container-solution.html',
254
+ 'hapag-lloyd': 'https://www.hapag-lloyd.com/en/online-business/track/track-by-container-solution.html',
255
+ cosu: 'https://elines.coscoshipping.com/ebusiness/cargoTracking',
256
+ cosco: 'https://elines.coscoshipping.com/ebusiness/cargoTracking',
257
+ eglv: 'https://www.evergreen-line.com/trkc/',
258
+ evergreen: 'https://www.evergreen-line.com/trkc/',
259
+ oolu: 'https://www.oocl.com/eng/ourservices/eservices/cargotracking',
260
+ oocl: 'https://www.oocl.com/eng/ourservices/eservices/cargotracking',
261
+ one: 'https://ecomm.one-line.com/ecom/CUP_HOM_3301.do',
262
+ oney: 'https://ecomm.one-line.com/ecom/CUP_HOM_3301.do',
263
+ ymlu: 'https://www.yangming.com/e-service/Track_Trace/track_trace_cargo_tracking.aspx',
264
+ 'yang-ming': 'https://www.yangming.com/e-service/Track_Trace/track_trace_cargo_tracking.aspx',
265
+ zimu: 'https://www.zim.com/tools/track-a-shipment',
266
+ zim: 'https://www.zim.com/tools/track-a-shipment',
267
+ }
268
+ return urls[code] || null
269
+ }
270
+
271
+ function getStatusBadgeVariant(status: string): 'default' | 'secondary' | 'destructive' | 'outline' {
272
+ switch (status) {
273
+ case 'ARRIVED':
274
+ case 'DELIVERED':
275
+ return 'default'
276
+ case 'IN_TRANSIT':
277
+ case 'DEPARTED':
278
+ return 'secondary'
279
+ case 'PENDING':
280
+ case 'BOOKED':
281
+ return 'outline'
282
+ default:
283
+ return 'outline'
284
+ }
285
+ }
286
+
287
+ // NOTE: extractRouteFromEvents logic has been moved to lib/route-extraction.ts
288
+ // and is now executed server-side in TrackingService.deriveShipmentStateFromEvents()
289
+ // The computed route is stored as `routeStops` JSONB on the shipment entity.
290
+
291
+ /**
292
+ * Determines facility location type based on facility code and vessel presence.
293
+ *
294
+ * Facility types (DCSA standard):
295
+ * - POTE = Port Terminal (always port)
296
+ * - INTE = Intermodal Terminal (port if vessel, inland if no vessel)
297
+ * - DEPO = Depot (always inland)
298
+ * - CLOC = Client Location (always inland)
299
+ * - null/undefined = Unknown (use MapPin icon)
300
+ *
301
+ * Returns: 'port' | 'inland' | 'unknown'
302
+ */
303
+ function getFacilityLocationType(
304
+ facilityTypeCode: string | null | undefined,
305
+ vesselName?: string | null
306
+ ): 'port' | 'inland' | 'unknown' {
307
+ // Port terminals are always at ports
308
+ if (facilityTypeCode === 'POTE') {
309
+ return 'port'
310
+ }
311
+
312
+ // Depots and client locations are always inland
313
+ if (facilityTypeCode === 'DEPO' || facilityTypeCode === 'CLOC') {
314
+ return 'inland'
315
+ }
316
+
317
+ // Intermodal terminals: port if vessel involved, inland otherwise
318
+ if (facilityTypeCode === 'INTE') {
319
+ return vesselName ? 'port' : 'inland'
320
+ }
321
+
322
+ // Unknown facility type - use MapPin
323
+ return 'unknown'
324
+ }
325
+
326
+ /**
327
+ * Get the appropriate icon for a tracking event based on event code, facility type, and vessel.
328
+ *
329
+ * Icon selection logic:
330
+ * - Port arrivals: Anchor (vessel arriving at port)
331
+ * - Port departures: Ship (vessel departing from port)
332
+ * - Inland arrivals (depot/client/inland intermodal): Warehouse
333
+ * - Inland departures (depot/client/inland intermodal): Truck
334
+ * - Unknown facility arrivals/departures: MapPin (generic location)
335
+ * - Load/Discharge: Package (cargo handling)
336
+ * - Proximity events: Navigation (approaching)
337
+ * - Waypoints: Radio (tracking point)
338
+ */
339
+ function getEventIcon(
340
+ eventCode: string,
341
+ facilityTypeCode?: string | null,
342
+ vesselName?: string | null
343
+ ) {
344
+ const locationType = getFacilityLocationType(facilityTypeCode, vesselName)
345
+
346
+ // Gate events
347
+ if (eventCode === 'GTIN') {
348
+ if (locationType === 'port') return Anchor
349
+ if (locationType === 'inland') return Warehouse
350
+ return MapPin
351
+ }
352
+ if (eventCode === 'GTOT') {
353
+ if (locationType === 'port') return Ship
354
+ if (locationType === 'inland') return Truck
355
+ return MapPin
356
+ }
357
+
358
+ switch (eventCode) {
359
+ // Standard DCSA events - check facility type for arrivals/departures
360
+ case 'ARRI':
361
+ if (locationType === 'port') return Anchor
362
+ if (locationType === 'inland') return Warehouse
363
+ return MapPin
364
+ case 'DEPA':
365
+ if (locationType === 'port') return Ship
366
+ if (locationType === 'inland') return Truck
367
+ return MapPin
368
+ case 'LOAD':
369
+ case 'DISC':
370
+ return Package
371
+ // POI/AIS events (always at ports/terminals)
372
+ case 'PARR': // Port Arrival
373
+ case 'TARR': // Terminal Arrival
374
+ return Anchor
375
+ case 'PPRD': // Port Departure
376
+ case 'TPRD': // Terminal Departure
377
+ return Ship
378
+ case 'PPRA': // Port Proximity Arrival
379
+ case 'TPRA': // Terminal Proximity Arrival
380
+ return Navigation
381
+ case 'WAYR': // Waypoint Reached
382
+ return Radio
383
+ default:
384
+ return Package
385
+ }
386
+ }
387
+
388
+ /**
389
+ * Hook to get translated event label.
390
+ * Uses i18n translations from shipment_tracking.event_codes
391
+ */
392
+ function useEventLabel() {
393
+ const t = useT()
394
+
395
+ return useCallback((eventCode: string): string => {
396
+ const key = `shipment_tracking.event_codes.${eventCode}`
397
+ const translated = t(key, eventCode.toLowerCase())
398
+ // If translation returns the key itself, fall back to lowercase code
399
+ return translated === key ? eventCode.toLowerCase() : translated
400
+ }, [t])
401
+ }
402
+
403
+ // ─── Sub-components ──────────────────────────────────────────
404
+
405
+ type LucideIcon = typeof Ship
406
+
407
+ interface CollapsibleSectionProps {
408
+ title: string
409
+ icon: LucideIcon
410
+ count?: number
411
+ defaultOpen?: boolean
412
+ children: React.ReactNode
413
+ }
414
+
415
+ function CollapsibleSection({
416
+ title,
417
+ icon: Icon,
418
+ count,
419
+ defaultOpen = false,
420
+ children,
421
+ }: CollapsibleSectionProps) {
422
+ const [isOpen, setIsOpen] = useState(defaultOpen)
423
+
424
+ return (
425
+ <div className="border border-border rounded-lg bg-card overflow-hidden">
426
+ <button
427
+ type="button"
428
+ className="w-full p-3 flex items-center justify-between text-left hover:bg-muted transition-colors"
429
+ onClick={() => setIsOpen(!isOpen)}
430
+ >
431
+ <div className="flex items-center space-x-2">
432
+ <Icon className="w-4 h-4" />
433
+ <span className="font-medium text-foreground">{title}</span>
434
+ {count !== undefined && (
435
+ <span className="bg-blue-100 dark:bg-blue-900/30 text-blue-600 dark:text-blue-400 px-2 py-0.5 rounded-full text-xs font-medium">
436
+ {count}
437
+ </span>
438
+ )}
439
+ </div>
440
+ {isOpen ? (
441
+ <ChevronUp className="w-4 h-4 text-muted-foreground" />
442
+ ) : (
443
+ <ChevronDown className="w-4 h-4 text-muted-foreground" />
444
+ )}
445
+ </button>
446
+ {isOpen && (
447
+ <div className="border-t border-border">
448
+ {children}
449
+ </div>
450
+ )}
451
+ </div>
452
+ )
453
+ }
454
+
455
+ interface ShipmentCardProps {
456
+ shipment: ShipmentDetailsData
457
+ }
458
+
459
+ function ShipmentCard({ shipment }: ShipmentCardProps) {
460
+ const t = useT()
461
+ const carrierLogo = getCarrierLogo(shipment.carrierCode)
462
+ // For deep-linking, prefer BOL > booking > container as reference
463
+ const trackingReference = shipment.bolNumber || shipment.bookingNumber || shipment.containerNumber
464
+ const trackingUrl = getCarrierTrackingUrl(shipment.carrierCode, trackingReference)
465
+ const atdActual = getPrimaryTimestamp(shipment.atdTimestamps)
466
+ const ataActual = getPrimaryTimestamp(shipment.ataTimestamps)
467
+ const etd = getPrimaryTimestamp(shipment.etdTimestamps)
468
+ const eta = getPrimaryTimestamp(shipment.etaTimestamps)
469
+ const atd = atdActual || etd
470
+ const ata = ataActual || eta
471
+
472
+ // Calculate transit time
473
+ const transitDays = calculateTransitDays(atdActual || etd, ataActual, eta)
474
+
475
+ // Extract location names from JSONB fields
476
+ const originName = shipment.originLocation?.name || shipment.originLocation?.unlocode || '-'
477
+ const destinationName = shipment.destinationLocation?.name || shipment.destinationLocation?.unlocode || '-'
478
+ const originFacilityCode = formatFacilityCode(
479
+ shipment.originLocation?.facilityCode,
480
+ shipment.originLocation?.facilityCodeListProvider
481
+ )
482
+ const destinationFacilityCode = formatFacilityCode(
483
+ shipment.destinationLocation?.facilityCode,
484
+ shipment.destinationLocation?.facilityCodeListProvider
485
+ )
486
+
487
+ return (
488
+ <div className="bg-card border border-border rounded-lg p-4 relative">
489
+ <div className="space-y-3">
490
+ {/* Header with reference and carrier logo */}
491
+ <div className="flex items-start justify-between">
492
+ <div>
493
+ <h2 className="text-lg font-semibold text-foreground">
494
+ {shipment.bolNumber || shipment.bookingNumber || shipment.containerNumber || shipment.id.slice(0, 8)}
495
+ </h2>
496
+ {shipment.containerNumber && (
497
+ <p className="text-sm text-muted-foreground font-mono mt-1">{shipment.containerNumber}</p>
498
+ )}
499
+ </div>
500
+ </div>
501
+
502
+ {/* Carrier logo */}
503
+ {carrierLogo && (
504
+ <div className="w-12 h-12 flex items-center justify-center absolute top-4 right-4">
505
+ <Image
506
+ src={carrierLogo}
507
+ alt={shipment.carrierCode || 'Carrier'}
508
+ width={40}
509
+ height={40}
510
+ className="object-contain"
511
+ />
512
+ </div>
513
+ )}
514
+
515
+ {/* Route: Origin -> Destination */}
516
+ <div className="flex items-center gap-2 text-sm">
517
+ <div className="font-medium text-foreground">
518
+ <b>{originName}</b>
519
+ {originFacilityCode && (
520
+ <span className="text-xs text-blue-600 dark:text-blue-400 ml-1 font-mono">[{originFacilityCode}]</span>
521
+ )}
522
+ {atd && <span className="text-muted-foreground"> ({formatDateOnly(atd)})</span>}
523
+ </div>
524
+ <ArrowRight className="w-4 h-4 text-muted-foreground" />
525
+ <div className="font-medium text-foreground">
526
+ <b>{destinationName}</b>
527
+ {destinationFacilityCode && (
528
+ <span className="text-xs text-blue-600 dark:text-blue-400 ml-1 font-mono">[{destinationFacilityCode}]</span>
529
+ )}
530
+ {ata && <span className="text-muted-foreground"> ({formatDateOnly(ata)})</span>}
531
+ </div>
532
+ </div>
533
+
534
+ {/* Vessel info */}
535
+ {shipment.vesselName && (
536
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
537
+ <Ship className="w-4 h-4" />
538
+ <span className="font-medium">{shipment.vesselName}</span>
539
+ {shipment.vesselImo && (
540
+ <>
541
+ <span className="text-muted-foreground/70">•</span>
542
+ <span className="font-mono text-xs">IMO: {shipment.vesselImo}</span>
543
+ </>
544
+ )}
545
+ </div>
546
+ )}
547
+
548
+ {/* Transit time */}
549
+ {transitDays !== null && (
550
+ <div className="flex items-center gap-2 text-sm text-muted-foreground">
551
+ <Clock className="w-4 h-4" />
552
+ <span>
553
+ {t('shipment_tracking.details.transitTime', 'Transit time')}: {transitDays} {t('shipment_tracking.details.days', 'days')}
554
+ </span>
555
+ </div>
556
+ )}
557
+
558
+ {/* Verify on carrier website */}
559
+ {trackingUrl && (
560
+ <a
561
+ href={trackingUrl}
562
+ target="_blank"
563
+ rel="noopener noreferrer"
564
+ className="inline-flex items-center gap-2 text-sm text-blue-600 hover:text-blue-700 font-medium"
565
+ >
566
+ <span>{t('shipment_tracking.details.verifyOnCarrier', 'Verify on carrier website')}</span>
567
+ <ExternalLink className="w-4 h-4" />
568
+ </a>
569
+ )}
570
+ </div>
571
+ </div>
572
+ )
573
+ }
574
+
575
+ interface DestinationStatusCardProps {
576
+ shipment: ShipmentDetailsData
577
+ }
578
+
579
+ function DestinationStatusCard({ shipment }: DestinationStatusCardProps) {
580
+ const t = useT()
581
+ const ata = getPrimaryTimestamp(shipment.ataTimestamps)
582
+ const eta = getPrimaryTimestamp(shipment.etaTimestamps)
583
+ const hasArrived = ata !== null
584
+
585
+ // Determine status
586
+ let statusLabel = t('shipment_tracking.details.status.enRoute', 'En Route')
587
+ let statusColor = 'bg-yellow-600'
588
+ if (shipment.status === 'ARRIVED' || hasArrived) {
589
+ statusLabel = t('shipment_tracking.details.status.inPort', 'In Port')
590
+ statusColor = 'bg-blue-600'
591
+ } else if (shipment.status === 'DELIVERED') {
592
+ statusLabel = t('shipment_tracking.details.status.delivered', 'Delivered')
593
+ statusColor = 'bg-green-600'
594
+ }
595
+
596
+ // Extract location name from JSONB field
597
+ const destLoc = shipment.destinationLocation
598
+ const destinationName = destLoc?.name || destLoc?.unlocode || '-'
599
+ const facilityCode = formatFacilityCode(destLoc?.facilityCode, destLoc?.facilityCodeListProvider)
600
+
601
+ return (
602
+ <div className="bg-gradient-to-r from-blue-50 to-indigo-50 dark:from-blue-950/30 dark:to-indigo-950/30 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
603
+ <div className="flex items-center justify-between mb-3">
604
+ <h3 className="font-semibold text-foreground flex items-center gap-2">
605
+ <MapPin className="w-5 h-5 text-blue-600 dark:text-blue-400" />
606
+ {t('shipment_tracking.details.destinationStatus', 'Destination Status')}
607
+ </h3>
608
+ <span className={`${statusColor} text-white px-3 py-1 rounded-full text-xs font-medium`}>
609
+ {statusLabel}
610
+ </span>
611
+ </div>
612
+ <div className="space-y-2">
613
+ <div className="flex items-center gap-2 text-sm">
614
+ <Anchor className="w-4 h-4 text-muted-foreground" />
615
+ <span className="font-medium text-foreground">{destinationName}</span>
616
+ {facilityCode && (
617
+ <span className="text-xs text-blue-600 dark:text-blue-400 font-mono">[{facilityCode}]</span>
618
+ )}
619
+ </div>
620
+ {/* Terminal address */}
621
+ {destLoc?.address && (
622
+ <div className="text-xs text-muted-foreground ml-6 truncate" title={destLoc.address}>
623
+ {destLoc.address}
624
+ </div>
625
+ )}
626
+ <div className="flex items-center gap-2 text-xs text-muted-foreground">
627
+ <Clock className="w-3 h-3" />
628
+ <span>
629
+ {hasArrived
630
+ ? `ATA: ${formatShortDate(ata)}`
631
+ : eta
632
+ ? `ETA: ${formatShortDate(eta)}`
633
+ : '-'}
634
+ </span>
635
+ </div>
636
+ </div>
637
+ </div>
638
+ )
639
+ }
640
+
641
+ interface RouteDetailsProps {
642
+ stops: RouteStop[]
643
+ }
644
+
645
+ /**
646
+ * Get colors based on stop type.
647
+ * Origin = green, Destination = blue, Transshipment = yellow
648
+ */
649
+ function getStopColors(type: 'origin' | 'transshipment' | 'port_call' | 'destination') {
650
+ switch (type) {
651
+ case 'origin':
652
+ return {
653
+ bg: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-300 dark:border-green-700',
654
+ badge: 'bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400',
655
+ }
656
+ case 'destination':
657
+ return {
658
+ bg: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400 border-blue-300 dark:border-blue-700',
659
+ badge: 'bg-blue-100 dark:bg-blue-900/30 text-blue-700 dark:text-blue-400',
660
+ }
661
+ case 'port_call':
662
+ return {
663
+ bg: 'bg-slate-100 dark:bg-slate-800/40 text-slate-700 dark:text-slate-300 border-slate-300 dark:border-slate-600',
664
+ badge: 'bg-slate-100 dark:bg-slate-800/40 text-slate-700 dark:text-slate-300',
665
+ }
666
+ case 'transshipment':
667
+ default:
668
+ return {
669
+ bg: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400 border-yellow-300 dark:border-yellow-700',
670
+ badge: 'bg-yellow-100 dark:bg-yellow-900/30 text-yellow-700 dark:text-yellow-400',
671
+ }
672
+ }
673
+ }
674
+
675
+ /**
676
+ * Formats facility code with provider badge (e.g., "DCT (SMDG)")
677
+ */
678
+ function formatFacilityCode(code: string | null | undefined, provider: string | null | undefined): string | null {
679
+ if (!code) return null
680
+ if (provider) return `${code} (${provider})`
681
+ return code
682
+ }
683
+
684
+ /**
685
+ * Get the appropriate icon for a route stop based on facility type, vessel, and stop type.
686
+ *
687
+ * Priority order:
688
+ * 1. Explicit facility types (DEPO, CLOC, POTE, INTE) - respect the data
689
+ * 2. Unknown facility type + origin/destination - assume port for sea container tracking
690
+ * 3. Unknown facility type + transshipment - use MapPin
691
+ *
692
+ * This ensures:
693
+ * - Depots/client locations show Warehouse even if they're origin/destination
694
+ * - Ports with missing facilityTypeCode (like NHAVA SHEVA) show Anchor
695
+ * - Intermodal terminals use vessel presence as the deciding factor
696
+ */
697
+ function getRouteStopIcon(
698
+ facilityTypeCode: string | null | undefined,
699
+ vesselName?: string | null,
700
+ stopType?: 'origin' | 'transshipment' | 'port_call' | 'destination'
701
+ ) {
702
+ // Explicit depot/client locations are always inland (warehouse icon)
703
+ // Respect the data when we know it's a depot or client location
704
+ if (facilityTypeCode === 'DEPO' || facilityTypeCode === 'CLOC') {
705
+ return Warehouse
706
+ }
707
+
708
+ // Port terminals are always ports
709
+ if (facilityTypeCode === 'POTE') {
710
+ return Anchor
711
+ }
712
+
713
+ // Intermodal terminals: use vessel presence as signal
714
+ if (facilityTypeCode === 'INTE') {
715
+ return vesselName ? Anchor : Warehouse
716
+ }
717
+
718
+ // Unknown facility type (null/undefined):
719
+ // For sea container origin/destination, assume port when we have no other info
720
+ // (this handles cases like NHAVA SHEVA where facilityTypeCode is missing)
721
+ if (stopType === 'origin' || stopType === 'destination') {
722
+ return Anchor
723
+ }
724
+
725
+ return MapPin // truly unknown transshipment or no stop type
726
+ }
727
+
728
+ function RouteDetails({ stops }: RouteDetailsProps) {
729
+ const t = useT()
730
+
731
+ if (stops.length === 0) {
732
+ return null
733
+ }
734
+
735
+ // Segment status helpers:
736
+ // - Completed (green): departed from current AND arrived at next
737
+ // - In progress (blue): departed from current but NOT arrived at next
738
+ // - Pending (gray): not departed yet
739
+ const getSegmentStatus = (currentStop: RouteStop, nextStop: RouteStop | undefined): 'completed' | 'in-progress' | 'pending' => {
740
+ if (!currentStop.atd) return 'pending'
741
+ if (nextStop?.ata) return 'completed'
742
+ return 'in-progress'
743
+ }
744
+
745
+ const getSegmentLineColor = (status: 'completed' | 'in-progress' | 'pending') => {
746
+ switch (status) {
747
+ case 'completed':
748
+ return 'bg-green-500 dark:bg-green-400'
749
+ case 'in-progress':
750
+ return 'bg-blue-500 dark:bg-blue-400'
751
+ default:
752
+ return 'bg-border'
753
+ }
754
+ }
755
+
756
+ return (
757
+ <div className="bg-card border border-border rounded-lg p-4">
758
+ <h3 className="font-semibold text-foreground mb-4 flex items-center gap-2">
759
+ <MapPin className="w-4 h-4" />
760
+ {t('shipment_tracking.details.routeDetails', 'Route Details')}
761
+ </h3>
762
+ <div className="relative">
763
+ <div className="space-y-0">
764
+ {stops.map((stop, index) => {
765
+ const colors = getStopColors(stop.type)
766
+ const badgeText = stop.type === 'destination'
767
+ ? t('shipment_tracking.details.destination', 'Destination')
768
+ : stop.type === 'transshipment'
769
+ ? t('shipment_tracking.details.transshipment', 'Transshipment')
770
+ : stop.type === 'port_call'
771
+ ? t('shipment_tracking.details.portCall', 'Port call')
772
+ : t('shipment_tracking.details.origin', 'Origin')
773
+
774
+ // Calculate delays by comparing planned vs actual
775
+ const departureDelay = calculateDelayFromPlanned(stop.etd, stop.atd)
776
+ const arrivalDelay = calculateDelayFromPlanned(stop.eta, stop.ata)
777
+
778
+ // Format facility code with provider
779
+ const facilityDisplay = formatFacilityCode(stop.facilityCode, stop.facilityCodeListProvider)
780
+
781
+ // Determine segment status (line to next stop)
782
+ const isLastStop = index === stops.length - 1
783
+ const nextStop = !isLastStop ? stops[index + 1] : undefined
784
+ const segmentStatus = getSegmentStatus(stop, nextStop)
785
+
786
+ // Get contextual icon based on facility type and stop type
787
+ const StopIcon = getRouteStopIcon(stop.facilityTypeCode, stop.vesselName, stop.type)
788
+
789
+ return (
790
+ <div key={`${stop.unlocode || stop.location}-${index}`} className="relative flex items-start gap-3 py-2">
791
+ {/* Connecting line to next stop - starts below current icon, extends to next icon center */}
792
+ {/* Icon: mt-1(4px) + p-2(8px) + icon(16px) + p-2(8px) = 36px total, center at 20px from item top */}
793
+ {/* Line starts at 36px (below icon), extends to bottom + 20px into next item */}
794
+ {!isLastStop && (
795
+ <div
796
+ className={`absolute left-[15px] top-[36px] bottom-[-20px] w-0.5 ${getSegmentLineColor(segmentStatus)}`}
797
+ />
798
+ )}
799
+ <div className={`relative z-10 mt-1 p-2 rounded-full ${colors.bg}`}>
800
+ <StopIcon className="w-4 h-4" />
801
+ </div>
802
+ <div className="flex-1 min-w-0 flex items-start justify-between gap-4">
803
+ <div className="flex-shrink-0">
804
+ <div className="flex items-center gap-2 mb-1">
805
+ <div className="font-semibold text-foreground">{stop.location}</div>
806
+ <div className={`px-2 py-1 rounded text-xs font-medium whitespace-nowrap ${colors.badge}`}>
807
+ {badgeText}
808
+ </div>
809
+ </div>
810
+ {/* UN/LOCODE and facility code */}
811
+ <div className="flex items-center gap-2 text-xs text-muted-foreground mb-1">
812
+ {stop.unlocode && <span className="font-mono">{stop.unlocode}</span>}
813
+ {facilityDisplay && (
814
+ <>
815
+ {stop.unlocode && <span>•</span>}
816
+ <span className="font-mono text-blue-600 dark:text-blue-400" title={t('shipment_tracking.details.terminalCode', 'Terminal Code')}>
817
+ {facilityDisplay}
818
+ </span>
819
+ </>
820
+ )}
821
+ </div>
822
+ {/* Facility address */}
823
+ {stop.facilityAddress && (
824
+ <div className="text-xs text-muted-foreground mb-1 max-w-[200px] truncate" title={stop.facilityAddress}>
825
+ {stop.facilityAddress}
826
+ </div>
827
+ )}
828
+ {/* Coordinates */}
829
+ {stop.coords && (
830
+ <div className="text-xs text-muted-foreground/70 mb-1 font-mono">
831
+ {stop.coords.latitude.toFixed(4)}, {stop.coords.longitude.toFixed(4)}
832
+ </div>
833
+ )}
834
+ {stop.vesselName && (
835
+ <div className="flex items-center gap-1 text-xs text-muted-foreground">
836
+ <Ship className="w-3 h-3" />
837
+ <span>{stop.vesselName}</span>
838
+ </div>
839
+ )}
840
+ </div>
841
+ <div className="text-right text-sm space-y-2 flex-shrink-0">
842
+ {/* Departure timestamps */}
843
+ {(stop.atd || stop.etd) && (
844
+ <div>
845
+ <div className="text-muted-foreground text-xs">{stop.atd ? 'ATD:' : 'ETD:'}</div>
846
+ <div className="font-medium text-foreground">{formatShortDate(stop.atd || stop.etd)}</div>
847
+ {departureDelay !== null && (
848
+ <div className={`text-xs ${departureDelay > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
849
+ {departureDelay > 0 ? '+' : ''}{departureDelay} {t('shipment_tracking.details.days', 'days')}
850
+ </div>
851
+ )}
852
+ </div>
853
+ )}
854
+ {/* Arrival timestamps */}
855
+ {(stop.ata || stop.eta) && (
856
+ <div>
857
+ <div className="text-muted-foreground text-xs">{stop.ata ? 'ATA:' : 'ETA:'}</div>
858
+ <div className="font-medium text-foreground">{formatShortDate(stop.ata || stop.eta)}</div>
859
+ {arrivalDelay !== null && (
860
+ <div className={`text-xs ${arrivalDelay > 0 ? 'text-red-600 dark:text-red-400' : 'text-green-600 dark:text-green-400'}`}>
861
+ {arrivalDelay > 0 ? '+' : ''}{arrivalDelay} {t('shipment_tracking.details.days', 'days')}
862
+ </div>
863
+ )}
864
+ </div>
865
+ )}
866
+ </div>
867
+ </div>
868
+ </div>
869
+ )
870
+ })}
871
+ </div>
872
+ </div>
873
+ </div>
874
+ )
875
+ }
876
+
877
+ interface JourneyTimelineProps {
878
+ events: TrackingEventData[]
879
+ }
880
+
881
+ function JourneyTimeline({ events }: JourneyTimelineProps) {
882
+ const t = useT()
883
+ const locale = useLocale()
884
+ const getEventLabel = useEventLabel()
885
+
886
+ // Sort events by datetime ascending (chronological order - oldest first at top)
887
+ // Use id as secondary sort key for stability when timestamps are equal
888
+ // Hide timeline noise: AIS waypoints (WAYR) and "approaching" proximity events
889
+ // (PPRA/TPRA). Actual arrivals/departures (PARR/TARR/PPRD/TPRD) and DCSA events stay.
890
+ const sortedEvents = useMemo(
891
+ () => events
892
+ .filter((e) => !['WAYR', 'PPRA', 'TPRA'].includes(e.eventCode))
893
+ .sort((a, b) => {
894
+ const timeA = new Date(a.eventDateTime).getTime()
895
+ const timeB = new Date(b.eventDateTime).getTime()
896
+ if (timeA !== timeB) return timeA - timeB
897
+ return a.id.localeCompare(b.id)
898
+ }),
899
+ [events]
900
+ )
901
+
902
+ // An event has "already happened" if the journey has progressed past it — i.e. there is an
903
+ // ACT (actual) event at or after its position. We anchor on the LAST actual event in
904
+ // chronological order: everything up to and including it is done (green), even planned
905
+ // (PLN/EST) milestones that were superseded by a later actual event (e.g. a planned
906
+ // departure followed by an actual arrival). Everything after it is still upcoming. See CHAME-185.
907
+ const lastActualIndex = useMemo(() => {
908
+ let last = -1
909
+ sortedEvents.forEach((e, i) => {
910
+ if (e.eventClassifierCode === 'ACT') last = i
911
+ })
912
+ return last
913
+ }, [sortedEvents])
914
+
915
+ // Segment (connector) status between event[index] and event[index + 1]:
916
+ // - Completed (green): both ends have already happened
917
+ // - In progress (blue): we've reached this event but not the next (the current leg)
918
+ // - Pending (gray): this event hasn't happened yet
919
+ const getSegmentStatus = (index: number): 'completed' | 'in-progress' | 'pending' => {
920
+ if (index + 1 <= lastActualIndex) return 'completed'
921
+ if (index <= lastActualIndex) return 'in-progress'
922
+ return 'pending'
923
+ }
924
+
925
+ const getSegmentLineColor = (status: 'completed' | 'in-progress' | 'pending') => {
926
+ switch (status) {
927
+ case 'completed':
928
+ return 'bg-green-500 dark:bg-green-400'
929
+ case 'in-progress':
930
+ return 'bg-blue-500 dark:bg-blue-400'
931
+ default:
932
+ return 'bg-border'
933
+ }
934
+ }
935
+
936
+ return (
937
+ <CollapsibleSection
938
+ title={t('shipment_tracking.details.journeyTimeline', 'Journey Timeline')}
939
+ icon={Calendar}
940
+ count={sortedEvents.length}
941
+ defaultOpen={false}
942
+ >
943
+ <div className="p-3">
944
+ <div className="relative">
945
+ <div className="space-y-4">
946
+ {sortedEvents.map((event, index) => {
947
+ const Icon = getEventIcon(event.eventCode, event.facilityTypeCode, event.vesselName)
948
+ const hasHappened = index <= lastActualIndex
949
+ const borderColor = hasHappened ? 'border-green-300 dark:border-green-700' : 'border-blue-300 dark:border-blue-700'
950
+ const bgColor = hasHappened
951
+ ? 'bg-green-100 dark:bg-green-900/30 border-green-300 dark:border-green-700 text-green-800 dark:text-green-300'
952
+ : 'bg-blue-100 dark:bg-blue-900/30 border-blue-300 dark:border-blue-700 text-blue-800 dark:text-blue-300'
953
+ const badgeColor = hasHappened
954
+ ? 'bg-green-200 dark:bg-green-800/50 text-green-800 dark:text-green-300'
955
+ : 'bg-blue-200 dark:bg-blue-800/50 text-blue-800 dark:text-blue-300'
956
+
957
+ const isLastEvent = index === sortedEvents.length - 1
958
+ const segmentStatus = getSegmentStatus(index)
959
+
960
+ return (
961
+ <div key={event.id} className="relative flex items-start gap-4">
962
+ {/* Connecting line to next event - connects icon centers symmetrically */}
963
+ {/* Icon: border-2(2px) + p-2(8px) + icon(20px) + p-2(8px) + border-2(2px) = 40px, center at 20px */}
964
+ {/* space-y-4 = 16px gap between items. Line: from 20px below center to 20px above next center */}
965
+ {/* Start: 20px (center) + 20px (half icon) = 40px. End: 16px gap + 20px into next = -36px */}
966
+ {!isLastEvent && (
967
+ <div
968
+ className={`absolute left-[19px] top-[40px] bottom-[-36px] w-0.5 ${getSegmentLineColor(segmentStatus)}`}
969
+ />
970
+ )}
971
+ <div className={`relative z-10 p-2 rounded-full border-2 bg-card ${borderColor}`}>
972
+ <Icon className={`w-5 h-5 ${hasHappened ? 'text-green-600 dark:text-green-400' : 'text-blue-600 dark:text-blue-400'}`} />
973
+ </div>
974
+ <div className={`flex-1 border rounded-lg p-3 ${bgColor}`}>
975
+ <div className="flex items-start justify-between">
976
+ <div className="flex-1">
977
+ <div className="flex items-center gap-2 mb-1">
978
+ <span className="font-semibold">{getEventLabel(event.eventCode)}</span>
979
+ <span className={`px-2 py-0.5 rounded text-xs font-medium ${badgeColor}`}>
980
+ {event.eventClassifierCode || 'N/A'}
981
+ </span>
982
+ </div>
983
+ <div className="text-sm font-medium mb-2">
984
+ {(() => {
985
+ // Precise place name wins: carrier events and AIS port/terminal
986
+ // events both populate locationName (e.g. "Gdańsk", "Qingdao QQCTN").
987
+ if (event.locationName) return event.locationName
988
+ // Open-water AIS waypoints have no place name — show the localized
989
+ // region plus the raw POI grid code muted beside it
990
+ // (e.g. "Morze Północne" · "N053W001-03738").
991
+ const regionName = pickRegionName(event, locale)
992
+ if (regionName) {
993
+ return (
994
+ <>
995
+ {regionName}
996
+ {event.locationUnlocode && (
997
+ <span className="ml-1.5 font-normal opacity-60">{event.locationUnlocode}</span>
998
+ )}
999
+ </>
1000
+ )
1001
+ }
1002
+ return event.locationUnlocode || '-'
1003
+ })()}
1004
+ </div>
1005
+ <div className="flex items-center gap-4 text-xs">
1006
+ <div className="flex items-center gap-1">
1007
+ <Calendar className="w-3 h-3" />
1008
+ {formatShortDate(event.eventDateTime)}
1009
+ </div>
1010
+ {event.vesselName && (
1011
+ <div className="flex items-center gap-1">
1012
+ <Ship className="w-3 h-3" />
1013
+ {event.vesselName}
1014
+ </div>
1015
+ )}
1016
+ </div>
1017
+ </div>
1018
+ </div>
1019
+ </div>
1020
+ </div>
1021
+ )
1022
+ })}
1023
+ </div>
1024
+ </div>
1025
+ </div>
1026
+ </CollapsibleSection>
1027
+ )
1028
+ }
1029
+
1030
+ interface BookingDetailsProps {
1031
+ shipment: ShipmentDetailsData
1032
+ }
1033
+
1034
+ function BookingDetails({ shipment }: BookingDetailsProps) {
1035
+ const t = useT()
1036
+
1037
+ return (
1038
+ <CollapsibleSection
1039
+ title={t('shipment_tracking.details.bookingDetails', 'Booking Details')}
1040
+ icon={Package}
1041
+ defaultOpen={false}
1042
+ >
1043
+ <div className="p-3">
1044
+ <div className="bg-muted rounded-lg p-4 space-y-2">
1045
+ <div className="flex justify-between items-center">
1046
+ <span className="text-sm text-muted-foreground">
1047
+ {t('shipment_tracking.shipments.fields.containerNumber', 'Container Number')}
1048
+ </span>
1049
+ <span className="font-mono text-sm font-semibold text-foreground">
1050
+ {shipment.containerNumber || 'N/A'}
1051
+ </span>
1052
+ </div>
1053
+ <div className="flex justify-between items-center">
1054
+ <span className="text-sm text-muted-foreground">
1055
+ {t('shipment_tracking.shipments.fields.size', 'Size')}
1056
+ </span>
1057
+ <span className="font-mono text-sm font-semibold text-foreground">
1058
+ {shipment.isoEquipmentCode || 'N/A'}
1059
+ </span>
1060
+ </div>
1061
+ <div className="flex justify-between items-center">
1062
+ <span className="text-sm text-muted-foreground">
1063
+ {t('shipment_tracking.shipments.fields.bookingNumber', 'Booking Number')}
1064
+ </span>
1065
+ <span className="font-mono text-sm font-semibold text-foreground">
1066
+ {shipment.bookingNumber || 'N/A'}
1067
+ </span>
1068
+ </div>
1069
+ <div className="flex justify-between items-center">
1070
+ <span className="text-sm text-muted-foreground">
1071
+ {t('shipment_tracking.shipments.fields.bolNumber', 'BOL Number')}
1072
+ </span>
1073
+ <span className="font-mono text-sm font-semibold text-foreground">
1074
+ {shipment.bolNumber || 'N/A'}
1075
+ </span>
1076
+ </div>
1077
+ <div className="flex justify-between items-center">
1078
+ <span className="text-sm text-muted-foreground">
1079
+ {t('shipment_tracking.shipments.fields.carrierCode', 'Carrier Code')}
1080
+ </span>
1081
+ <span className="font-mono text-sm font-semibold text-foreground">
1082
+ {shipment.carrierCode || 'N/A'}
1083
+ </span>
1084
+ </div>
1085
+ <div className="flex justify-between items-center">
1086
+ <span className="text-sm text-muted-foreground">
1087
+ {t('shipment_tracking.shipments.fields.seals', 'Seals')}
1088
+ </span>
1089
+ <span className="font-mono text-sm font-semibold text-foreground">
1090
+ {shipment.seals?.length
1091
+ ? shipment.seals.map(s => s.number).join(', ')
1092
+ : 'N/A'}
1093
+ </span>
1094
+ </div>
1095
+ <div className="flex justify-between items-center">
1096
+ <span className="text-sm text-muted-foreground">
1097
+ {t('shipment_tracking.shipments.fields.status', 'Status')}
1098
+ </span>
1099
+ <Badge variant={getStatusBadgeVariant(shipment.status)}>
1100
+ {shipment.status}
1101
+ </Badge>
1102
+ </div>
1103
+ </div>
1104
+ </div>
1105
+ </CollapsibleSection>
1106
+ )
1107
+ }
1108
+
1109
+ // ─── Main Component ──────────────────────────────────────────
1110
+
1111
+ export interface ShipmentDetailsDrawerProps {
1112
+ open: boolean
1113
+ onOpenChange: (open: boolean) => void
1114
+ shipmentId: string | null
1115
+ }
1116
+
1117
+ export function ShipmentDetailsDrawer({
1118
+ open,
1119
+ onOpenChange,
1120
+ shipmentId,
1121
+ }: ShipmentDetailsDrawerProps) {
1122
+ const t = useT()
1123
+
1124
+ // Fetch shipment data
1125
+ const { data: shipment, isLoading: isLoadingShipment } = useQuery({
1126
+ queryKey: ['shipment_tracking_shipment_details', shipmentId],
1127
+ queryFn: async () => {
1128
+ if (!shipmentId) return null
1129
+ const response = await apiCall<{ items: Record<string, unknown>[] }>(
1130
+ `/api/shipment_tracking/shipments?id=${shipmentId}`
1131
+ )
1132
+ if (!response.ok) throw new Error('Failed to load shipment')
1133
+ const items = response.result?.items ?? []
1134
+ const item = items.find((i) => i.id === shipmentId) ?? items[0]
1135
+ if (!item) return null
1136
+
1137
+ // Normalize API response
1138
+ // Handle trackingJob which can be: { id: string }, string (just the ID), or null
1139
+ const trackingJobRaw = item.trackingJob ?? item.tracking_job ?? item.trackingJobId ?? item.tracking_job_id
1140
+ let trackingJobId: string | null = null
1141
+ if (trackingJobRaw) {
1142
+ if (typeof trackingJobRaw === 'string') {
1143
+ trackingJobId = trackingJobRaw
1144
+ } else if (typeof trackingJobRaw === 'object' && 'id' in trackingJobRaw) {
1145
+ trackingJobId = (trackingJobRaw as { id: string }).id
1146
+ }
1147
+ }
1148
+
1149
+ // Parse denormalized route stops
1150
+ const routeStopsRaw = item.routeStops ?? item.route_stops
1151
+ const routeStops = Array.isArray(routeStopsRaw) ? routeStopsRaw.map((stop: Record<string, unknown>) => ({
1152
+ location: stop.location as string,
1153
+ unlocode: (stop.unlocode) as string | undefined,
1154
+ type: (stop.type ?? 'transshipment') as 'origin' | 'transshipment' | 'port_call' | 'destination',
1155
+ vesselName: (stop.vesselName ?? stop.vessel_name) as string | undefined,
1156
+ vesselImo: (stop.vesselImo ?? stop.vessel_imo) as string | null,
1157
+ ata: (stop.ata) as string | null,
1158
+ atd: (stop.atd) as string | null,
1159
+ eta: (stop.eta) as string | null,
1160
+ etd: (stop.etd) as string | null,
1161
+ // Facility/terminal details
1162
+ facilityCode: (stop.facilityCode ?? stop.facility_code) as string | null,
1163
+ facilityCodeListProvider: (stop.facilityCodeListProvider ?? stop.facility_code_list_provider) as 'BIC' | 'SMDG' | null,
1164
+ facilityTypeCode: (stop.facilityTypeCode ?? stop.facility_type_code) as string | null,
1165
+ facilityAddress: (stop.facilityAddress ?? stop.facility_address) as string | null,
1166
+ coords: stop.coords ? stop.coords as Coords : null,
1167
+ })) : null
1168
+
1169
+ // Parse denormalized cargo events
1170
+ const cargoEventsRaw = item.cargoEvents ?? item.cargo_events
1171
+ const cargoEvents = Array.isArray(cargoEventsRaw) ? cargoEventsRaw.map((evt: Record<string, unknown>) => ({
1172
+ id: evt.id as string,
1173
+ eventType: (evt.eventType ?? evt.event_type ?? '') as string,
1174
+ eventCode: (evt.eventCode ?? evt.event_code ?? '') as string,
1175
+ eventClassifierCode: (evt.eventClassifierCode ?? evt.event_classifier_code) as 'ACT' | 'PLN' | 'EST' | null,
1176
+ eventDateTime: (evt.eventDateTime ?? evt.event_date_time) as string,
1177
+ description: (evt.description) as string | null,
1178
+ locationName: (evt.locationName ?? evt.location_name) as string | null,
1179
+ locationUnlocode: (evt.locationUnlocode ?? evt.location_unlocode) as string | null,
1180
+ regionNamePl: (evt.regionNamePl ?? evt.region_name_pl) as string | null,
1181
+ regionNameEn: (evt.regionNameEn ?? evt.region_name_en) as string | null,
1182
+ vesselName: (evt.vesselName ?? evt.vessel_name) as string | null,
1183
+ vesselImo: (evt.vesselImo ?? evt.vessel_imo) as string | null,
1184
+ voyageNumber: (evt.voyageNumber ?? evt.voyage_number) as string | null,
1185
+ isTransshipmentMove: (evt.isTransshipmentMove ?? evt.is_transshipment_move) as boolean | null,
1186
+ // Facility/terminal details
1187
+ facilityCode: (evt.facilityCode ?? evt.facility_code) as string | null,
1188
+ facilityCodeListProvider: (evt.facilityCodeListProvider ?? evt.facility_code_list_provider) as 'BIC' | 'SMDG' | null,
1189
+ facilityTypeCode: (evt.facilityTypeCode ?? evt.facility_type_code) as string | null,
1190
+ facilityAddress: (evt.facilityAddress ?? evt.facility_address) as string | null,
1191
+ latitude: (evt.latitude) as number | null,
1192
+ longitude: (evt.longitude) as number | null,
1193
+ })) : null
1194
+
1195
+ // Parse rich location data
1196
+ const originLocationRaw = item.originLocation ?? item.origin_location
1197
+ const destinationLocationRaw = item.destinationLocation ?? item.destination_location
1198
+
1199
+ return {
1200
+ id: item.id as string,
1201
+ status: (item.status ?? 'PENDING') as string,
1202
+ carrierCode: (item.carrierCode ?? item.carrier_code) as string | null,
1203
+ containerNumber: (item.containerNumber ?? item.container_number) as string | null,
1204
+ bookingNumber: (item.bookingNumber ?? item.booking_number) as string | null,
1205
+ isoEquipmentCode: (item.isoEquipmentCode ?? item.iso_equipment_code) as string | null,
1206
+ bolNumber: (item.bolNumber ?? item.bol_number) as string | null,
1207
+ etdTimestamps: (item.etdTimestamps ?? item.etd_timestamps) as TimestampEntry[] | null,
1208
+ etaTimestamps: (item.etaTimestamps ?? item.eta_timestamps) as TimestampEntry[] | null,
1209
+ atdTimestamps: (item.atdTimestamps ?? item.atd_timestamps) as TimestampEntry[] | null,
1210
+ ataTimestamps: (item.ataTimestamps ?? item.ata_timestamps) as TimestampEntry[] | null,
1211
+ originLocation: originLocationRaw as FacilityLocation | null,
1212
+ destinationLocation: destinationLocationRaw as FacilityLocation | null,
1213
+ vesselName: (item.vesselName ?? item.vessel_name) as string | null,
1214
+ vesselImo: (item.vesselImo ?? item.vessel_imo) as string | null,
1215
+ voyageNumber: (item.voyageNumber ?? item.voyage_number) as string | null,
1216
+ routeStops,
1217
+ cargoEvents,
1218
+ trackingJob: trackingJobId ? { id: trackingJobId } : null,
1219
+ } as ShipmentDetailsData
1220
+ },
1221
+ enabled: open && !!shipmentId,
1222
+ })
1223
+
1224
+ // Use denormalized data directly from shipment - no separate events query needed!
1225
+ const routeStops = shipment?.routeStops ?? []
1226
+ const events = shipment?.cargoEvents ?? []
1227
+
1228
+ // Derive current vessel from shipment progress
1229
+ const currentVessel = useMemo<CurrentVesselInfo>(() => {
1230
+ return getCurrentVessel(
1231
+ shipment?.routeStops as RouteStopEntry[] | null | undefined,
1232
+ shipment?.cargoEvents as CargoEventEntry[] | null | undefined,
1233
+ { vesselName: shipment?.vesselName, vesselImo: shipment?.vesselImo }
1234
+ )
1235
+ }, [shipment?.routeStops, shipment?.cargoEvents, shipment?.vesselName, shipment?.vesselImo])
1236
+
1237
+ const isLoading = isLoadingShipment
1238
+
1239
+ // Handle keyboard shortcuts
1240
+ const handleKeyDown = React.useCallback(
1241
+ (e: React.KeyboardEvent) => {
1242
+ if (e.key === 'Escape') {
1243
+ onOpenChange(false)
1244
+ }
1245
+ },
1246
+ [onOpenChange]
1247
+ )
1248
+
1249
+ return (
1250
+ <Sheet open={open} onOpenChange={onOpenChange}>
1251
+ <SheetContent
1252
+ className="flex flex-col p-0 w-full sm:max-w-xl md:max-w-2xl"
1253
+ overlayClassName="backdrop-blur-none"
1254
+ onKeyDown={handleKeyDown}
1255
+ >
1256
+ {/* Header */}
1257
+ <div className="flex justify-between px-4 py-6 border-b border-border">
1258
+ <div className="flex flex-col gap-2">
1259
+ <h1 className="text-xl font-semibold text-foreground">
1260
+ {t('shipment_tracking.details.title', 'Shipment Details')}
1261
+ </h1>
1262
+ </div>
1263
+ </div>
1264
+
1265
+ {/* Content */}
1266
+ <div className="flex-1 overflow-y-auto p-4">
1267
+ {isLoading ? (
1268
+ <div className="flex flex-col items-center justify-center h-48 gap-2">
1269
+ <Spinner className="h-6 w-6" />
1270
+ <span className="text-sm text-muted-foreground">
1271
+ {t('shipment_tracking.details.loading', 'Loading shipment details...')}
1272
+ </span>
1273
+ </div>
1274
+ ) : !shipment ? (
1275
+ <div className="flex flex-col items-center justify-center h-48 gap-2">
1276
+ <span className="text-sm text-muted-foreground">
1277
+ {t('shipment_tracking.details.notFound', 'Shipment not found')}
1278
+ </span>
1279
+ </div>
1280
+ ) : (
1281
+ <div className="space-y-4">
1282
+ {/* Shipment Card */}
1283
+ <ShipmentCard shipment={shipment} />
1284
+
1285
+ {/* Destination Status */}
1286
+ <DestinationStatusCard shipment={shipment} />
1287
+
1288
+ {/* Route Details */}
1289
+ {routeStops.length > 0 && <RouteDetails stops={routeStops} />}
1290
+
1291
+ {/* Vessel Tracking Map */}
1292
+ {currentVessel.status === 'delivered' ? (
1293
+ // Container delivered - show delivered message instead of map
1294
+ <div className="relative w-full h-32 border border-green-200 dark:border-green-800 rounded-lg overflow-hidden bg-green-50 dark:bg-green-950/30">
1295
+ <div className="absolute inset-0 flex items-center justify-center">
1296
+ <div className="text-center">
1297
+ <Package className="w-8 h-8 text-green-500 dark:text-green-400 mx-auto mb-2" />
1298
+ <p className="text-sm font-medium text-green-700 dark:text-green-300">
1299
+ {t('shipment_tracking.map.delivered', 'Container delivered')}
1300
+ </p>
1301
+ {currentVessel.currentPort && (
1302
+ <p className="text-xs text-green-600 dark:text-green-400 mt-1">
1303
+ {currentVessel.currentPort}
1304
+ </p>
1305
+ )}
1306
+ </div>
1307
+ </div>
1308
+ </div>
1309
+ ) : currentVessel.vesselImo ? (
1310
+ // Show map with current or planned vessel
1311
+ <VesselTrackingMap
1312
+ vesselImo={currentVessel.vesselImo}
1313
+ vesselName={currentVessel.vesselName}
1314
+ height="250px"
1315
+ autoRefresh={true}
1316
+ showInfoOverlay={true}
1317
+ containerStatus={currentVessel.status}
1318
+ currentPort={currentVessel.currentPort}
1319
+ isPlannedVessel={currentVessel.isPlannedVessel}
1320
+ currentLeg={currentVessel.currentLeg}
1321
+ traceFrom={currentVessel.traceFrom}
1322
+ />
1323
+ ) : (
1324
+ // No vessel IMO available
1325
+ <div className="relative w-full h-48 border border-border rounded-lg overflow-hidden bg-muted">
1326
+ <div className="absolute inset-0 flex items-center justify-center">
1327
+ <div className="text-center">
1328
+ <Ship className="w-8 h-8 text-muted-foreground mx-auto mb-2" />
1329
+ <p className="text-sm text-muted-foreground">
1330
+ {t('shipment_tracking.details.noVesselData', 'No vessel data available')}
1331
+ </p>
1332
+ </div>
1333
+ </div>
1334
+ {/* Vessel name overlay when no IMO */}
1335
+ {currentVessel.vesselName && (
1336
+ <div className="absolute top-3 left-3 bg-card/95 backdrop-blur-sm rounded-lg shadow-md px-3 py-2 border border-border">
1337
+ <div className="flex items-center gap-2">
1338
+ <Ship className="w-4 h-4 text-blue-500 dark:text-blue-400" />
1339
+ <div>
1340
+ <h2 className="font-semibold text-sm text-foreground">{currentVessel.vesselName}</h2>
1341
+ <p className="text-xs text-muted-foreground">
1342
+ {t('shipment_tracking.details.noImoNumber', 'IMO number not available')}
1343
+ </p>
1344
+ </div>
1345
+ </div>
1346
+ </div>
1347
+ )}
1348
+ </div>
1349
+ )}
1350
+
1351
+ {/* Journey Timeline */}
1352
+ {events.length > 0 && <JourneyTimeline events={events} />}
1353
+
1354
+ {/* Booking Details */}
1355
+ <BookingDetails shipment={shipment} />
1356
+ </div>
1357
+ )}
1358
+ </div>
1359
+ </SheetContent>
1360
+ </Sheet>
1361
+ )
1362
+ }
1363
+
1364
+ export default ShipmentDetailsDrawer