@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,243 @@
1
+ import { NextResponse } from 'next/server'
2
+ import type { NextRequest } from 'next/server'
3
+ import { makeCrudRoute } from '@open-mercato/shared/lib/crud/factory'
4
+ import type { EntityManager } from '@mikro-orm/postgresql'
5
+ import { z } from 'zod'
6
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
7
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
8
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
9
+ import { TerminalTrackingJob, TerminalEvent } from '../../data/entities'
10
+ import { trackingJobListSchema, trackingJobCreateSchema } from '../../data/validators'
11
+ import {
12
+ createTerminalTrackingCrudOpenApi,
13
+ createPagedListResponseSchema,
14
+ defaultOkResponseSchema,
15
+ } from '../openapi'
16
+ import { withScopedPayload } from '../utils'
17
+ import { resolveTranslations } from '@open-mercato/shared/lib/i18n/server'
18
+ import { parseDynamicTableFilters } from '@freighttech/ui/backend/dynamic-table/server'
19
+
20
+ const rawBodySchema = z.object({}).passthrough()
21
+
22
+ type ListQuery = z.infer<typeof trackingJobListSchema>
23
+
24
+ const routeMetadata = {
25
+ GET: { requireAuth: true, requireFeatures: ['terminal_tracking.tracking_jobs.view'] },
26
+ POST: { requireAuth: true, requireFeatures: ['terminal_tracking.tracking_jobs.manage'] },
27
+ DELETE: { requireAuth: true, requireFeatures: ['terminal_tracking.tracking_jobs.manage'] },
28
+ }
29
+
30
+ // Maps DynamicTable column keys → MikroORM property paths. Doubles as an
31
+ // allowlist: unknown fields are dropped by the parser. Only real DB columns are
32
+ // filterable — gateInAt/gateOutAt/holds are derived in afterList, not stored.
33
+ const FILTER_FIELD_MAP: Record<string, string> = {
34
+ terminalCode: 'terminalCode',
35
+ containerNumber: 'containerNumber',
36
+ status: 'status',
37
+ }
38
+
39
+ const buildFilters = (query: ListQuery): Record<string, unknown> => {
40
+ const filters: Record<string, unknown> = { deletedAt: null }
41
+ if (query.terminalCode) filters.terminalCode = query.terminalCode
42
+ if (query.containerNumber) filters.containerNumber = query.containerNumber
43
+ if (query.status) filters.status = query.status
44
+ // DynamicTable filter popover sends filters as a JSON-stringified FilterRow[].
45
+ if (query.filters) {
46
+ try {
47
+ const dyn = JSON.parse(query.filters)
48
+ const parsed = parseDynamicTableFilters(dyn, FILTER_FIELD_MAP)
49
+ if (parsed.length) filters.$and = [...((filters.$and as Record<string, unknown>[]) ?? []), ...parsed]
50
+ } catch {
51
+ // Invalid filter JSON must not 500 the list — ignore and return the base filters.
52
+ }
53
+ }
54
+ return filters
55
+ }
56
+
57
+ // Arrival milestones: a truck/rail gate-in (GTIN) or a vessel discharge (DISC).
58
+ // Either means "entered the terminal", so both feed the Gate-in column.
59
+ const ARRIVAL_CODES = new Set(['GTIN', 'DISC'])
60
+
61
+ /**
62
+ * Enrich each listed job row with its Gate-in / Gate-out times and current holds,
63
+ * derived from its TerminalEvent rows. One batched, tenant+org-scoped query for
64
+ * the whole page (jobIds are already scoped by the list). Gate-in = earliest
65
+ * arrival event; Gate-out = latest DEPA; holds = impediments on the most recent
66
+ * event (the current snapshot).
67
+ */
68
+ async function enrichWithMilestones(
69
+ payload: { items?: Array<Record<string, unknown>> },
70
+ ctx: { container: { resolve: (name: string) => unknown }; auth: { tenantId?: string | null } | null; organizationIds: string[] | null },
71
+ ): Promise<void> {
72
+ const items = Array.isArray(payload?.items) ? payload.items : []
73
+ const tenantId = ctx.auth?.tenantId
74
+ if (!items.length || !tenantId) return
75
+ const jobIds = items.map((i) => i?.id).filter((x): x is string => typeof x === 'string')
76
+ if (!jobIds.length) return
77
+
78
+ const em = ctx.container.resolve('em') as EntityManager
79
+ const where: Record<string, unknown> = { job: { $in: jobIds }, tenantId }
80
+ if (ctx.organizationIds && ctx.organizationIds.length) where.organizationId = { $in: ctx.organizationIds }
81
+ const events = await em.find(TerminalEvent, where, { orderBy: { eventDateTime: 'asc' } })
82
+
83
+ const byJob = new Map<string, TerminalEvent[]>()
84
+ for (const e of events) {
85
+ const jobId = String((e.job as unknown as { id?: string })?.id ?? '')
86
+ if (!jobId) continue
87
+ const arr = byJob.get(jobId)
88
+ if (arr) arr.push(e)
89
+ else byJob.set(jobId, [e])
90
+ }
91
+
92
+ payload.items = items.map((item) => {
93
+ const evs = byJob.get(String((item as { id?: unknown }).id)) ?? []
94
+ const arrival = evs.find((e) => ARRIVAL_CODES.has(e.eventCode)) // earliest (sorted asc)
95
+ const departure = [...evs].reverse().find((e) => e.eventCode === 'DEPA')
96
+ const latest = evs.length ? evs[evs.length - 1] : null
97
+ // List items are MikroORM entity instances whose JSON serialization only
98
+ // emits *mapped* columns — ad-hoc props assigned here are dropped on the way
99
+ // out. Round-trip to a plain object (same shape the response already produces)
100
+ // so the computed columns survive. On cache hits items are already plain, so
101
+ // this is just a clone.
102
+ const plain = JSON.parse(JSON.stringify(item)) as Record<string, unknown>
103
+ plain.gateInAt = arrival?.eventDateTime ? arrival.eventDateTime.toISOString() : null
104
+ plain.gateOutAt = departure?.eventDateTime ? departure.eventDateTime.toISOString() : null
105
+ plain.holds = latest?.impediments && latest.impediments.length ? latest.impediments : []
106
+ return plain
107
+ })
108
+ }
109
+
110
+ // Sort keys the grid may send → MikroORM entity properties. Derived columns
111
+ // (gateInAt/gateOutAt/holds) are computed post-query and are not sortable.
112
+ const SORT_FIELD_MAP: Record<string, string> = {
113
+ id: 'id',
114
+ terminalCode: 'terminalCode',
115
+ containerNumber: 'containerNumber',
116
+ status: 'status',
117
+ createdAt: 'createdAt',
118
+ lastPollAt: 'lastPollAt',
119
+ }
120
+
121
+ /**
122
+ * Paginated list of tracking jobs. Hand-rolled (rather than the CRUD factory's
123
+ * ORM-fallback list, which fetches every row and ignores page/pageSize) so the
124
+ * DynamicTable pager gets real DB `limit`/`offset` and a true total. Mirrors the
125
+ * sibling vessel-visits route; filters/derived columns reuse the shared helpers.
126
+ */
127
+ async function listTrackingJobs(request: NextRequest): Promise<Response> {
128
+ const auth = await getAuthFromRequest(request)
129
+ if (!auth || !auth.orgId || !auth.tenantId) {
130
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
131
+ }
132
+
133
+ const parsed = trackingJobListSchema.safeParse(
134
+ Object.fromEntries(new URL(request.url).searchParams),
135
+ )
136
+ if (!parsed.success) {
137
+ return NextResponse.json({ error: 'Invalid query', details: parsed.error.flatten() }, { status: 400 })
138
+ }
139
+ const page = parsed.data.page ?? 1
140
+ const pageSize = parsed.data.pageSize ?? parsed.data.limit ?? 50
141
+
142
+ const container = await createRequestContainer()
143
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
144
+ const em = container.resolve('em') as EntityManager
145
+ const orgIds = scope?.filterIds ?? [auth.orgId]
146
+
147
+ const where: Record<string, unknown> = {
148
+ ...buildFilters(parsed.data),
149
+ tenantId: auth.tenantId,
150
+ organizationId: { $in: orgIds },
151
+ }
152
+
153
+ const sortField = SORT_FIELD_MAP[parsed.data.sortField ?? ''] ?? 'createdAt'
154
+ const sortDir = parsed.data.sortDir ?? 'desc'
155
+
156
+ const [items, total] = await em.findAndCount(TerminalTrackingJob, where, {
157
+ orderBy: { [sortField]: sortDir },
158
+ limit: pageSize,
159
+ offset: (page - 1) * pageSize,
160
+ })
161
+
162
+ const payload: {
163
+ items: Array<Record<string, unknown>>
164
+ total: number
165
+ page: number
166
+ pageSize: number
167
+ totalPages: number
168
+ } = {
169
+ items: items as unknown as Array<Record<string, unknown>>,
170
+ total,
171
+ page,
172
+ pageSize,
173
+ totalPages: Math.ceil(total / pageSize),
174
+ }
175
+ await enrichWithMilestones(payload, { container, auth, organizationIds: orgIds })
176
+ return NextResponse.json(payload)
177
+ }
178
+
179
+ const crud = makeCrudRoute({
180
+ metadata: routeMetadata,
181
+ orm: {
182
+ entity: TerminalTrackingJob,
183
+ idField: 'id',
184
+ orgField: 'organizationId',
185
+ tenantField: 'tenantId',
186
+ softDeleteField: 'deletedAt',
187
+ },
188
+ actions: {
189
+ create: {
190
+ commandId: 'terminal_tracking.tracking_job.create',
191
+ schema: rawBodySchema,
192
+ mapInput: async ({ raw, ctx }) => {
193
+ const { translate } = await resolveTranslations()
194
+ const scoped = withScopedPayload(raw ?? {}, ctx, translate)
195
+ return trackingJobCreateSchema.parse(scoped)
196
+ },
197
+ response: ({ result }) => ({ id: result?.id ?? null, newEvents: result?.newEvents ?? 0 }),
198
+ status: 201,
199
+ },
200
+ delete: {
201
+ commandId: 'terminal_tracking.tracking_job.delete',
202
+ schema: rawBodySchema,
203
+ mapInput: async ({ raw, ctx }) => {
204
+ const { translate } = await resolveTranslations()
205
+ const body = (raw as any)?.body ?? raw ?? {}
206
+ const scoped = withScopedPayload(body, ctx, translate)
207
+ return { id: body.id, ...scoped }
208
+ },
209
+ response: ({ result }) => ({ id: result?.id ?? null }),
210
+ status: 200,
211
+ },
212
+ },
213
+ })
214
+
215
+ export const openApi = createTerminalTrackingCrudOpenApi({
216
+ resourceName: 'TerminalTrackingJob',
217
+ pluralName: 'TerminalTrackingJobs',
218
+ querySchema: trackingJobListSchema,
219
+ listResponseSchema: createPagedListResponseSchema(
220
+ z.object({
221
+ id: z.string().uuid(),
222
+ terminalCode: z.string(),
223
+ containerNumber: z.string(),
224
+ status: z.string(),
225
+ gateInAt: z.string().datetime().nullable(),
226
+ gateOutAt: z.string().datetime().nullable(),
227
+ holds: z.array(z.string()),
228
+ }),
229
+ ),
230
+ create: { schema: rawBodySchema, description: 'Creates a terminal tracking job for a container.' },
231
+ del: {
232
+ schema: z.object({ id: z.string().uuid() }),
233
+ responseSchema: defaultOkResponseSchema,
234
+ description: 'Deletes a terminal tracking job.',
235
+ },
236
+ })
237
+
238
+ export const metadata = routeMetadata
239
+ // GET is hand-rolled for real DB pagination (the factory's ORM-fallback list
240
+ // returns every row and ignores page/pageSize); POST/DELETE stay on the factory.
241
+ export const GET = listTrackingJobs
242
+ export const POST = crud.POST
243
+ export const DELETE = crud.DELETE
@@ -0,0 +1,10 @@
1
+ import { createScopedApiHelpers } from '@open-mercato/shared/lib/api/scoped'
2
+
3
+ const { withScopedPayload, parseScopedCommandInput } = createScopedApiHelpers({
4
+ messages: {
5
+ tenantRequired: { key: 'terminal_tracking.errors.tenant_required', fallback: 'Tenant context is required' },
6
+ organizationRequired: { key: 'terminal_tracking.errors.organization_required', fallback: 'Organization context is required' },
7
+ },
8
+ })
9
+
10
+ export { withScopedPayload, parseScopedCommandInput }
@@ -0,0 +1,95 @@
1
+ import { NextRequest, NextResponse } from 'next/server'
2
+ import type { EntityManager } from '@mikro-orm/postgresql'
3
+ import { createRequestContainer } from '@open-mercato/shared/lib/di/container'
4
+ import { getAuthFromRequest } from '@open-mercato/shared/lib/auth/server'
5
+ import { resolveOrganizationScopeForRequest } from '@open-mercato/core/modules/directory/utils/organizationScope'
6
+ import { TerminalEvent, TerminalVesselVisit } from '../../data/entities'
7
+ import { vesselVisitListSchema } from '../../data/validators'
8
+
9
+ export const metadata = {
10
+ GET: { requireAuth: true, requireFeatures: ['terminal_tracking.events.view'] },
11
+ }
12
+
13
+ function serialize(v: TerminalVesselVisit) {
14
+ return {
15
+ id: v.id,
16
+ terminalCode: v.terminalCode,
17
+ visitRef: v.visitRef,
18
+ vesselName: v.vesselName ?? null,
19
+ ibVoyage: v.ibVoyage ?? null,
20
+ obVoyage: v.obVoyage ?? null,
21
+ line: v.line ?? null,
22
+ phase: v.phase ?? null,
23
+ eta: v.eta ? v.eta.toISOString() : null,
24
+ etd: v.etd ? v.etd.toISOString() : null,
25
+ ata: v.ata ? v.ata.toISOString() : null,
26
+ atd: v.atd ? v.atd.toISOString() : null,
27
+ beginReceive: v.beginReceive ? v.beginReceive.toISOString() : null,
28
+ dryCutoff: v.dryCutoff ? v.dryCutoff.toISOString() : null,
29
+ updatedAt: v.updatedAt ? v.updatedAt.toISOString() : null,
30
+ }
31
+ }
32
+
33
+ /**
34
+ * List resolved vessel visits. Filter by `visitRef`, by `containerNumber`
35
+ * (resolved through the container's terminal events' visit refs), or page the
36
+ * most-recently-updated visits.
37
+ */
38
+ export async function GET(request: NextRequest) {
39
+ const auth = await getAuthFromRequest(request)
40
+ if (!auth || !auth.orgId || !auth.tenantId) {
41
+ return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
42
+ }
43
+
44
+ const parsed = vesselVisitListSchema.safeParse(
45
+ Object.fromEntries(new URL(request.url).searchParams),
46
+ )
47
+ if (!parsed.success) {
48
+ return NextResponse.json({ error: 'Invalid query', details: parsed.error.flatten() }, { status: 400 })
49
+ }
50
+ const { visitRef, containerNumber, terminalCode } = parsed.data
51
+ const page = parsed.data.page ?? 1
52
+ const pageSize = parsed.data.pageSize ?? 50
53
+
54
+ const container = await createRequestContainer()
55
+ const scope = await resolveOrganizationScopeForRequest({ container, auth, request })
56
+ const em = container.resolve('em') as EntityManager
57
+ const orgIds = scope?.filterIds ?? [auth.orgId]
58
+
59
+ const baseWhere = {
60
+ tenantId: auth.tenantId,
61
+ organizationId: { $in: orgIds },
62
+ ...(terminalCode ? { terminalCode } : {}),
63
+ }
64
+
65
+ // Resolve the set of visit refs to query.
66
+ let refFilter: Record<string, unknown> | null = null
67
+ if (visitRef) {
68
+ refFilter = { visitRef }
69
+ } else if (containerNumber) {
70
+ const events = await em.find(
71
+ TerminalEvent,
72
+ { containerNumber, tenantId: auth.tenantId, organizationId: { $in: orgIds } },
73
+ { fields: ['visitRefIn', 'visitRefOut'] },
74
+ )
75
+ const refs = [
76
+ ...new Set(
77
+ events.flatMap((e) => [e.visitRefIn, e.visitRefOut]).filter((r): r is string => !!r),
78
+ ),
79
+ ]
80
+ if (refs.length === 0) {
81
+ return NextResponse.json({ items: [], total: 0, page, pageSize })
82
+ }
83
+ refFilter = { visitRef: { $in: refs } }
84
+ }
85
+
86
+ const where = { ...baseWhere, ...(refFilter ?? {}) }
87
+
88
+ const [items, total] = await em.findAndCount(TerminalVesselVisit, where, {
89
+ orderBy: { updatedAt: 'desc' },
90
+ limit: pageSize,
91
+ offset: (page - 1) * pageSize,
92
+ })
93
+
94
+ return NextResponse.json({ items: items.map(serialize), total, page, pageSize })
95
+ }
@@ -0,0 +1,16 @@
1
+ import React from 'react'
2
+ import { Building2 } from 'lucide-react'
3
+
4
+ export const metadata = {
5
+ requireAuth: true,
6
+ requireFeatures: ['terminal_tracking.terminal_configs.view'],
7
+ pageTitle: 'Terminals',
8
+ pageTitleKey: 'terminal_tracking.nav.terminal_configs',
9
+ pageGroup: 'Terminal Tracking',
10
+ pageGroupKey: 'terminal_tracking.nav.group',
11
+ pageContext: 'settings' as const,
12
+ pagePriority: 10,
13
+ pageOrder: 100,
14
+ icon: React.createElement(Building2, { size: 16 }),
15
+ breadcrumb: [{ label: 'Terminals', labelKey: 'terminal_tracking.nav.terminal_configs' }],
16
+ }