@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,16 @@
1
+ import React from 'react'
2
+ import { Container } from 'lucide-react'
3
+
4
+ export const metadata = {
5
+ requireAuth: true,
6
+ requireFeatures: ['terminal_tracking.tracking_jobs.view'],
7
+ pageTitle: 'Tracked Containers',
8
+ pageTitleKey: 'terminal_tracking.nav.tracking_jobs',
9
+ pageGroup: 'Terminal Tracking',
10
+ pageGroupKey: 'terminal_tracking.nav.group',
11
+ pageContext: 'settings' as const,
12
+ pagePriority: 10,
13
+ pageOrder: 110,
14
+ icon: React.createElement(Container, { size: 16 }),
15
+ breadcrumb: [{ label: 'Tracked Containers', labelKey: 'terminal_tracking.nav.tracking_jobs' }],
16
+ }
@@ -0,0 +1,328 @@
1
+ 'use client'
2
+
3
+ import * as React from 'react'
4
+ import { useCallback, useMemo, useState } from 'react'
5
+ import { Page, PageBody } from '@freighttech/ui/backend/Page'
6
+ import { DynamicTable, TableSkeleton, useDynamicTablePage } from '@freighttech/ui/backend/dynamic-table'
7
+ import type { ColumnDef } from '@freighttech/ui/backend/dynamic-table'
8
+ import { RowActions, type RowActionItem } from '@freighttech/ui/backend/RowActions'
9
+ import { apiCallOrThrow } from '@freighttech/ui/backend/utils/apiCall'
10
+ import { flash } from '@freighttech/ui/backend/FlashMessages'
11
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
12
+ import {
13
+ Dialog,
14
+ DialogContent,
15
+ DialogHeader,
16
+ DialogTitle,
17
+ DialogFooter,
18
+ } from '@freighttech/ui/primitives/dialog'
19
+ import { Button } from '@freighttech/ui/primitives/button'
20
+ import { Input } from '@freighttech/ui/primitives/input'
21
+ import { Label } from '@freighttech/ui/primitives/label'
22
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@freighttech/ui/primitives/card'
23
+ import { Loader2 } from 'lucide-react'
24
+ import { ContainerDetailsDrawer } from '../../components/ContainerDetailsDrawer'
25
+
26
+ type JobRow = {
27
+ id: string
28
+ terminalCode: string
29
+ containerNumber: string
30
+ status: string
31
+ gateInAt: string | null
32
+ gateOutAt: string | null
33
+ holds: string[]
34
+ lastPollAt: string | null
35
+ }
36
+
37
+ function mapItem(item: Record<string, unknown>): JobRow | null {
38
+ const id = typeof item.id === 'string' ? item.id : null
39
+ if (!id) return null
40
+ return {
41
+ id,
42
+ terminalCode: (item.terminalCode as string) ?? '',
43
+ containerNumber: (item.containerNumber as string) ?? '',
44
+ status: (item.status as string) ?? '',
45
+ gateInAt: (item.gateInAt as string) ?? null,
46
+ gateOutAt: (item.gateOutAt as string) ?? null,
47
+ holds: Array.isArray(item.holds) ? (item.holds as string[]) : [],
48
+ lastPollAt: (item.lastPollAt as string) ?? null,
49
+ }
50
+ }
51
+
52
+ function DateTimeCell({ value }: { value: unknown }): React.ReactElement {
53
+ return <span className="text-sm">{value ? new Date(String(value)).toLocaleString() : '-'}</span>
54
+ }
55
+
56
+ const API = '/api/terminal_tracking/tracking-jobs'
57
+
58
+ export default function TrackingJobsPage() {
59
+ const t = useT()
60
+ const [dialogOpen, setDialogOpen] = useState(false)
61
+ const [terminalCode, setTerminalCode] = useState('')
62
+ const [containerNumber, setContainerNumber] = useState('')
63
+ const [submitting, setSubmitting] = useState(false)
64
+
65
+ const columns = useMemo<ColumnDef[]>(
66
+ () => [
67
+ {
68
+ data: 'containerNumber',
69
+ title: t('terminal_tracking.tracking_job.containerNumber', 'Container'),
70
+ width: 160,
71
+ readOnly: true,
72
+ renderer: (v: unknown) => <span className="font-medium font-mono">{String(v ?? '')}</span>,
73
+ },
74
+ {
75
+ data: 'terminalCode',
76
+ title: t('terminal_tracking.terminal_config.terminalCode', 'Terminal'),
77
+ width: 120,
78
+ readOnly: true,
79
+ },
80
+ {
81
+ data: 'status',
82
+ title: t('terminal_tracking.tracking_job.status', 'Status'),
83
+ width: 110,
84
+ readOnly: true,
85
+ },
86
+ {
87
+ data: 'gateInAt',
88
+ title: t('terminal_tracking.tracking_job.gateIn', 'Gate in'),
89
+ width: 170,
90
+ readOnly: true,
91
+ renderer: (v: unknown) => <DateTimeCell value={v} />,
92
+ },
93
+ {
94
+ data: 'gateOutAt',
95
+ title: t('terminal_tracking.tracking_job.gateOut', 'Gate out'),
96
+ width: 170,
97
+ readOnly: true,
98
+ renderer: (v: unknown) => <DateTimeCell value={v} />,
99
+ },
100
+ {
101
+ data: 'holds',
102
+ title: t('terminal_tracking.tracking_job.holds', 'Holds'),
103
+ width: 220,
104
+ readOnly: true,
105
+ renderer: (v: unknown) => {
106
+ const holds = Array.isArray(v) ? (v as string[]) : []
107
+ if (!holds.length) return <span className="text-sm text-muted-foreground">-</span>
108
+ return (
109
+ <span className="text-sm" title={holds.join(', ')}>
110
+ {holds.join(', ')}
111
+ </span>
112
+ )
113
+ },
114
+ },
115
+ {
116
+ data: 'lastPollAt',
117
+ title: t('terminal_tracking.tracking_job.lastPollAt', 'Last polled'),
118
+ width: 180,
119
+ readOnly: true,
120
+ renderer: (v: unknown) => <DateTimeCell value={v} />,
121
+ },
122
+ ],
123
+ [t],
124
+ )
125
+
126
+ const openCreate = useCallback(() => {
127
+ setTerminalCode('')
128
+ setContainerNumber('')
129
+ setDialogOpen(true)
130
+ }, [])
131
+
132
+ const addButton = useMemo(
133
+ () => (
134
+ <Button size="sm" onClick={openCreate}>
135
+ {t('terminal_tracking.tracking_job.create', 'Track Container')}
136
+ </Button>
137
+ ),
138
+ [openCreate, t],
139
+ )
140
+
141
+ const table = useDynamicTablePage<JobRow>({
142
+ source: API,
143
+ columns,
144
+ tableName: t('terminal_tracking.tracking_job.title', 'Tracked Containers'),
145
+ defaultPageSize: 20,
146
+ mapApiItem: mapItem,
147
+ cellEdit: false,
148
+ tableProps: {
149
+ uiConfig: {
150
+ readOnlyStyle: 'normal',
151
+ hideAddRowButton: true,
152
+ topBarEnd: addButton,
153
+ },
154
+ },
155
+ })
156
+
157
+ // Grow the grid to fit its rows so the page — not the table — owns the scroll.
158
+ // `height` sizes the WHOLE DynamicTable (toolbar + scroll area + pagination as a
159
+ // flex column), so it must cover the chrome (~100px) plus the scroll area's own
160
+ // content: a 37px column header + one 37px row each. Sizing to content this way
161
+ // removes the inner scrollbar; a small row floor keeps the empty state sensible.
162
+ const CHROME_PX = 100 // top toolbar + bottom pagination
163
+ const ROW_PX = 37 // matches the DynamicTable virtualizer row estimate
164
+ const rowCount = table.props.data?.length ?? 0
165
+ const tableHeight = `${CHROME_PX + ROW_PX + Math.max(rowCount, 3) * ROW_PX}px`
166
+
167
+ const handleSubmit = useCallback(async () => {
168
+ if (submitting) return
169
+ if (!terminalCode.trim() || !containerNumber.trim()) {
170
+ flash(t('terminal_tracking.tracking_job.validation.required', 'Terminal code and container number are required'), 'error')
171
+ return
172
+ }
173
+ setSubmitting(true)
174
+ try {
175
+ await apiCallOrThrow(API, {
176
+ method: 'POST',
177
+ body: JSON.stringify({ terminalCode: terminalCode.trim(), containerNumber: containerNumber.trim() }),
178
+ })
179
+ flash(t('terminal_tracking.tracking_job.flash.created', 'Container is now tracked'), 'success')
180
+ setDialogOpen(false)
181
+ table.refresh()
182
+ } catch (err) {
183
+ flash(err instanceof Error ? err.message : 'Failed to track container', 'error')
184
+ } finally {
185
+ setSubmitting(false)
186
+ }
187
+ }, [submitting, terminalCode, containerNumber, table, t])
188
+
189
+ const handlePoll = useCallback(
190
+ async (id: string) => {
191
+ try {
192
+ const { result } = await apiCallOrThrow<{ newEvents: number; updatedEvents?: number }>(`${API}/poll`, {
193
+ method: 'POST',
194
+ body: JSON.stringify({ id }),
195
+ })
196
+ flash(
197
+ t('terminal_tracking.tracking_job.flash.polled', 'Polled — {n} new, {u} updated event(s)')
198
+ .replace('{n}', String(result?.newEvents ?? 0))
199
+ .replace('{u}', String(result?.updatedEvents ?? 0)),
200
+ 'success',
201
+ )
202
+ table.refresh()
203
+ } catch (err) {
204
+ flash(err instanceof Error ? err.message : 'Poll failed', 'error')
205
+ }
206
+ },
207
+ [table, t],
208
+ )
209
+
210
+ const handleDelete = useCallback(
211
+ async (id: string) => {
212
+ if (!confirm(t('terminal_tracking.tracking_job.confirmDelete', 'Stop tracking this container?'))) return
213
+ try {
214
+ await apiCallOrThrow(API, { method: 'DELETE', body: JSON.stringify({ id }) })
215
+ flash(t('terminal_tracking.tracking_job.flash.deleted', 'Tracking stopped'), 'success')
216
+ table.refresh()
217
+ } catch (err) {
218
+ flash(err instanceof Error ? err.message : 'Failed to stop tracking', 'error')
219
+ }
220
+ },
221
+ [table, t],
222
+ )
223
+
224
+ const handleDialogKeyDown = useCallback(
225
+ (event: React.KeyboardEvent) => {
226
+ if ((event.metaKey || event.ctrlKey) && event.key === 'Enter') {
227
+ event.preventDefault()
228
+ handleSubmit()
229
+ }
230
+ },
231
+ [handleSubmit],
232
+ )
233
+
234
+ const [selectedJobId, setSelectedJobId] = useState<string | null>(null)
235
+ const [drawerOpen, setDrawerOpen] = useState(false)
236
+
237
+ const openDetails = useCallback((id: string) => {
238
+ setSelectedJobId(id)
239
+ setDrawerOpen(true)
240
+ }, [])
241
+
242
+ const handleRowClick = useCallback(
243
+ (_rowIndex: number, rowData: Record<string, unknown>) => {
244
+ const id = rowData?.id as string | undefined
245
+ if (id) openDetails(id)
246
+ },
247
+ [openDetails],
248
+ )
249
+
250
+ const actionsRenderer = useCallback(
251
+ (rowData: { id: string }) => {
252
+ if (!rowData?.id) return null
253
+ const items: RowActionItem[] = [
254
+ { label: t('terminal_tracking.tracking_job.actions.details', 'Details'), onSelect: () => openDetails(rowData.id) },
255
+ { label: t('terminal_tracking.tracking_job.actions.poll', 'Poll now'), onSelect: () => handlePoll(rowData.id) },
256
+ { label: t('common.delete', 'Delete'), onSelect: () => handleDelete(rowData.id), destructive: true },
257
+ ]
258
+ return <RowActions items={items} />
259
+ },
260
+ [t, openDetails, handlePoll, handleDelete],
261
+ )
262
+
263
+ if (table.isLoading) {
264
+ return (
265
+ <Page>
266
+ <PageBody className="space-y-6">
267
+ <Card>
268
+ <CardHeader>
269
+ <CardTitle>{t('terminal_tracking.tracking_job.title', 'Tracked Containers')}</CardTitle>
270
+ </CardHeader>
271
+ <CardContent>
272
+ <TableSkeleton rows={5} columns={7} />
273
+ </CardContent>
274
+ </Card>
275
+ </PageBody>
276
+ </Page>
277
+ )
278
+ }
279
+
280
+ return (
281
+ <Page>
282
+ <PageBody className="space-y-6">
283
+ <Card>
284
+ <CardHeader>
285
+ <CardTitle>{t('terminal_tracking.tracking_job.title', 'Tracked Containers')}</CardTitle>
286
+ <CardDescription>
287
+ {t('terminal_tracking.tracking_job.description', 'Containers polled at configured terminals. Use "Poll now" to fetch immediately.')}
288
+ </CardDescription>
289
+ </CardHeader>
290
+ <CardContent>
291
+ <DynamicTable {...table.props} height={tableHeight} actionsRenderer={actionsRenderer} onRowClick={handleRowClick} />
292
+ </CardContent>
293
+ </Card>
294
+
295
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
296
+ <DialogContent className="sm:max-w-md" onKeyDown={handleDialogKeyDown}>
297
+ <DialogHeader>
298
+ <DialogTitle>{t('terminal_tracking.tracking_job.create', 'Track Container')}</DialogTitle>
299
+ </DialogHeader>
300
+ <div className="grid gap-4 py-2">
301
+ <div className="grid gap-2">
302
+ <Label htmlFor="terminalCode">{t('terminal_tracking.terminal_config.terminalCode', 'Terminal code')}</Label>
303
+ <Input id="terminalCode" value={terminalCode} autoFocus
304
+ onChange={(e) => setTerminalCode(e.target.value)} placeholder="bct" />
305
+ </div>
306
+ <div className="grid gap-2">
307
+ <Label htmlFor="containerNumber">{t('terminal_tracking.tracking_job.containerNumber', 'Container')}</Label>
308
+ <Input id="containerNumber" value={containerNumber}
309
+ onChange={(e) => setContainerNumber(e.target.value.toUpperCase())} placeholder="GCXU5598460" className="font-mono" />
310
+ </div>
311
+ </div>
312
+ <DialogFooter>
313
+ <Button variant="outline" onClick={() => setDialogOpen(false)} disabled={submitting}>
314
+ {t('common.cancel', 'Cancel')}
315
+ </Button>
316
+ <Button onClick={handleSubmit} disabled={submitting}>
317
+ {submitting ? <Loader2 className="mr-2 h-4 w-4 animate-spin" /> : null}
318
+ {t('common.create', 'Create')}
319
+ </Button>
320
+ </DialogFooter>
321
+ </DialogContent>
322
+ </Dialog>
323
+
324
+ <ContainerDetailsDrawer open={drawerOpen} onOpenChange={setDrawerOpen} jobId={selectedJobId} />
325
+ </PageBody>
326
+ </Page>
327
+ )
328
+ }
@@ -0,0 +1,31 @@
1
+ export const entities = [
2
+ {
3
+ id: 'terminal_tracking:terminal_config',
4
+ label: 'Terminal Config',
5
+ description: 'Container terminal API configuration with credentials and matching identifiers.',
6
+ labelField: 'displayName',
7
+ showInSidebar: false,
8
+ defaultEditor: false,
9
+ fields: [],
10
+ },
11
+ {
12
+ id: 'terminal_tracking:tracking_job',
13
+ label: 'Terminal Tracking Job',
14
+ description: 'A container tracked at a terminal.',
15
+ labelField: 'containerNumber',
16
+ showInSidebar: false,
17
+ defaultEditor: false,
18
+ fields: [],
19
+ },
20
+ {
21
+ id: 'terminal_tracking:terminal_event',
22
+ label: 'Terminal Event',
23
+ description: 'A normalized terminal event for a container.',
24
+ labelField: 'eventCode',
25
+ showInSidebar: false,
26
+ defaultEditor: false,
27
+ fields: [],
28
+ },
29
+ ]
30
+
31
+ export default entities
@@ -0,0 +1,3 @@
1
+ import './terminal-configs'
2
+ import './tracking-jobs'
3
+ import './terminal-tracking'
@@ -0,0 +1,132 @@
1
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
2
+ import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { findOneWithDecryption } from '@open-mercato/shared/lib/encryption/find'
5
+ import { TerminalConfig } from '../data/entities'
6
+ import type { TerminalTrackingService } from '../services/terminalTrackingService'
7
+ import type { TerminalRegistry } from '../services/terminalRegistry'
8
+ import { ensureTerminalPollSchedule } from '../lib/ensure-schedule'
9
+ import type {
10
+ TerminalConfigCreateInput,
11
+ TerminalConfigUpdateInput,
12
+ TerminalConfigDeleteInput,
13
+ TerminalConfigTestInput,
14
+ } from '../data/validators'
15
+
16
+ function ensureScope(ctx: CommandRuntimeContext, tenantId: string, organizationId: string) {
17
+ if (ctx.auth?.tenantId && ctx.auth.tenantId !== tenantId) throw new Error('Tenant mismatch')
18
+ if (!ctx.auth?.isSuperAdmin && ctx.auth?.organizationId && ctx.auth.organizationId !== organizationId) {
19
+ throw new Error('Organization mismatch')
20
+ }
21
+ }
22
+
23
+ const createTerminalConfig: CommandHandler<TerminalConfigCreateInput, { id: string }> = {
24
+ id: 'terminal_tracking.terminal_config.create',
25
+ async execute(input, ctx) {
26
+ ensureScope(ctx, input.tenantId, input.organizationId)
27
+ const em = ctx.container.resolve<EntityManager>('em').fork()
28
+
29
+ // Endpoint paths are standardized per adapter family; default from the
30
+ // adapter when the client omits them (only non-standard terminals override).
31
+ const registry = ctx.container.resolve<TerminalRegistry>('terminalTrackingRegistry')
32
+ const endpoints = input.endpoints ?? registry.get(input.adapterType)?.defaultEndpoints
33
+ if (!endpoints) {
34
+ throw new Error(`Unknown adapter type '${input.adapterType}' and no endpoints provided`)
35
+ }
36
+
37
+ const config = em.create(TerminalConfig, {
38
+ organizationId: input.organizationId,
39
+ tenantId: input.tenantId,
40
+ terminalCode: input.terminalCode,
41
+ adapterType: input.adapterType,
42
+ displayName: input.displayName,
43
+ baseUrl: input.baseUrl,
44
+ proxyUrl: input.proxyUrl ?? null,
45
+ endpoints,
46
+ authType: input.authType,
47
+ tokenUrl: input.tokenUrl ?? null,
48
+ scope: input.scope ?? null,
49
+ clientId: input.clientId ?? null,
50
+ authConfig: input.authConfig ?? null,
51
+ rateLimitRequests: input.rateLimitRequests ?? 200,
52
+ rateLimitWindowSeconds: input.rateLimitWindowSeconds ?? 60,
53
+ unlocode: input.unlocode ?? null,
54
+ bicCodes: input.bicCodes ?? null,
55
+ smdgCodes: input.smdgCodes ?? null,
56
+ nameAliases: input.nameAliases ?? null,
57
+ isActive: input.isActive ?? true,
58
+ })
59
+ await em.flush()
60
+ // Self-heal the periodic poll schedule for orgs that predate the module.
61
+ await ensureTerminalPollSchedule(ctx.container, {
62
+ organizationId: input.organizationId,
63
+ tenantId: input.tenantId,
64
+ })
65
+ return { id: config.id }
66
+ },
67
+ }
68
+
69
+ const updateTerminalConfig: CommandHandler<TerminalConfigUpdateInput, { id: string }> = {
70
+ id: 'terminal_tracking.terminal_config.update',
71
+ async execute(input, ctx) {
72
+ const em = ctx.container.resolve<EntityManager>('em').fork()
73
+ const config = await findOneWithDecryption(em, TerminalConfig, { id: input.id, deletedAt: null })
74
+ if (!config) throw new Error('Terminal config not found')
75
+ ensureScope(ctx, config.tenantId, config.organizationId)
76
+
77
+ if (input.adapterType !== undefined) config.adapterType = input.adapterType
78
+ if (input.displayName !== undefined) config.displayName = input.displayName
79
+ if (input.baseUrl !== undefined) config.baseUrl = input.baseUrl
80
+ if (input.proxyUrl !== undefined) config.proxyUrl = input.proxyUrl ?? null
81
+ if (input.endpoints !== undefined) config.endpoints = input.endpoints
82
+ if (input.authType !== undefined) config.authType = input.authType
83
+ if (input.tokenUrl !== undefined) config.tokenUrl = input.tokenUrl ?? null
84
+ if (input.scope !== undefined) config.scope = input.scope ?? null
85
+ if (input.clientId !== undefined) config.clientId = input.clientId ?? null
86
+ if (input.authConfig !== undefined) config.authConfig = input.authConfig ?? null
87
+ if (input.rateLimitRequests !== undefined) config.rateLimitRequests = input.rateLimitRequests
88
+ if (input.rateLimitWindowSeconds !== undefined) config.rateLimitWindowSeconds = input.rateLimitWindowSeconds
89
+ if (input.unlocode !== undefined) config.unlocode = input.unlocode ?? null
90
+ if (input.bicCodes !== undefined) config.bicCodes = input.bicCodes ?? null
91
+ if (input.smdgCodes !== undefined) config.smdgCodes = input.smdgCodes ?? null
92
+ if (input.nameAliases !== undefined) config.nameAliases = input.nameAliases ?? null
93
+ if (input.isActive !== undefined) config.isActive = input.isActive
94
+
95
+ await em.flush()
96
+ await ensureTerminalPollSchedule(ctx.container, {
97
+ organizationId: config.organizationId,
98
+ tenantId: config.tenantId,
99
+ })
100
+ return { id: config.id }
101
+ },
102
+ }
103
+
104
+ const deleteTerminalConfig: CommandHandler<TerminalConfigDeleteInput, { id: string }> = {
105
+ id: 'terminal_tracking.terminal_config.delete',
106
+ async execute(input, ctx) {
107
+ const em = ctx.container.resolve<EntityManager>('em').fork()
108
+ const config = await findOneWithDecryption(em, TerminalConfig, { id: input.id, deletedAt: null })
109
+ if (!config) throw new Error('Terminal config not found')
110
+ ensureScope(ctx, config.tenantId, config.organizationId)
111
+ config.deletedAt = new Date()
112
+ await em.flush()
113
+ return { id: config.id }
114
+ },
115
+ }
116
+
117
+ const testTerminalConfig: CommandHandler<
118
+ TerminalConfigTestInput,
119
+ { success: boolean; message: string; latencyMs?: number }
120
+ > = {
121
+ id: 'terminal_tracking.terminal_config.test',
122
+ async execute(input, ctx) {
123
+ ensureScope(ctx, input.tenantId, input.organizationId)
124
+ const service = ctx.container.resolve<TerminalTrackingService>('terminalTrackingService')
125
+ return service.testTerminalConfig(input)
126
+ },
127
+ }
128
+
129
+ registerCommand(createTerminalConfig)
130
+ registerCommand(updateTerminalConfig)
131
+ registerCommand(deleteTerminalConfig)
132
+ registerCommand(testTerminalConfig)
@@ -0,0 +1,34 @@
1
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
2
+ import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
3
+ import type { TerminalTrackingService } from '../services/terminalTrackingService'
4
+
5
+ function ensureScope(ctx: CommandRuntimeContext, tenantId: string, organizationId?: string) {
6
+ if (ctx.auth?.tenantId && ctx.auth.tenantId !== tenantId) throw new Error('Tenant mismatch')
7
+ if (organizationId && !ctx.auth?.isSuperAdmin && ctx.auth?.organizationId && ctx.auth.organizationId !== organizationId) {
8
+ throw new Error('Organization mismatch')
9
+ }
10
+ }
11
+
12
+ type PollAllInput = { tenantId: string; organizationId?: string }
13
+ type PollAllResult = { polled: number; newEvents: number; failed: number }
14
+
15
+ const pollAllActiveJobs: CommandHandler<PollAllInput, PollAllResult> = {
16
+ id: 'terminal_tracking.tracking.poll_all',
17
+ async execute(input, ctx) {
18
+ ensureScope(ctx, input.tenantId, input.organizationId)
19
+ const service = ctx.container.resolve<TerminalTrackingService>('terminalTrackingService')
20
+ return service.pollAllActiveJobs(input.tenantId, input.organizationId)
21
+ },
22
+ buildLog({ result, input }) {
23
+ return {
24
+ actionLabel: 'Poll all terminal tracking jobs',
25
+ resourceKind: 'terminal_tracking.tracking',
26
+ resourceId: input.organizationId ?? input.tenantId,
27
+ tenantId: input.tenantId,
28
+ organizationId: input.organizationId,
29
+ details: { polled: result.polled, newEvents: result.newEvents, failed: result.failed },
30
+ }
31
+ },
32
+ }
33
+
34
+ registerCommand(pollAllActiveJobs)
@@ -0,0 +1,69 @@
1
+ import { registerCommand } from '@open-mercato/shared/lib/commands'
2
+ import type { CommandHandler, CommandRuntimeContext } from '@open-mercato/shared/lib/commands'
3
+ import type { EntityManager } from '@mikro-orm/postgresql'
4
+ import { TerminalTrackingJob } from '../data/entities'
5
+ import type { TerminalTrackingService } from '../services/terminalTrackingService'
6
+ import { ensureTerminalPollSchedule } from '../lib/ensure-schedule'
7
+ import type {
8
+ TrackingJobCreateInput,
9
+ TrackingJobDeleteInput,
10
+ TrackingJobPollInput,
11
+ } from '../data/validators'
12
+
13
+ function ensureScope(ctx: CommandRuntimeContext, tenantId: string, organizationId: string) {
14
+ if (ctx.auth?.tenantId && ctx.auth.tenantId !== tenantId) throw new Error('Tenant mismatch')
15
+ if (!ctx.auth?.isSuperAdmin && ctx.auth?.organizationId && ctx.auth.organizationId !== organizationId) {
16
+ throw new Error('Organization mismatch')
17
+ }
18
+ }
19
+
20
+ const createTrackingJob: CommandHandler<TrackingJobCreateInput, { id: string; newEvents: number }> = {
21
+ id: 'terminal_tracking.tracking_job.create',
22
+ async execute(input, ctx) {
23
+ ensureScope(ctx, input.tenantId, input.organizationId)
24
+ const service = ctx.container.resolve<TerminalTrackingService>('terminalTrackingService')
25
+ const result = await service.createJob({
26
+ organizationId: input.organizationId,
27
+ tenantId: input.tenantId,
28
+ terminalCode: input.terminalCode,
29
+ containerNumber: input.containerNumber,
30
+ schedule: input.schedule ?? null,
31
+ })
32
+ // Self-heal the periodic poll schedule for orgs that predate the module.
33
+ await ensureTerminalPollSchedule(ctx.container, {
34
+ organizationId: input.organizationId,
35
+ tenantId: input.tenantId,
36
+ })
37
+ return { id: result.trackingJobId, newEvents: result.newEvents }
38
+ },
39
+ }
40
+
41
+ const deleteTrackingJob: CommandHandler<TrackingJobDeleteInput, { id: string }> = {
42
+ id: 'terminal_tracking.tracking_job.delete',
43
+ async execute(input, ctx) {
44
+ const em = ctx.container.resolve<EntityManager>('em').fork()
45
+ const job = await em.findOne(TerminalTrackingJob, { id: input.id, deletedAt: null })
46
+ if (!job) throw new Error('Terminal tracking job not found')
47
+ ensureScope(ctx, job.tenantId, job.organizationId)
48
+ job.deletedAt = new Date()
49
+ job.status = 'deactivated'
50
+ await em.flush()
51
+ return { id: job.id }
52
+ },
53
+ }
54
+
55
+ const pollTrackingJob: CommandHandler<TrackingJobPollInput, { newEvents: number }> = {
56
+ id: 'terminal_tracking.tracking.poll_single',
57
+ async execute(input, ctx) {
58
+ const em = ctx.container.resolve<EntityManager>('em').fork()
59
+ const job = await em.findOne(TerminalTrackingJob, { id: input.jobId, deletedAt: null })
60
+ if (!job) throw new Error('Terminal tracking job not found')
61
+ ensureScope(ctx, job.tenantId, job.organizationId)
62
+ const service = ctx.container.resolve<TerminalTrackingService>('terminalTrackingService')
63
+ return service.pollJob(input.jobId)
64
+ },
65
+ }
66
+
67
+ registerCommand(createTrackingJob)
68
+ registerCommand(deleteTrackingJob)
69
+ registerCommand(pollTrackingJob)