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

93 lines
7.5 KiB
TypeScript

import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
export const workTypes = ['PICKUP', 'WAREHOUSE_RECEIPT', 'DELIVERY', 'TRANSPORT', 'EMPTY_RETURN'] as const
export type WorkType = typeof workTypes[number] | 'ROUTE'
export type WorkFlow = 'TRANSFER' | 'DIRECT' | 'UNKNOWN'
export type WorkView = 'ALL' | 'FOLLOW_TODAY' | 'OVERDUE' | 'UNDATED' | 'REVIEW'
export type WorkBucket = 'PENDING' | 'CONFIRMED' | 'NO_BOOKING_REQUIRED' | 'REVIEW' | 'COMPLETED'
export interface WorkSlot { date: string; startTime: string | null; endDate: string | null; endTime: string | null; timezone: string | null }
export const workCommands = ['CONFIRM_ROUTE', 'SAVE_FOLLOWUP', 'RESOLVE_REVIEW', 'SAVE_REQUIREMENT', 'HANDOFF_RETURN', 'ACCEPT_RETURN'] as const
export type WorkCommandName = typeof workCommands[number]
export const actionCodes = ['VIEW_DETAIL', 'SAVE_PLAN', 'REQUEST_BOOKING', 'RECORD_CONFIRMATION', 'CHANGE_BOOKING', 'CANCEL_BOOKING', 'VERIFY_CARGO', 'EDIT_BATCH', 'ARRANGE_TRANSPORT', 'RECORD_EXECUTION', 'CONFIRM_ROUTE', 'SAVE_FOLLOWUP', 'RESOLVE_REVIEW', 'SAVE_REQUIREMENT', 'HANDOFF_RETURN', 'ACCEPT_RETURN', 'VIEW_HISTORY'] as const
export interface SourceReference { customerId: string; sourceKey: string; destinationKey: string | null }
export interface BusinessReference { kind: string; key: string; businessNo: string | null; label: string }
export type ActionTarget =
| { kind: 'WORK_COMMAND'; command: WorkCommandName }
| { kind: 'CARGO_VERIFICATION'; source: SourceReference }
| { kind: 'FBA_APPOINTMENT'; appointmentId: string | null; batchKey: string | null; sources: SourceReference[] }
| { kind: 'FIRSTLEG_CONTAINER'; containerBookingId: string }
| { kind: 'FLEET_TRIP'; tripId: string | null; batchKeys: string[] }
| { kind: 'WAREHOUSE_RECEIPT'; inboundPlanId: string | null; inboundPlanNo: string | null; customerId: string | null; containerBookingId: string }
| { kind: 'READ_ONLY'; references: BusinessReference[] }
export interface WorkAction { code: typeof actionCodes[number]; label: string; targetWorkItemKey: string | null; target: ActionTarget }
export interface Responsibility { kind: 'USER' | 'SUPPLIER' | 'EXTERNAL'; id: string | null; name: string }
export interface WorkItem {
workItemKey: string; version: number; type: WorkType; flow: WorkFlow
title: string; businessNo: string | null; containers: Array<{ containerBookingId: string; bookingNo: string | null; containerNo: string | null }>
requirementKey: string | null; batchKey: string | null; batchNo: string | null
originName: string | null; destinationName: string | null
planStatus: 'UNPLANNED' | 'TARGET_SET' | 'AWAITING_REPLY' | 'CONFIRMED' | 'NOT_REQUIRED'
executionStatus: 'PENDING' | 'IN_PROGRESS' | 'COMPLETED'; reviewRequired: boolean
target: WorkSlot | null; confirmed: WorkSlot | null
actual: { occurredAt: string | null; localOccurredAt: string | null; timezone: string | null } | null
calendarDate: string | null; dateBasis: 'ACTUAL' | 'CONFIRMED' | 'TARGET' | 'NONE'; timezone: string | null
owner: { id: string; name: string } | null; followupDate: string | null; overdueReasons: string[]
allowedActions: WorkAction[]; nextAction: WorkAction | null; blockReasons: string[]; missingFields: string[]
}
export interface WorkDetail {
item: WorkItem; relatedItems: WorkItem[]
references: BusinessReference[]
history: Array<{ occurredAt: string; operatorName: string | null; action: string; summary: string }>
review: { version: number; reasons: string[] } | null
requirement: { bookingRequired: boolean; receivingRequirements: string; reason: string | null } | null
returnHandoff: { version: number; from: Responsibility; to: Responsibility; status: 'PENDING_ACCEPTANCE' | 'ACCEPTED'; reason: string } | null
}
export interface WorkSummary { all: number; followToday: number; overdue: number; undated: number; review: number }
export interface WorkCounts { pending: number; confirmed: number; noBookingRequired: number; review: number; completed: number }
export interface WorkDay { date: string; total: number; typeCounts: Partial<Record<WorkType, number>>; counts: WorkCounts }
export interface WorkCalendar { from: string; to: string; total: number; counts: WorkCounts; days: WorkDay[] }
export interface WorkPage { records: WorkItem[]; page: number; size: number; total: number; pages: number }
export interface WorkCalendarQuery { from: string; to: string; flow?: WorkFlow; types?: WorkType[] }
export interface WorkItemsQuery { view: WorkView; page: number; size: number; date?: string; flow?: WorkFlow; types?: WorkType[]; containerBookingId?: string; bucket?: WorkBucket; keyword?: string }
export type WorkCommandPayload =
| { command: 'CONFIRM_ROUTE'; payload: { flow: 'TRANSFER' | 'DIRECT'; reason: string | null } }
| { command: 'SAVE_FOLLOWUP'; payload: { ownerId: string; followupDate: string | null; note: string } }
| { command: 'RESOLVE_REVIEW'; payload: { reviewVersion: number; note: string } }
| { command: 'SAVE_REQUIREMENT'; payload: { target: WorkSlot | null; bookingRequired: boolean; receivingRequirements: string; reason: string } }
| { command: 'HANDOFF_RETURN'; payload: { recipient: Responsibility; reason: string } }
| { command: 'ACCEPT_RETURN'; payload: { handoffVersion: number; note: string | null } }
export type WorkCommand = WorkCommandPayload & { expectedVersion: number; idempotencyKey: string }
export interface WorkCommandResult { item: WorkItem; affectedWorkItemKeys: string[] }
export interface BatchWorkResolution { batchKey: string; customerId: string | null; detail: WorkDetail | null; candidates: Array<{ customerId: string; workItemKey: string; batchNo: string | null }> }
const base = '/platform/v1/last-leg/appointment-work'
// Repeated types parameters are part of v1; Axios' default types[] syntax is not.
function params(query: WorkCalendarQuery | WorkItemsQuery) {
const result = new URLSearchParams()
for (const [key, value] of Object.entries(query)) {
if (value === undefined) continue
if (Array.isArray(value)) value.forEach(type => result.append(key, type))
else result.set(key, String(value))
}
return result
}
export async function getWorkCalendar(query: WorkCalendarQuery, signal?: AbortSignal) {
return unwrapApiResponse(await http.get<ApiEnvelope<WorkCalendar>, ApiEnvelope<WorkCalendar>>(`${base}/calendar`, { params: params(query), signal }))
}
export async function getWorkSummary(signal?: AbortSignal) {
return unwrapApiResponse(await http.get<ApiEnvelope<WorkSummary>, ApiEnvelope<WorkSummary>>(`${base}/summary`, { signal }))
}
export async function getWorkItems(query: WorkItemsQuery, signal?: AbortSignal) {
const result = unwrapApiResponse(await http.get<ApiEnvelope<WorkPage>, ApiEnvelope<WorkPage>>(`${base}/items`, { params: params(query), signal }))
return { ...result, page: Number(result.page), size: Number(result.size), total: Number(result.total), pages: Number(result.pages) }
}
export async function getWorkDetail(key: string, signal?: AbortSignal) {
return unwrapApiResponse(await http.get<ApiEnvelope<WorkDetail>, ApiEnvelope<WorkDetail>>(`${base}/items/${encodeURIComponent(key)}`, { signal }))
}
export async function resolveBatchWork(batchKey: string, customerId?: string, signal?: AbortSignal) {
return unwrapApiResponse(await http.get<ApiEnvelope<BatchWorkResolution>, ApiEnvelope<BatchWorkResolution>>(`${base}/items/resolve`, { params: { batchKey, customerId }, signal }))
}
export async function submitWorkCommand(key: string, command: WorkCommand) {
return unwrapApiResponse(await http.post<ApiEnvelope<WorkCommandResult>, ApiEnvelope<WorkCommandResult>>(`${base}/items/${encodeURIComponent(key)}/commands`, command))
}