@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,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
|
+
}
|