Files
admin-web/src/modules/appointment-calendar/planningApi.ts
T

131 lines
9.5 KiB
TypeScript

import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
import type { FbaAppointmentDetailsPayload, FbaAppointmentDto } from '@/modules/overseas-warehouse'
export interface PlanningCargo {
sourceKey: string
orderId: string
customerId: string
customerName: string | null
inboundCode: string | null
containerBookingId: string
containerNo: string | null
originWarehouseId: string
originWarehouseCode: string
originWarehouseName: string
destinationWarehouseCode: string
packages: number | null
receivedPackages: number | null
shippedPackages: number | null
sourcePalletCount: number | null
volumeCbm: number | null
estimatedArrivalAt: string | null
estimatedWarehouseAt: string | null
orderVersion: number
standardNode: string
shippingVersion: string | null
shippingObservedAt: string | null
shippingProvider: string | null
hasInboundPlan: boolean
hasActiveAppointment: boolean
detained: boolean
mixedDestinations: boolean
}
export interface PlanningCargoPage { records: PlanningCargo[]; page: number; size: number; total: number; pages: number }
export async function listPlanningCargo(query: { page: number; size: number; keyword?: string }, signal?: AbortSignal) {
const response = await http.get<ApiEnvelope<PlanningCargoPage>, ApiEnvelope<PlanningCargoPage>>(
'/platform/v1/last-leg/appointment-planning/cargo', { params: query, signal },
)
const page = unwrapApiResponse(response)
return { ...page, page: Number(page.page), size: Number(page.size), total: Number(page.total), pages: Number(page.pages) }
}
export type FollowupStage = 'DRAYAGE_CONTACT' | 'TERMINAL_APPOINTMENT' | 'WAREHOUSE_RECEIVING' | 'FBA_RECEIVING'
export interface FollowupRecord { stage: FollowupStage; version: number; contactedParty: string | null; result: string; nextAction: string | null; followupDate: string | null; operatorName: string | null; createdAt: string }
export interface FollowupHistory { history: { records: FollowupRecord[]; page: number; size: number; total: number; pages: number }; versions: Record<FollowupStage, number> }
export interface SaveFollowup { stage: FollowupStage; expectedVersion: number; idempotencyKey: string; contactedParty?: string; result: string; nextAction?: string; followupDate: string | null }
export async function getFollowups(sourceKey: string, customerId: string, page: number, size: number) {
const result = unwrapApiResponse(await http.get<ApiEnvelope<FollowupHistory>, ApiEnvelope<FollowupHistory>>(
`/platform/v1/last-leg/appointment-planning/sources/${encodeURIComponent(sourceKey)}/follow-ups`, { params: { customerId, page, size } },
))
return { ...result, history: { ...result.history, total: Number(result.history.total) }, versions: Object.fromEntries(Object.entries(result.versions).map(([stage, version]) => [stage, Number(version)])) as Record<FollowupStage, number> }
}
export async function saveFollowup(sourceKey: string, customerId: string, body: SaveFollowup) {
return unwrapApiResponse(await http.post<ApiEnvelope<FollowupRecord>, ApiEnvelope<FollowupRecord>>(
`/platform/v1/last-leg/appointment-planning/sources/${encodeURIComponent(sourceKey)}/follow-ups`, body, { params: { customerId } },
))
}
export interface PlanningSource {
sourceKey: string; customerId: string; orderId: string | null; inboundCode: string | null; bookingNo: string | null;
containerNo: string | null; originWarehouseId: string; originWarehouseName: string; destinationKey: string;
destinationWarehouseCode: string; packages: number | null; sourceVersion: string | null; canFollowup: boolean; blockReasons: string[];
}
export interface AppointmentCoverage {
status: string; missingFields: string[]; changeReviewRequired: boolean;
appointments: Array<{ appointmentNo: string; confirmationNo: string | null; status: string; confirmedAt: string | null; confirmedEndAt: string | null; timezone: string | null }>;
}
export interface ResolvedPlanningSource { source: PlanningSource; coverage: AppointmentCoverage }
export interface ExternalAppointmentCandidate extends ResolvedPlanningSource { customerName: string | null; tripNo: string }
export async function listExternalAppointmentCandidates(query: { page: number; size: number; keyword?: string }, signal?: AbortSignal) {
const result = unwrapApiResponse(await http.get<ApiEnvelope<{ records: ExternalAppointmentCandidate[]; total: number }>, ApiEnvelope<{ records: ExternalAppointmentCandidate[]; total: number }>>(
'/platform/v1/last-leg/appointment-planning/external-candidates', { params: query, signal },
))
return { ...result, total: Number(result.total) }
}
export interface PlanningBatchPreview {
group: { originWarehouseId: string; destinationKey: string; packages: number | null; sourceCount: number; customerCount: number; readinessIncomplete: boolean; readyFrom: string | null; readyTo: string | null; unknownQuantitySources: number }
originWarehouseName: string
destinationWarehouseCode: string
sources: PlanningSource[]
missingFields: string[]
}
export async function previewPlanningBatch(sources: Array<{sourceKey: string; customerId: string; destinationKey: string}>) {
const result = unwrapApiResponse(await http.post<ApiEnvelope<PlanningBatchPreview>, ApiEnvelope<PlanningBatchPreview>>(
'/platform/v1/last-leg/appointment-planning/batches/preview', { sources },
))
return { ...result, group: { ...result.group, packages: result.group.packages == null ? null : Number(result.group.packages), sourceCount: Number(result.group.sourceCount), customerCount: Number(result.group.customerCount) } }
}
export interface SourceAppointmentPayload { originWarehouseId: string; fbaWarehouseCode: string; sources: Array<{sourceKey: string; customerId: string; destinationKey: string}>; details: FbaAppointmentDetailsPayload; idempotencyKey: string }
export async function createSourceAppointment(payload: SourceAppointmentPayload) {
return unwrapApiResponse(await http.post<ApiEnvelope<FbaAppointmentDto>, ApiEnvelope<FbaAppointmentDto>>('/platform/v1/last-leg/appointment-planning/appointments', payload))
}
export async function listUnscheduledAppointments(page: number, size: number) {
const result = unwrapApiResponse(await http.get<ApiEnvelope<{records: FbaAppointmentDto[]; total: number}>, ApiEnvelope<{records: FbaAppointmentDto[]; total: number}>>('/platform/v1/last-leg/appointment-planning/appointments/unscheduled', { params: { page, size } }))
return { ...result, total: Number(result.total) }
}
export async function resolvePlanningSource(query: { sourceKey: string; customerId: string; destinationKey?: string }) {
return unwrapApiResponse(await http.get<ApiEnvelope<ResolvedPlanningSource>, ApiEnvelope<ResolvedPlanningSource>>(
'/platform/v1/last-leg/appointment-planning/sources/resolve', { params: query },
))
}
export interface PlanningContainer {
containerBookingId: string; containerNo: string | null; orderCount: number; customerCount: number;
packages: number | null; unknownPackageOrders: number; earliestArrivalAt: string | null; latestArrivalAt: string | null;
standardNode: string; sourceVersion: string | null; incompleteScope: boolean;
warehouses: Array<{ warehouseId: string; warehouseName: string; orderCount: number; customerCount: number }>;
}
export interface ContainerFollowupContext {
containerBookingId: string; containerNo: string | null; warehouseId: string | null; warehouseName: string | null;
canFollowup: boolean; blockReasons: string[];
}
const containerBase = '/platform/v1/last-leg/appointment-planning/containers'
export async function listPlanningContainers(query: { page: number; size: number; keyword?: string }, signal?: AbortSignal) {
const result = unwrapApiResponse(await http.get<ApiEnvelope<{ records: PlanningContainer[]; total: number }>, ApiEnvelope<{ records: PlanningContainer[]; total: number }>>(containerBase, { params: query, signal }))
return { ...result, total: Number(result.total), records: result.records.map(row => ({
...row, orderCount: Number(row.orderCount), customerCount: Number(row.customerCount),
packages: row.packages == null ? null : Number(row.packages), unknownPackageOrders: Number(row.unknownPackageOrders),
warehouses: row.warehouses.map(warehouse => ({ ...warehouse, orderCount: Number(warehouse.orderCount), customerCount: Number(warehouse.customerCount) })),
})) }
}
export async function resolvePlanningContainer(containerId: string, warehouseId?: string) {
return unwrapApiResponse(await http.get<ApiEnvelope<ContainerFollowupContext>, ApiEnvelope<ContainerFollowupContext>>(`${containerBase}/${encodeURIComponent(containerId)}`, { params: { warehouseId } }))
}
export async function getContainerFollowups(context: ContainerFollowupContext, page: number, size: number) {
const result = unwrapApiResponse(await http.get<ApiEnvelope<FollowupHistory>, ApiEnvelope<FollowupHistory>>(`${containerBase}/${encodeURIComponent(context.containerBookingId)}/follow-ups`, { params: { warehouseId: context.warehouseId || undefined, page, size } }))
return { ...result, history: { ...result.history, total: Number(result.history.total) }, versions: Object.fromEntries(Object.entries(result.versions).map(([stage, version]) => [stage, Number(version)])) as Record<FollowupStage, number> }
}
export async function saveContainerFollowup(context: ContainerFollowupContext, body: SaveFollowup) {
return unwrapApiResponse(await http.post<ApiEnvelope<FollowupRecord>, ApiEnvelope<FollowupRecord>>(`${containerBase}/${encodeURIComponent(context.containerBookingId)}/follow-ups`, body, { params: { warehouseId: context.warehouseId || undefined } }))
}