优化
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
<script setup lang="ts">
|
||||
import type { InboundFileSummary, InboundInternalNote, InboundPalletLocation, InboundStatusHistory } from './types'
|
||||
|
||||
defineProps<{
|
||||
palletLocations: InboundPalletLocation[]
|
||||
files: InboundFileSummary[]
|
||||
internalNotes: InboundInternalNote[]
|
||||
statusHistory: InboundStatusHistory[]
|
||||
publicRemark: string | null
|
||||
}>()
|
||||
|
||||
function value(value: string | number | null | undefined) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
||||
function number(value: number | null) { return value === null ? '—' : value.toFixed(3) }
|
||||
function date(value: string | null) {
|
||||
if (!value) return '—'
|
||||
const parsed = new Date(value)
|
||||
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="history-panel">
|
||||
<section class="history-section">
|
||||
<div class="section-heading"><h3>托盘与库位</h3><span>{{ palletLocations.length }} 条分配</span></div>
|
||||
<div v-if="palletLocations.length" class="allocation-list">
|
||||
<div v-for="item in palletLocations" :key="`${item.allocationNo}-${item.palletId}-${item.locationId}`" class="allocation-row">
|
||||
<div><strong>{{ value(item.palletCode) }}</strong><span>分配 {{ value(item.allocationNo) }}</span></div>
|
||||
<div><span>{{ number(item.allocatedVolumeCbm) }} CBM</span><span>{{ value(item.allocatedPackageCount) }} 箱</span></div>
|
||||
<div class="allocation-location">{{ value(item.locationCode) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<p v-else class="muted-empty">暂无托盘库位分配记录</p>
|
||||
</section>
|
||||
<section class="history-section">
|
||||
<div class="section-heading"><h3>文件记录</h3><span>{{ files.length }} 个文件</span></div>
|
||||
<div v-if="files.length" class="simple-list"><div v-for="file in files" :key="`${file.fileId}-${file.fileVersion}`"><strong>{{ value(file.fileNameSnapshot) }}</strong><span>{{ value(file.fileType) }} · {{ date(file.uploadedAt) }}</span></div></div>
|
||||
<p v-else class="muted-empty">暂无文件记录</p>
|
||||
</section>
|
||||
<section class="history-section">
|
||||
<div class="section-heading"><h3>内部备注</h3><span>{{ internalNotes.length }} 条</span></div>
|
||||
<div v-if="internalNotes.length" class="simple-list"><div v-for="note in internalNotes" :key="String(note.id)"><strong>{{ value(note.createdByName || note.createdBy) }}</strong><span>{{ value(note.content) }} · {{ date(note.createdAt) }}</span></div></div>
|
||||
<p v-else class="muted-empty">暂无内部备注</p>
|
||||
</section>
|
||||
<section class="history-section">
|
||||
<div class="section-heading"><h3>状态历史</h3><span>{{ statusHistory.length }} 条</span></div>
|
||||
<ol v-if="statusHistory.length" class="timeline"><li v-for="(history, index) in statusHistory" :key="`${history.occurredAt}-${index}`"><span class="timeline-dot" /><div><strong>{{ value(history.action) }}</strong><span>{{ value(history.fromStatus) }} → {{ value(history.toStatus) }} · {{ value(history.operatorName || history.operatorId) }}</span><small>{{ date(history.occurredAt) }}<template v-if="history.reason"> · {{ history.reason }}</template></small></div></li></ol>
|
||||
<p v-else class="muted-empty">暂无状态历史</p>
|
||||
</section>
|
||||
<section v-if="publicRemark" class="history-section remark-section"><div class="section-heading"><h3>公开备注</h3></div><p>{{ publicRemark }}</p></section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.history-panel { display: grid; gap: 22px; min-width: 0; }
|
||||
.history-section { min-width: 0; }
|
||||
.section-heading { display: flex; align-items: baseline; justify-content: space-between; gap: 12px; margin-bottom: 10px; }
|
||||
.section-heading h3 { margin: 0; color: var(--cc-text); font-size: 14px; }
|
||||
.section-heading span { color: var(--cc-muted); font-size: 12px; }
|
||||
.allocation-list, .simple-list { overflow: hidden; border: 1px solid var(--cc-border); border-radius: 8px; }
|
||||
.allocation-row { display: grid; grid-template-columns: 1fr auto minmax(100px, 0.8fr); gap: 12px; align-items: center; padding: 11px 12px; border-bottom: 1px solid var(--cc-border); font-size: 12px; }
|
||||
.allocation-row:last-child, .simple-list > div:last-child { border-bottom: 0; }
|
||||
.allocation-row > div, .simple-list > div { min-width: 0; }
|
||||
.allocation-row span, .simple-list span { display: block; margin-top: 3px; color: var(--cc-muted); }
|
||||
.allocation-location { overflow-wrap: anywhere; color: var(--cc-primary); text-align: right; }
|
||||
.simple-list > div { padding: 11px 12px; border-bottom: 1px solid var(--cc-border); font-size: 12px; }
|
||||
.simple-list strong, .simple-list span { overflow-wrap: anywhere; }
|
||||
.muted-empty { margin: 0; padding: 18px 12px; border: 1px dashed var(--cc-border); border-radius: 8px; color: var(--cc-muted); font-size: 12px; text-align: center; }
|
||||
.timeline { display: grid; gap: 0; margin: 0; padding: 0; list-style: none; }
|
||||
.timeline li { display: grid; grid-template-columns: 16px 1fr; gap: 10px; min-width: 0; padding-bottom: 16px; }
|
||||
.timeline-dot { width: 8px; height: 8px; margin-top: 5px; border: 2px solid var(--cc-primary); border-radius: 50%; background: #fff; box-shadow: 0 0 0 3px color-mix(in srgb, var(--cc-primary) 12%, transparent); }
|
||||
.timeline li > div { min-width: 0; padding-bottom: 12px; border-bottom: 1px solid var(--cc-border); }
|
||||
.timeline strong, .timeline span, .timeline small { display: block; overflow-wrap: anywhere; }
|
||||
.timeline span, .timeline small { margin-top: 3px; color: var(--cc-muted); font-size: 12px; line-height: 1.5; }
|
||||
.remark-section p { margin: 0; padding: 12px; border-radius: 8px; background: #f8fafc; color: var(--cc-text); font-size: 13px; line-height: 1.6; white-space: pre-wrap; }
|
||||
@media (max-width: 480px) { .allocation-row { grid-template-columns: 1fr auto; } .allocation-location { grid-column: 1 / -1; text-align: left; } }
|
||||
</style>
|
||||
@@ -0,0 +1,23 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
const source = readFileSync(resolve(process.cwd(), 'src/modules/inbounds/core/InboundSummaryPanel.vue'), 'utf8')
|
||||
|
||||
describe('InboundSummaryPanel', () => {
|
||||
it('organizes the aggregate detail into the prototype-aligned backend-backed sections', () => {
|
||||
expect(source).toContain('基本信息')
|
||||
expect(source).toContain('仓库与运输')
|
||||
expect(source).toContain('测量与计费')
|
||||
expect(source).toContain('入库资源')
|
||||
expect(source).toContain('summary.deliveryWarehouseName')
|
||||
expect(source).toContain('summary.chargeableWeightKg')
|
||||
expect(source).toContain('summary.locationCodes')
|
||||
expect(source).toContain('summary.notificationStatus')
|
||||
})
|
||||
|
||||
it('does not restore excluded receiving review or customer fee confirmation actions', () => {
|
||||
expect(source).not.toContain('接收审核')
|
||||
expect(source).not.toContain('客户确认费用')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,124 @@
|
||||
<script setup lang="ts">
|
||||
import { Box, MapPin, Scale, Warehouse } from '@lucide/vue'
|
||||
import type { InboundOrderSummary } from './types'
|
||||
|
||||
defineProps<{ summary: InboundOrderSummary }>()
|
||||
|
||||
const processLabels: Record<string, string> = {
|
||||
WAIT_MEASURE: '待测量', MEASURING: '测量中', MEASURE_PENDING: '待审核', MEASURE_REJECTED: '测量驳回',
|
||||
WAIT_PRICING: '待计费', FEE_CONFIRMING: '待发送费用', INBOUNDING: '入库处理中', INBOUNDED: '已入库', VOIDED: '已作废',
|
||||
}
|
||||
|
||||
function processLabel(status: string) { return processLabels[status] || status || '未知状态' }
|
||||
function processType(status: string) {
|
||||
if (status === 'INBOUNDED') return 'success'
|
||||
if (status === 'MEASURE_REJECTED' || status === 'VOIDED') return 'danger'
|
||||
if (status === 'MEASURING' || status === 'INBOUNDING') return 'info'
|
||||
return 'warning'
|
||||
}
|
||||
function value(value: string | number | null | undefined) { return value === null || value === undefined || value === '' ? '—' : String(value) }
|
||||
function number(value: number | null, digits = 3) { return value === null ? '—' : value.toFixed(digits) }
|
||||
function date(value: string | null) {
|
||||
if (!value) return '—'
|
||||
const parsed = new Date(value)
|
||||
return Number.isNaN(parsed.getTime()) ? value : parsed.toLocaleString('zh-CN', { hour12: false })
|
||||
}
|
||||
function yesNo(value: boolean | null) { return value === null ? '—' : value ? '是' : '否' }
|
||||
const measureReviewLabels: Record<string, string> = { PENDING: '待审核', APPROVED: '审核通过', REJECTED: '审核驳回' }
|
||||
const feeLabels: Record<string, string> = { UNPRICED: '未计费', CALCULATED: '已试算', CONFIRMED: '已确认', SENT: '已发送', VOIDED: '已作废' }
|
||||
const notificationLabels: Record<string, string> = { NOT_CREATED: '未创建', PENDING: '待发送', SUCCESS: '发送成功', FAILED: '发送失败' }
|
||||
function label(value: string | null, labels: Record<string, string>) { return value ? labels[value] || value : '—' }
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="summary-panel" aria-label="入库单概览">
|
||||
<div class="summary-heading">
|
||||
<div>
|
||||
<h3>{{ summary.inboundCode || `入库单 ${summary.id}` }}</h3>
|
||||
<p class="summary-subtitle">订单 {{ value(summary.orderNo) }} · {{ value(summary.customerName) }}</p>
|
||||
</div>
|
||||
<el-tag :type="processType(summary.processStatus)" effect="light">{{ processLabel(summary.processStatus) }}</el-tag>
|
||||
</div>
|
||||
<p v-if="summary.holdFlag" class="hold-note" role="status">当前挂起:{{ summary.currentHoldReason || '待处理异常' }}</p>
|
||||
<div class="summary-sections">
|
||||
<section aria-labelledby="inbound-basic-heading">
|
||||
<h4 id="inbound-basic-heading">基本信息</h4>
|
||||
<dl class="summary-grid">
|
||||
<div><dt>头程订单号</dt><dd>{{ value(summary.orderNo) }}</dd></div>
|
||||
<div><dt>订单状态</dt><dd>{{ value(summary.orderStatusSnapshot) }}</dd></div>
|
||||
<div><dt>客户</dt><dd>{{ value(summary.customerName) }}</dd></div>
|
||||
<div><dt>公司</dt><dd>{{ value(summary.companyName) }}</dd></div>
|
||||
<div><dt>业务类型</dt><dd>{{ value(summary.businessType) }}</dd></div>
|
||||
<div><dt>渠道</dt><dd>{{ value(summary.channelName || summary.channelCode) }}</dd></div>
|
||||
<div><dt>计税模式</dt><dd>{{ value(summary.taxMode) }}</dd></div>
|
||||
<div><dt>装载模式</dt><dd>{{ value(summary.containerMode) }}</dd></div>
|
||||
<div><dt>商检验</dt><dd>{{ yesNo(summary.customsInspection) }}</dd></div>
|
||||
<div><dt>创建时间</dt><dd>{{ date(summary.createdAt) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="inbound-route-heading">
|
||||
<h4 id="inbound-route-heading">仓库与运输</h4>
|
||||
<dl class="summary-grid">
|
||||
<div><dt><Warehouse :size="14" />收货仓</dt><dd>{{ value(summary.warehouseName) }}</dd></div>
|
||||
<div><dt>交货仓</dt><dd>{{ value(summary.deliveryWarehouseName) }}</dd></div>
|
||||
<div><dt><MapPin :size="14" />目的仓</dt><dd>{{ value(summary.destinationName || summary.destinationCode) }}</dd></div>
|
||||
<div><dt>目的仓类型</dt><dd>{{ value(summary.destinationType) }}</dd></div>
|
||||
<div><dt>集装箱号</dt><dd>{{ value(summary.containerNo) }}</dd></div>
|
||||
<div><dt>柜号</dt><dd>{{ value(summary.cabinetNo) }}</dd></div>
|
||||
<div><dt>船名 / 航次</dt><dd>{{ value(summary.vesselName) }} / {{ value(summary.voyageNo) }}</dd></div>
|
||||
<div><dt>配载状态</dt><dd>{{ value(summary.loadingStatus) }}</dd></div>
|
||||
<div><dt>预计到达</dt><dd>{{ date(summary.expectedArrivalAt) }}</dd></div>
|
||||
<div><dt>开船 / 到港</dt><dd>{{ date(summary.sailingAt) }} / {{ date(summary.portArrivalAt) }}</dd></div>
|
||||
<div><dt>快递公司</dt><dd>{{ value(summary.expressCompanyName || summary.expressCompanyCode) }}</dd></div>
|
||||
<div><dt>快递单号</dt><dd>{{ value(summary.expressTrackingNo) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="inbound-measure-heading">
|
||||
<h4 id="inbound-measure-heading">测量与计费</h4>
|
||||
<dl class="summary-grid">
|
||||
<div><dt><Box :size="14" />总箱数</dt><dd>{{ value(summary.totalPackageCount) }} 箱</dd></div>
|
||||
<div><dt><Scale :size="14" />总毛重</dt><dd>{{ number(summary.actualGrossWeightKg) }} kg</dd></div>
|
||||
<div><dt>总净重</dt><dd>{{ number(summary.actualNetWeightKg) }} kg</dd></div>
|
||||
<div><dt>总体积</dt><dd>{{ number(summary.actualVolumeCbm) }} CBM</dd></div>
|
||||
<div><dt>总材积重</dt><dd>{{ number(summary.volumeWeightKg) }} kg</dd></div>
|
||||
<div><dt>计费重量</dt><dd>{{ number(summary.chargeableWeightKg) }} kg</dd></div>
|
||||
<div><dt>计费体积</dt><dd>{{ number(summary.chargeableVolumeCbm) }} CBM</dd></div>
|
||||
<div><dt>测量审核</dt><dd>{{ label(summary.measureReviewStatus, measureReviewLabels) }}</dd></div>
|
||||
<div><dt>费用状态</dt><dd>{{ label(summary.feeStatus, feeLabels) }}</dd></div>
|
||||
<div><dt>费用审核状态</dt><dd>{{ value(summary.feeReviewStatus) }}</dd></div>
|
||||
<div><dt>测量完成时间</dt><dd>{{ date(summary.measureCompletedAt) }}</dd></div>
|
||||
<div><dt>通知状态</dt><dd>{{ label(summary.notificationStatus, notificationLabels) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
||||
<section aria-labelledby="inbound-storage-heading">
|
||||
<h4 id="inbound-storage-heading">入库资源</h4>
|
||||
<dl class="summary-grid">
|
||||
<div><dt>总托盘数</dt><dd>{{ value(summary.palletCount) }}</dd></div>
|
||||
<div><dt>库位位置</dt><dd>{{ value(summary.locationCodes) }}</dd></div>
|
||||
<div><dt>入库完成时间</dt><dd>{{ date(summary.inboundedAt) }}</dd></div>
|
||||
<div><dt>最后更新时间</dt><dd>{{ date(summary.updatedAt) }}</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.summary-panel { min-width: 0; }
|
||||
.summary-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; padding-bottom: 18px; border-bottom: 1px solid var(--cc-border); }
|
||||
h3 { margin: 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 18px; line-height: 1.3; }
|
||||
.summary-subtitle { margin: 6px 0 0; color: var(--cc-muted); font-size: 12px; }
|
||||
.summary-sections { display: grid; min-width: 0; gap: 22px; margin-top: 18px; }
|
||||
.summary-sections section { min-width: 0; }
|
||||
h4 { margin: 0 0 9px; color: var(--cc-text); font-size: 14px; line-height: 1.4; }
|
||||
.summary-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); margin: 0; border-top: 1px solid var(--cc-border); border-left: 1px solid var(--cc-border); }
|
||||
.summary-grid > div { min-width: 0; min-height: 66px; padding: 11px 12px; border-right: 1px solid var(--cc-border); border-bottom: 1px solid var(--cc-border); }
|
||||
dt { display: flex; align-items: center; gap: 6px; color: var(--cc-muted); font-size: 12px; }
|
||||
dd { margin: 5px 0 0; overflow-wrap: anywhere; color: var(--cc-text); font-size: 13px; line-height: 1.5; }
|
||||
.hold-note { margin: 16px 0 0; padding: 10px 12px; border: 1px solid #f5c26b; border-radius: 8px; background: #fffaeb; color: #93370d; font-size: 12px; line-height: 1.5; }
|
||||
@media (max-width: 900px) { .summary-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (max-width: 480px) { .summary-heading { align-items: stretch; flex-direction: column; } .summary-grid { grid-template-columns: 1fr; } }
|
||||
</style>
|
||||
@@ -0,0 +1,86 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { http } from '@/shared/api/http'
|
||||
import { getPlatformInbound, listPlatformInbounds } from './api'
|
||||
|
||||
vi.mock('@/shared/api/http', () => ({
|
||||
http: { get: vi.fn() },
|
||||
unwrapApiResponse: <T>(response: { data: T }) => response.data,
|
||||
}))
|
||||
|
||||
describe('platform inbound query API', () => {
|
||||
beforeEach(() => vi.mocked(http.get).mockReset())
|
||||
|
||||
it('omits the backend tenant filter for an all-customer list query', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 },
|
||||
})
|
||||
|
||||
await listPlatformInbounds(undefined, { page: 1, size: 20 })
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', { params: { page: 1, size: 20 } })
|
||||
})
|
||||
|
||||
it('uses the implemented non-v1 list path and normalizes page values and IDs', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: {
|
||||
records: [{ id: 19001, orderId: 18001, customerId: 10001, totalPackageCount: '6', holdFlag: 'false' }],
|
||||
page: '1', size: '20', total: '1', pages: '1',
|
||||
},
|
||||
})
|
||||
|
||||
await expect(listPlatformInbounds('10001', {
|
||||
page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false,
|
||||
})).resolves.toMatchObject({
|
||||
records: [{ id: '19001', orderId: '18001', customerId: '10001', totalPackageCount: 6, holdFlag: false }],
|
||||
total: 1, pages: 1,
|
||||
})
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', {
|
||||
params: { tenantId: '10001', page: 1, size: 20, tab: 'WAIT_MEASURE', inboundCode: 'RK', holdFlag: false },
|
||||
})
|
||||
})
|
||||
|
||||
it('loads a tenant-scoped aggregate detail and keeps nested IDs as strings', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: {
|
||||
summary: { id: 19001, orderId: 18001 }, publicRemark: null,
|
||||
palletLocations: [{ palletId: 21001, locationId: 22001 }], files: [{ fileId: 23001, uploadedBy: 2 }],
|
||||
internalNotes: [{ id: 24001, createdBy: 2 }], statusHistory: [{ operatorId: 2 }],
|
||||
},
|
||||
})
|
||||
|
||||
await expect(getPlatformInbound('10001', '19001')).resolves.toMatchObject({
|
||||
summary: { id: '19001', orderId: '18001' },
|
||||
palletLocations: [{ palletId: '21001', locationId: '22001' }],
|
||||
files: [{ fileId: '23001', uploadedBy: '2' }],
|
||||
internalNotes: [{ id: '24001', createdBy: '2' }],
|
||||
statusHistory: [{ operatorId: '2' }],
|
||||
})
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds/19001', { params: { tenantId: '10001' } })
|
||||
})
|
||||
|
||||
it('passes the implemented order, process, measurement and fee review filters without renaming them', async () => {
|
||||
vi.mocked(http.get).mockResolvedValue({
|
||||
code: '200', message: 'success', data: { records: [], page: 1, size: 20, total: 0, pages: 0 },
|
||||
})
|
||||
|
||||
await listPlatformInbounds('10001', {
|
||||
page: 1,
|
||||
size: 20,
|
||||
processStatus: 'MEASURE_PENDING',
|
||||
orderStatus: 'APPROVED',
|
||||
measureReviewStatus: 'PENDING',
|
||||
feeStatus: 'UNPRICED',
|
||||
feeReviewStatus: 'PENDING',
|
||||
warehouseId: '9007199254740993',
|
||||
deliveryWarehouseId: '9007199254740995',
|
||||
})
|
||||
|
||||
expect(http.get).toHaveBeenCalledWith('/platform/inbounds', {
|
||||
params: {
|
||||
tenantId: '10001', page: 1, size: 20, processStatus: 'MEASURE_PENDING',
|
||||
orderStatus: 'APPROVED', measureReviewStatus: 'PENDING', feeStatus: 'UNPRICED', feeReviewStatus: 'PENDING',
|
||||
warehouseId: '9007199254740993', deliveryWarehouseId: '9007199254740995',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,127 @@
|
||||
import { http, unwrapApiResponse, type ApiEnvelope } from '@/shared/api/http'
|
||||
import type {
|
||||
InboundFileSummary,
|
||||
InboundInternalNote,
|
||||
InboundListQuery,
|
||||
InboundOrderDetail,
|
||||
InboundOrderSummary,
|
||||
InboundPageResult,
|
||||
InboundPalletLocation,
|
||||
InboundStatusHistory,
|
||||
} from './types'
|
||||
|
||||
const basePath = '/platform/inbounds'
|
||||
|
||||
function id(value: unknown): string | null {
|
||||
return value === null || value === undefined ? null : String(value)
|
||||
}
|
||||
|
||||
function numberValue(value: unknown): number | null {
|
||||
if (value === null || value === undefined || value === '') return null
|
||||
const parsed = typeof value === 'number' ? value : Number(value)
|
||||
return Number.isFinite(parsed) ? parsed : null
|
||||
}
|
||||
|
||||
function integerValue(value: unknown): number | null {
|
||||
const parsed = numberValue(value)
|
||||
return parsed === null ? null : Math.trunc(parsed)
|
||||
}
|
||||
|
||||
function boolValue(value: unknown): boolean | null {
|
||||
if (value === null || value === undefined) return null
|
||||
if (typeof value === 'boolean') return value
|
||||
if (value === 'true' || value === 1 || value === '1') return true
|
||||
if (value === 'false' || value === 0 || value === '0') return false
|
||||
return null
|
||||
}
|
||||
|
||||
export function normalizeInboundSummary(raw: InboundOrderSummary): InboundOrderSummary {
|
||||
return {
|
||||
...raw,
|
||||
id: String(raw.id),
|
||||
orderId: id(raw.orderId),
|
||||
customerId: id(raw.customerId),
|
||||
companyId: id(raw.companyId),
|
||||
warehouseId: id(raw.warehouseId),
|
||||
deliveryWarehouseId: id(raw.deliveryWarehouseId),
|
||||
destinationWarehouseId: id(raw.destinationWarehouseId),
|
||||
version: integerValue(raw.version),
|
||||
customsInspection: boolValue(raw.customsInspection),
|
||||
holdFlag: boolValue(raw.holdFlag),
|
||||
totalPackageCount: integerValue(raw.totalPackageCount),
|
||||
actualGrossWeightKg: numberValue(raw.actualGrossWeightKg),
|
||||
actualNetWeightKg: numberValue(raw.actualNetWeightKg),
|
||||
actualVolumeCbm: numberValue(raw.actualVolumeCbm),
|
||||
volumeWeightKg: numberValue(raw.volumeWeightKg),
|
||||
chargeableWeightKg: numberValue(raw.chargeableWeightKg),
|
||||
chargeableVolumeCbm: numberValue(raw.chargeableVolumeCbm),
|
||||
palletCount: integerValue(raw.palletCount),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePage(raw: InboundPageResult): InboundPageResult {
|
||||
return {
|
||||
records: (raw.records || []).map(normalizeInboundSummary),
|
||||
page: integerValue(raw.page) || 1,
|
||||
size: integerValue(raw.size) || 20,
|
||||
total: integerValue(raw.total) || 0,
|
||||
pages: integerValue(raw.pages) || 0,
|
||||
}
|
||||
}
|
||||
|
||||
function normalizePallet(raw: InboundPalletLocation): InboundPalletLocation {
|
||||
return {
|
||||
...raw,
|
||||
allocationNo: integerValue(raw.allocationNo),
|
||||
warehouseId: id(raw.warehouseId),
|
||||
palletId: id(raw.palletId),
|
||||
locationId: id(raw.locationId),
|
||||
palletCapacityCbm: numberValue(raw.palletCapacityCbm),
|
||||
allocatedVolumeCbm: numberValue(raw.allocatedVolumeCbm),
|
||||
allocatedPackageCount: integerValue(raw.allocatedPackageCount),
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeFile(raw: InboundFileSummary): InboundFileSummary {
|
||||
return { ...raw, fileId: id(raw.fileId), uploadedBy: id(raw.uploadedBy), fileVersion: integerValue(raw.fileVersion) }
|
||||
}
|
||||
|
||||
function normalizeNote(raw: InboundInternalNote): InboundInternalNote {
|
||||
return { ...raw, id: id(raw.id), createdBy: id(raw.createdBy) }
|
||||
}
|
||||
|
||||
function normalizeHistory(raw: InboundStatusHistory): InboundStatusHistory {
|
||||
return { ...raw, operatorId: id(raw.operatorId) }
|
||||
}
|
||||
|
||||
function normalizeDetail(raw: InboundOrderDetail): InboundOrderDetail {
|
||||
return {
|
||||
...raw,
|
||||
summary: normalizeInboundSummary(raw.summary),
|
||||
palletLocations: (raw.palletLocations || []).map(normalizePallet),
|
||||
files: (raw.files || []).map(normalizeFile),
|
||||
internalNotes: (raw.internalNotes || []).map(normalizeNote),
|
||||
statusHistory: (raw.statusHistory || []).map(normalizeHistory),
|
||||
}
|
||||
}
|
||||
|
||||
function queryParams(tenantId: string | undefined, query: InboundListQuery) {
|
||||
return Object.fromEntries(
|
||||
Object.entries({ tenantId, ...query }).filter(([, value]) => value !== undefined && value !== ''),
|
||||
)
|
||||
}
|
||||
|
||||
export async function listPlatformInbounds(tenantId: string | undefined, query: InboundListQuery) {
|
||||
const response = await http.get<ApiEnvelope<InboundPageResult>, ApiEnvelope<InboundPageResult>>(basePath, {
|
||||
params: queryParams(tenantId, query),
|
||||
})
|
||||
return normalizePage(unwrapApiResponse(response))
|
||||
}
|
||||
|
||||
export async function getPlatformInbound(tenantId: string, inboundId: string) {
|
||||
const response = await http.get<ApiEnvelope<InboundOrderDetail>, ApiEnvelope<InboundOrderDetail>>(
|
||||
`${basePath}/${inboundId}`,
|
||||
{ params: { tenantId } },
|
||||
)
|
||||
return normalizeDetail(unwrapApiResponse(response))
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
export type InboundListTab = 'WAIT_MEASURE' | 'WAIT_REVIEW' | 'WAIT_FEE_SEND' | 'INBOUNDED'
|
||||
|
||||
export type InboundProcessStatus =
|
||||
| 'WAIT_MEASURE'
|
||||
| 'MEASURING'
|
||||
| 'MEASURE_PENDING'
|
||||
| 'MEASURE_REJECTED'
|
||||
| 'WAIT_PRICING'
|
||||
| 'FEE_CONFIRMING'
|
||||
| 'INBOUNDING'
|
||||
| 'INBOUNDED'
|
||||
| 'VOIDED'
|
||||
|
||||
export type InboundFeeStatus = 'UNPRICED' | 'CALCULATED' | 'CONFIRMED' | 'SENT' | 'VOIDED' | string
|
||||
|
||||
export interface InboundOrderSummary {
|
||||
id: string
|
||||
orderId: string | null
|
||||
orderNo: string | null
|
||||
inboundCode: string | null
|
||||
processStatus: InboundProcessStatus | string
|
||||
orderStatusSnapshot: string | null
|
||||
measureReviewStatus: string | null
|
||||
feeStatus: InboundFeeStatus | null
|
||||
feeReviewStatus: string | null
|
||||
notificationStatus: string | null
|
||||
version: number | null
|
||||
expectedArrivalAt: string | null
|
||||
customerId: string | null
|
||||
customerName: string | null
|
||||
companyId: string | null
|
||||
companyName: string | null
|
||||
warehouseId: string | null
|
||||
warehouseName: string | null
|
||||
deliveryWarehouseId: string | null
|
||||
deliveryWarehouseName: string | null
|
||||
businessType: string | null
|
||||
channelCode: string | null
|
||||
channelName: string | null
|
||||
taxMode: string | null
|
||||
containerMode: string | null
|
||||
customsInspection: boolean | null
|
||||
destinationType: string | null
|
||||
destinationWarehouseId: string | null
|
||||
destinationCode: string | null
|
||||
destinationName: string | null
|
||||
holdFlag: boolean | null
|
||||
currentHoldReason: string | null
|
||||
totalPackageCount: number | null
|
||||
actualGrossWeightKg: number | null
|
||||
actualNetWeightKg: number | null
|
||||
actualVolumeCbm: number | null
|
||||
volumeWeightKg: number | null
|
||||
chargeableWeightKg: number | null
|
||||
chargeableVolumeCbm: number | null
|
||||
palletCount: number | null
|
||||
locationCodes: string | null
|
||||
measureCompletedAt: string | null
|
||||
inboundedAt: string | null
|
||||
expressCompanyCode: string | null
|
||||
expressCompanyName: string | null
|
||||
expressTrackingNo: string | null
|
||||
loadingStatus: string | null
|
||||
containerNo: string | null
|
||||
cabinetNo: string | null
|
||||
vesselName: string | null
|
||||
voyageNo: string | null
|
||||
sailingAt: string | null
|
||||
portArrivalAt: string | null
|
||||
createdAt: string | null
|
||||
updatedAt: string | null
|
||||
}
|
||||
|
||||
export interface InboundPalletLocation {
|
||||
allocationNo: number | null
|
||||
warehouseId: string | null
|
||||
palletId: string | null
|
||||
palletCode: string | null
|
||||
palletCapacityCbm: number | null
|
||||
allocatedVolumeCbm: number | null
|
||||
allocatedPackageCount: number | null
|
||||
locationId: string | null
|
||||
locationCode: string | null
|
||||
reservationNo: string | null
|
||||
allocationStatus: string | null
|
||||
}
|
||||
|
||||
export interface InboundFileSummary {
|
||||
fileId: string | null
|
||||
fileType: string | null
|
||||
fileNameSnapshot: string | null
|
||||
visibility: string | null
|
||||
fileVersion: number | null
|
||||
uploadedBy: string | null
|
||||
uploadedAt: string | null
|
||||
}
|
||||
|
||||
export interface InboundInternalNote {
|
||||
id: string | null
|
||||
content: string | null
|
||||
createdBy: string | null
|
||||
createdByName: string | null
|
||||
createdAt: string | null
|
||||
}
|
||||
|
||||
export interface InboundStatusHistory {
|
||||
fromStatus: string | null
|
||||
toStatus: string | null
|
||||
action: string | null
|
||||
reason: string | null
|
||||
operatorId: string | null
|
||||
operatorName: string | null
|
||||
occurredAt: string | null
|
||||
}
|
||||
|
||||
export interface InboundOrderDetail {
|
||||
summary: InboundOrderSummary
|
||||
publicRemark: string | null
|
||||
palletLocations: InboundPalletLocation[]
|
||||
files: InboundFileSummary[]
|
||||
internalNotes: InboundInternalNote[]
|
||||
statusHistory: InboundStatusHistory[]
|
||||
}
|
||||
|
||||
export interface InboundPageResult {
|
||||
records: InboundOrderSummary[]
|
||||
page: number
|
||||
size: number
|
||||
total: number
|
||||
pages: number
|
||||
}
|
||||
|
||||
export interface InboundListQuery {
|
||||
page: number
|
||||
size: number
|
||||
tab?: InboundListTab
|
||||
inboundCode?: string
|
||||
orderNo?: string
|
||||
processStatus?: string
|
||||
orderStatus?: string
|
||||
measureReviewStatus?: string
|
||||
customerName?: string
|
||||
destinationCode?: string
|
||||
feeStatus?: string
|
||||
feeReviewStatus?: string
|
||||
customerId?: string
|
||||
holdFlag?: boolean
|
||||
companyId?: string
|
||||
warehouseId?: string
|
||||
deliveryWarehouseId?: string
|
||||
expectedArrivalFrom?: string
|
||||
expectedArrivalTo?: string
|
||||
}
|
||||
Reference in New Issue
Block a user