@freighttech/terminal-tracking 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +121 -0
- package/dist/generated/entities/terminal_config/index.js +53 -0
- package/dist/generated/entities/terminal_config/index.js.map +7 -0
- package/dist/generated/entities/terminal_event/index.js +59 -0
- package/dist/generated/entities/terminal_event/index.js.map +7 -0
- package/dist/generated/entities/terminal_tracking_job/index.js +37 -0
- package/dist/generated/entities/terminal_tracking_job/index.js.map +7 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js +41 -0
- package/dist/generated/entities/terminal_vessel_visit/index.js.map +7 -0
- package/dist/generated/entities.ids.generated.js +26 -0
- package/dist/generated/entities.ids.generated.js.map +7 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +7 -0
- package/dist/modules/terminal_tracking/acl.js +14 -0
- package/dist/modules/terminal_tracking/acl.js.map +7 -0
- package/dist/modules/terminal_tracking/api/openapi.js +27 -0
- package/dist/modules/terminal_tracking/api/openapi.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js +140 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js +58 -0
- package/dist/modules/terminal_tracking/api/terminal-configs/test/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js +109 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/details/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js +58 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/poll/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js +189 -0
- package/dist/modules/terminal_tracking/api/tracking-jobs/route.js.map +7 -0
- package/dist/modules/terminal_tracking/api/utils.js +12 -0
- package/dist/modules/terminal_tracking/api/utils.js.map +7 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js +83 -0
- package/dist/modules/terminal_tracking/api/vessel-visits/route.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js +669 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-configs/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js +275 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.js.map +7 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js +19 -0
- package/dist/modules/terminal_tracking/backend/terminal-containers/page.meta.js.map +7 -0
- package/dist/modules/terminal_tracking/ce.js +35 -0
- package/dist/modules/terminal_tracking/ce.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/index.js +4 -0
- package/dist/modules/terminal_tracking/commands/index.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js +107 -0
- package/dist/modules/terminal_tracking/commands/terminal-configs.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js +27 -0
- package/dist/modules/terminal_tracking/commands/terminal-tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js +56 -0
- package/dist/modules/terminal_tracking/commands/tracking-jobs.js.map +7 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js +371 -0
- package/dist/modules/terminal_tracking/components/ContainerDetailsDrawer.js.map +7 -0
- package/dist/modules/terminal_tracking/data/entities.js +337 -0
- package/dist/modules/terminal_tracking/data/entities.js.map +7 -0
- package/dist/modules/terminal_tracking/data/validators.js +98 -0
- package/dist/modules/terminal_tracking/data/validators.js.map +7 -0
- package/dist/modules/terminal_tracking/di.js +39 -0
- package/dist/modules/terminal_tracking/di.js.map +7 -0
- package/dist/modules/terminal_tracking/encryption.js +14 -0
- package/dist/modules/terminal_tracking/encryption.js.map +7 -0
- package/dist/modules/terminal_tracking/events.js +45 -0
- package/dist/modules/terminal_tracking/events.js.map +7 -0
- package/dist/modules/terminal_tracking/i18n/de.json +175 -0
- package/dist/modules/terminal_tracking/i18n/en.json +178 -0
- package/dist/modules/terminal_tracking/i18n/es.json +175 -0
- package/dist/modules/terminal_tracking/i18n/pl.json +206 -0
- package/dist/modules/terminal_tracking/index.js +15 -0
- package/dist/modules/terminal_tracking/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js +164 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js +23 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/auth/basic.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js +146 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/incos-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/bct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js +106 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js +120 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/auth/token.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js +77 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/gct-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/gct/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js +12 -0
- package/dist/modules/terminal_tracking/lib/adapters/index.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js +121 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js +81 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/auth/ropc.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js +66 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/http.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js +95 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/n4-semantics.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js +70 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js +44 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js +15 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js +1 -0
- package/dist/modules/terminal_tracking/lib/adapters/n4/types.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js +27 -0
- package/dist/modules/terminal_tracking/lib/adapters/zoned-time.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/availability.js +40 -0
- package/dist/modules/terminal_tracking/lib/availability.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js +51 -0
- package/dist/modules/terminal_tracking/lib/ensure-schedule.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/holds.js +240 -0
- package/dist/modules/terminal_tracking/lib/holds.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/logger.js +19 -0
- package/dist/modules/terminal_tracking/lib/logger.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js +30 -0
- package/dist/modules/terminal_tracking/lib/rate-limiter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/stops.js +48 -0
- package/dist/modules/terminal_tracking/lib/stops.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js +1 -0
- package/dist/modules/terminal_tracking/lib/terminal-adapter.js.map +7 -0
- package/dist/modules/terminal_tracking/lib/totp.js +41 -0
- package/dist/modules/terminal_tracking/lib/totp.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js +101 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js +39 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js +15 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.js.map +7 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js +13 -0
- package/dist/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js +61 -0
- package/dist/modules/terminal_tracking/services/terminalMatcherService.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js +24 -0
- package/dist/modules/terminal_tracking/services/terminalRegistry.js.map +7 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js +687 -0
- package/dist/modules/terminal_tracking/services/terminalTrackingService.js.map +7 -0
- package/dist/modules/terminal_tracking/setup.js +23 -0
- package/dist/modules/terminal_tracking/setup.js.map +7 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js +22 -0
- package/dist/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.js.map +7 -0
- package/generated/entities/terminal_config/index.ts +36 -0
- package/generated/entities/terminal_event/index.ts +39 -0
- package/generated/entities/terminal_tracking_job/index.ts +28 -0
- package/generated/entities/terminal_vessel_visit/index.ts +30 -0
- package/generated/entities.ids.generated.ts +52 -0
- package/package.json +80 -0
- package/src/index.ts +44 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-301-gct-config-test-connection.spec.ts +66 -0
- package/src/modules/terminal_tracking/__integration__/TC-TRACK-317-bct-config-test-connection.spec.ts +68 -0
- package/src/modules/terminal_tracking/__integration__/helpers.ts +148 -0
- package/src/modules/terminal_tracking/__integration__/meta.ts +4 -0
- package/src/modules/terminal_tracking/acl.ts +10 -0
- package/src/modules/terminal_tracking/api/openapi.ts +28 -0
- package/src/modules/terminal_tracking/api/terminal-configs/route.ts +140 -0
- package/src/modules/terminal_tracking/api/terminal-configs/test/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/details/route.ts +116 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/poll/route.ts +64 -0
- package/src/modules/terminal_tracking/api/tracking-jobs/route.ts +243 -0
- package/src/modules/terminal_tracking/api/utils.ts +10 -0
- package/src/modules/terminal_tracking/api/vessel-visits/route.ts +95 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-configs/page.tsx +728 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.meta.ts +16 -0
- package/src/modules/terminal_tracking/backend/terminal-containers/page.tsx +328 -0
- package/src/modules/terminal_tracking/ce.ts +31 -0
- package/src/modules/terminal_tracking/commands/index.ts +3 -0
- package/src/modules/terminal_tracking/commands/terminal-configs.ts +132 -0
- package/src/modules/terminal_tracking/commands/terminal-tracking.ts +34 -0
- package/src/modules/terminal_tracking/commands/tracking-jobs.ts +69 -0
- package/src/modules/terminal_tracking/components/ContainerDetailsDrawer.tsx +540 -0
- package/src/modules/terminal_tracking/data/entities.ts +386 -0
- package/src/modules/terminal_tracking/data/validators.ts +113 -0
- package/src/modules/terminal_tracking/di.ts +46 -0
- package/src/modules/terminal_tracking/encryption.ts +12 -0
- package/src/modules/terminal_tracking/events.ts +48 -0
- package/src/modules/terminal_tracking/i18n/de.json +175 -0
- package/src/modules/terminal_tracking/i18n/en.json +178 -0
- package/src/modules/terminal_tracking/i18n/es.json +175 -0
- package/src/modules/terminal_tracking/i18n/pl.json +206 -0
- package/src/modules/terminal_tracking/index.ts +13 -0
- package/src/modules/terminal_tracking/lib/__tests__/availability.test.ts +136 -0
- package/src/modules/terminal_tracking/lib/__tests__/ensure-schedule.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/__tests__/holds.test.ts +73 -0
- package/src/modules/terminal_tracking/lib/__tests__/stops.test.ts +80 -0
- package/src/modules/terminal_tracking/lib/__tests__/totp.test.ts +38 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/adapter.test.ts +196 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/__tests__/incos-semantics.test.ts +303 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/adapter.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/auth/basic.ts +34 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/incos-semantics.ts +291 -0
- package/src/modules/terminal_tracking/lib/adapters/bct/types.ts +113 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/adapter.test.ts +238 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/__tests__/gct-semantics.test.ts +133 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/adapter.ts +152 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/auth/token.ts +219 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/gct-semantics.ts +136 -0
- package/src/modules/terminal_tracking/lib/adapters/gct/types.ts +58 -0
- package/src/modules/terminal_tracking/lib/adapters/index.ts +16 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/adapter-proxy.test.ts +69 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/data-table.test.ts +179 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/http.test.ts +98 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/n4-semantics.test.ts +140 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/proxy.test.ts +19 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/__tests__/vessel-data-table.test.ts +67 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/adapter.ts +163 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/auth/ropc.ts +104 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/http.ts +127 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/n4-semantics.ts +156 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/data-table.ts +102 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/parsers/vessel-data-table.ts +60 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/proxy.ts +24 -0
- package/src/modules/terminal_tracking/lib/adapters/n4/types.ts +56 -0
- package/src/modules/terminal_tracking/lib/adapters/zoned-time.ts +65 -0
- package/src/modules/terminal_tracking/lib/availability.ts +119 -0
- package/src/modules/terminal_tracking/lib/ensure-schedule.ts +83 -0
- package/src/modules/terminal_tracking/lib/holds.ts +344 -0
- package/src/modules/terminal_tracking/lib/logger.ts +21 -0
- package/src/modules/terminal_tracking/lib/rate-limiter.ts +52 -0
- package/src/modules/terminal_tracking/lib/stops.ts +124 -0
- package/src/modules/terminal_tracking/lib/terminal-adapter.ts +136 -0
- package/src/modules/terminal_tracking/lib/totp.ts +63 -0
- package/src/modules/terminal_tracking/migrations/Migration20260609120000_terminal_tracking.ts +104 -0
- package/src/modules/terminal_tracking/migrations/Migration20260610120000_terminal_vessel_visits.ts +40 -0
- package/src/modules/terminal_tracking/migrations/Migration20260611120000_terminal_config_proxy.ts +11 -0
- package/src/modules/terminal_tracking/migrations/Migration20260707120000_terminal_job_availability_markers.ts +13 -0
- package/src/modules/terminal_tracking/migrations/Migration20260805120000_terminal_event_loaded_at.ts +16 -0
- package/src/modules/terminal_tracking/services/__tests__/availabilityEmit.test.ts +277 -0
- package/src/modules/terminal_tracking/services/__tests__/createJob.test.ts +59 -0
- package/src/modules/terminal_tracking/services/__tests__/pollAllBatch.test.ts +147 -0
- package/src/modules/terminal_tracking/services/__tests__/terminalMatcher.test.ts +70 -0
- package/src/modules/terminal_tracking/services/__tests__/vesselResolution.test.ts +453 -0
- package/src/modules/terminal_tracking/services/terminalMatcherService.ts +97 -0
- package/src/modules/terminal_tracking/services/terminalRegistry.ts +30 -0
- package/src/modules/terminal_tracking/services/terminalTrackingService.ts +912 -0
- package/src/modules/terminal_tracking/setup.ts +24 -0
- package/src/modules/terminal_tracking/subscribers/terminal-tracking-job-initial-poll.ts +32 -0
|
@@ -0,0 +1,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,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)
|