按客户与证用品类自动选证,未配置生效时间拦住安排;齐备仅放行安排,归档前须完善挂证,并补充迁移与测试造数脚本。 Co-authored-by: Cursor <cursoragent@cursor.com>
625 lines
18 KiB
TypeScript
625 lines
18 KiB
TypeScript
import request, { pickBxData } from '@/utils/http/bx'
|
|
|
|
export type CertType = 'purchase' | 'transport'
|
|
export interface PurchaseReference {
|
|
id?: string
|
|
referenceType: 'purchase_permit' | 'purchase_filing' | 'export_permit' | 'unknown'
|
|
referenceNo: string
|
|
purchaseCertId?: string | null
|
|
remark?: string | null
|
|
orderNum?: number | null
|
|
}
|
|
export interface CertBody {
|
|
certNo: string
|
|
issueDate?: string | null
|
|
productNameOnCert: string
|
|
totalQty: string | number
|
|
quantityUnit: string
|
|
validFrom?: string | null
|
|
validTo?: string | null
|
|
validUseText: string
|
|
entryMode: 'manual' | 'image_parse'
|
|
attachmentUrls?: string | null
|
|
status?: 'draft' | 'suspended' | 'closed' | 'void'
|
|
remark?: string | null
|
|
orderNum?: number | null
|
|
buyerName?: string | null
|
|
buyerAddress?: string | null
|
|
buyerLegalRepresentative?: string | null
|
|
buyerPhone?: string | null
|
|
sellerName?: string | null
|
|
sellerAddress?: string | null
|
|
sellerLegalRepresentative?: string | null
|
|
sellerPhone?: string | null
|
|
purchasePurpose?: string | null
|
|
shipperName?: string | null
|
|
shipperAddress?: string | null
|
|
shipperLegalRepresentative?: string | null
|
|
shipperPhone?: string | null
|
|
packagingText?: string | null
|
|
carrierName?: string | null
|
|
carrierAddressPhone?: string | null
|
|
transportMode?: string | null
|
|
vehicleNumbersJson?: string[]
|
|
routeText?: string | null
|
|
consigneeName?: string | null
|
|
applicantName?: string | null
|
|
customerId?: string | null
|
|
categoryId?: string | null
|
|
contractBindings?: CertificateContractBinding[]
|
|
purchaseRefs?: PurchaseReference[]
|
|
}
|
|
export interface QuotaCert extends Omit<CertBody, 'status'> {
|
|
id: string
|
|
status: 'draft' | 'active' | 'suspended' | 'closed' | 'void'
|
|
bindingsEditable?: boolean
|
|
referencesEditable?: boolean
|
|
revision: number
|
|
reservedQty: string | number
|
|
usedQty: string | number
|
|
availableQty: string | number
|
|
verifiedTime?: string | null
|
|
openingConfirmed: boolean
|
|
usagePolicy: 'once' | 'multiple' | 'needs_confirmation'
|
|
createTime?: string
|
|
certType?: CertType
|
|
derivedCategoryIds?: string[]
|
|
}
|
|
export interface ScopeProduct {
|
|
id?: string
|
|
contractProductId: string
|
|
scopeStatus?: 'active' | 'inactive'
|
|
remark?: string | null
|
|
orderNum?: number | null
|
|
}
|
|
export interface CertBinding {
|
|
id?: string
|
|
certType: CertType
|
|
certId: string
|
|
linkStatus?: 'active' | 'inactive'
|
|
products: ScopeProduct[]
|
|
scopeRevision?: number
|
|
remark?: string | null
|
|
orderNum?: number | null
|
|
}
|
|
export interface ContractOption {
|
|
id: string
|
|
contractNo: string
|
|
contractName: string
|
|
contractType?: string
|
|
customerId?: string
|
|
customerName?: string
|
|
status?: string
|
|
signDate?: string | null
|
|
productSummary?: string
|
|
}
|
|
export interface ContractProduct {
|
|
id: string
|
|
productId: string
|
|
productName: string
|
|
productNo?: string
|
|
categoryName?: string
|
|
brandName?: string
|
|
model?: string
|
|
unit?: string
|
|
quantity?: string | number
|
|
packageSpec?: string
|
|
deliveryMethod?: string
|
|
}
|
|
export interface LegalDocument {
|
|
id: string
|
|
planNo: string
|
|
archiveStatus: string
|
|
transportStatus: string
|
|
products: {
|
|
id: string
|
|
productId: string
|
|
productName: string
|
|
model?: string
|
|
legalUseCertUrl: string | null
|
|
documentStatus: 'uploaded' | 'pending'
|
|
contexts?: {
|
|
stopType: string
|
|
stopProductId: string
|
|
partyName?: string
|
|
address?: string
|
|
contractId?: string
|
|
contractNo?: string
|
|
contractProductId?: string
|
|
}[]
|
|
}[]
|
|
}
|
|
const base = '/transport/quota-certs'
|
|
export const quotaCertApi = {
|
|
list: (kind: CertType, params: { keyword?: string; status?: string } = {}) =>
|
|
request.get(`${base}/${kind}`, { params }).then(pickBxData<QuotaCert[]>),
|
|
get: (kind: CertType, id: string) =>
|
|
request.get(`${base}/${kind}/${id}`).then(pickBxData<QuotaCert>),
|
|
save: (kind: CertType, body: CertBody, id?: string) =>
|
|
(id ? request.put(`${base}/${kind}/${id}`, body) : request.post(`${base}/${kind}`, body)).then(
|
|
pickBxData<QuotaCert>
|
|
),
|
|
remove: (kind: CertType, id: string) => request.delete(`${base}/${kind}/${id}`),
|
|
confirm: (
|
|
kind: CertType,
|
|
id: string,
|
|
body: {
|
|
operationId: string
|
|
usedQty: string
|
|
singleUseConsumed: boolean
|
|
reason: string
|
|
sourceNote?: string
|
|
evidenceUrls?: string
|
|
}
|
|
) => request.post(`${base}/${kind}/${id}/confirm`, body).then(pickBxData<QuotaCert>),
|
|
verify: (kind: CertType, id: string, body: { operationId: string; reason: string }) =>
|
|
request.post(`${base}/${kind}/${id}/verify`, body).then(pickBxData<QuotaCert>),
|
|
recognize: (kind: CertType, attachmentUrl: string) =>
|
|
request
|
|
.post(
|
|
`${base}/${kind}/recognize`,
|
|
{ attachmentUrl },
|
|
{
|
|
timeout: 90000,
|
|
skipErrorMessage: true
|
|
}
|
|
)
|
|
.then(
|
|
pickBxData<{
|
|
candidates: Partial<CertBody>
|
|
invalidFields: Record<string, string>
|
|
requiresConfirmation: boolean
|
|
}>
|
|
),
|
|
contracts: (keyword = '') =>
|
|
request.get(`${base}/contracts`, { params: { keyword } }).then(pickBxData<ContractOption[]>),
|
|
products: (id: string) =>
|
|
request.get(`${base}/contracts/${id}/products`).then(pickBxData<ContractProduct[]>),
|
|
bindings: (id: string) =>
|
|
request.get(`${base}/contracts/${id}/bindings`).then(pickBxData<CertBinding[]>),
|
|
saveBindings: (id: string, bindings: CertBinding[]) =>
|
|
request.put(`${base}/contracts/${id}/bindings`, { bindings }).then(pickBxData<CertBinding[]>),
|
|
certificateContracts: (kind: CertType, id: string) =>
|
|
request.get(`${base}/${kind}/${id}/contracts`).then(pickBxData<ContractOption[]>)
|
|
}
|
|
const legalBase = '/transport/legal-use-documents'
|
|
export interface StopLegalDocumentItem {
|
|
stopType: 'load' | 'delivery'
|
|
stopId: string
|
|
stopOrder: number
|
|
stopProductId: string
|
|
productId: string
|
|
productName: string
|
|
model?: string
|
|
unit?: string
|
|
contractId?: string | null
|
|
contractNo?: string | null
|
|
contractProductId?: string | null
|
|
partyName?: string | null
|
|
address?: string | null
|
|
legalUseCertUrl: string
|
|
documentStatus: 'pending' | 'uploaded'
|
|
uploadedBy?: string | null
|
|
uploadedTime?: string | null
|
|
}
|
|
export interface StopLegalDocument {
|
|
id: string
|
|
planNo: string
|
|
planDate?: string
|
|
licensePlate?: string
|
|
productNames?: string
|
|
supplierName?: string
|
|
customerName?: string
|
|
followStatus?: string
|
|
transportStatus: string
|
|
archiveStatus: string
|
|
requiredCount: number
|
|
uploadedCount: number
|
|
documentStatus: 'pending' | 'uploaded' | 'not_required'
|
|
legalUseCertUrl?: string | null
|
|
planProductIds?: string[]
|
|
items: StopLegalDocumentItem[]
|
|
legacyProducts: {
|
|
id: string
|
|
productId: string
|
|
productName: string
|
|
model?: string
|
|
legalUseCertUrl?: string | null
|
|
}[]
|
|
}
|
|
export const legalDocumentApi = {
|
|
listStops: (params: {
|
|
keyword?: string
|
|
planNo?: string
|
|
planDateFrom?: string
|
|
planDateTo?: string
|
|
licensePlate?: string
|
|
productNames?: string
|
|
supplierName?: string
|
|
customerName?: string
|
|
followStatusIn?: string
|
|
progress?: string
|
|
status?: 'pending' | 'uploaded'
|
|
contractId?: string
|
|
page?: number
|
|
pageSize?: number
|
|
}) =>
|
|
request
|
|
.get(`${legalBase}/stops`, { params })
|
|
.then(
|
|
pickBxData<{ list: StopLegalDocument[]; total: number; page: number; pageSize: number }>
|
|
),
|
|
getStops: (id: string, contractId?: string) =>
|
|
request
|
|
.get(`${legalBase}/stops/${id}`, { params: { contractId } })
|
|
.then(pickBxData<StopLegalDocument>),
|
|
saveStops: (
|
|
id: string,
|
|
items: Pick<StopLegalDocumentItem, 'stopType' | 'stopProductId' | 'legalUseCertUrl'>[],
|
|
contractId?: string
|
|
) =>
|
|
request
|
|
.patch(`${legalBase}/stops/${id}`, { items }, { params: { contractId } })
|
|
.then(pickBxData<StopLegalDocument>),
|
|
list: (params: { keyword?: string; status?: string }) =>
|
|
request.get(legalBase, { params }).then(pickBxData<LegalDocument[]>),
|
|
get: (id: string) => request.get(`${legalBase}/${id}`).then(pickBxData<LegalDocument>),
|
|
save: (id: string, products: { id: string; legalUseCertUrl?: string | null }[]) =>
|
|
request.patch(`${legalBase}/${id}`, { products }).then(pickBxData<LegalDocument>)
|
|
}
|
|
|
|
export type MeasurementVersion = 'per_stop_v2' | 'legacy_single_side'
|
|
export interface QuotaSourceOption {
|
|
sourceStopId?: string
|
|
stopOrder?: number
|
|
contractNo?: string | null
|
|
bindingsReadable?: boolean
|
|
certificates?: { certType: CertType; certId: string; certNo: string; quantityUnit: string }[]
|
|
isToxic?: boolean
|
|
isExplosive?: boolean
|
|
sourceStopType: 'load' | 'delivery'
|
|
sourceStopProductId: string
|
|
planProductId: string
|
|
productName: string
|
|
productNo?: string
|
|
model?: string
|
|
quantity: string | number
|
|
unit: string
|
|
contractId: string
|
|
contractProductId: string
|
|
partyName?: string
|
|
/** 本段勾选同市时只要求购买证 */
|
|
sameCity?: boolean
|
|
}
|
|
export interface QuotaSourcePair {
|
|
sourceStopType: 'load' | 'delivery'
|
|
sourceStopProductId: string
|
|
relatedStopProductIds: string[]
|
|
}
|
|
export interface ManualQuotaQuantity {
|
|
permitKind: CertType
|
|
sourceId: string
|
|
permitId: string
|
|
sourceQuantity: string | number
|
|
permitQuantity: string | number
|
|
note: string
|
|
}
|
|
export interface QuotaCondition {
|
|
certType: CertType
|
|
certId: string
|
|
certRevision: number
|
|
sourceFingerprint: string
|
|
partiesConfirmed: boolean
|
|
routeConfirmed: boolean
|
|
note: string
|
|
}
|
|
export interface QuotaConfiguration {
|
|
measurementVersion?: MeasurementVersion
|
|
sourceMapping?: QuotaSourcePair[] | null
|
|
purchaseCertIds: string[]
|
|
transportCertIds: string[]
|
|
manualAllocations: ManualQuotaQuantity[]
|
|
conditions: QuotaCondition[]
|
|
}
|
|
export interface PlanQuota {
|
|
measurementVersion?: MeasurementVersion
|
|
stopProducts?: QuotaSourceOption[]
|
|
planId: string
|
|
planNo: string
|
|
licensePlate?: string
|
|
canSaveConfiguration?: boolean
|
|
arrangeBlockMessage?: string
|
|
quotaCertWaived?: boolean
|
|
configuration: Partial<QuotaConfiguration>
|
|
sourceOptions: QuotaSourceOption[]
|
|
usages: Record<string, any>[]
|
|
}
|
|
export interface QuotaContext {
|
|
sourceFingerprint: string
|
|
sources: {
|
|
source_stop_type: string
|
|
source_stop_product_id: string
|
|
quantity: string
|
|
unit: string
|
|
snapshot: { stops: Record<string, any>[]; vehicle?: string; trailer?: string }
|
|
}[]
|
|
}
|
|
export interface QuotaPreview extends Partial<QuotaContext> {
|
|
status: string
|
|
reasons?: string[]
|
|
diagnostics?: {
|
|
certType: CertType
|
|
sourceIds: string[]
|
|
unit: string
|
|
demand: string
|
|
availableUpperBound: string | null
|
|
gapLowerBound: string | null
|
|
reason: string
|
|
}[]
|
|
purchaseAllocations?: Record<string, any>[]
|
|
transportAllocations?: Record<string, any>[]
|
|
}
|
|
export interface CertReadyPlan {
|
|
id: string
|
|
planNo: string
|
|
planDate?: string | null
|
|
licensePlate?: string | null
|
|
supplierName?: string | null
|
|
customerName?: string | null
|
|
productNames?: string
|
|
followStatus?: string | null
|
|
quotaCertWaived: boolean
|
|
quotaCertWaivedByName?: string | null
|
|
quotaCertWaivedTime?: string | null
|
|
}
|
|
|
|
export const certReadyApi = {
|
|
list: (params: {
|
|
keyword?: string
|
|
waived?: string
|
|
planNo?: string
|
|
planDateFrom?: string
|
|
planDateTo?: string
|
|
licensePlate?: string
|
|
productNames?: string
|
|
supplierName?: string
|
|
customerName?: string
|
|
followStatusIn?: string
|
|
quotaCertWaivedIn?: string
|
|
quotaCertWaivedByName?: string
|
|
quotaCertWaivedTimeFrom?: string
|
|
quotaCertWaivedTimeTo?: string
|
|
page: number
|
|
pageSize: number
|
|
}) =>
|
|
request
|
|
.get(`${base}/plans/cert-ready`, { params })
|
|
.then(pickBxData<{ total: number; list: CertReadyPlan[] }>),
|
|
mark: (id: string) =>
|
|
request.post(`${base}/plans/${id}/cert-ready`).then(pickBxData<CertReadyPlan>),
|
|
listPendingFulfill: (params: { keyword?: string; page: number; pageSize: number }) =>
|
|
request
|
|
.get(`${base}/plans/cert-ready/pending-fulfill`, { params })
|
|
.then(pickBxData<{ total: number; list: CertReadyPlan[] }>),
|
|
fulfill: (id: string) =>
|
|
request.post(`${base}/plans/${id}/fulfill-quota-certs`).then(pickBxData<CertReadyPlan>),
|
|
fulfillBatch: (planIds: string[]) =>
|
|
request
|
|
.post(`${base}/plans/fulfill-quota-certs/batch`, { planIds })
|
|
.then(pickBxData<{ results: Array<{ planId: string; ok: boolean; message?: string }> }>)
|
|
}
|
|
|
|
export const certCategoryApi = {
|
|
list: (params?: { keyword?: string; status?: string }) =>
|
|
request.get(`${base}/categories`, { params }).then(pickBxData<CertCategory[]>),
|
|
get: (id: string) => request.get(`${base}/categories/${id}`).then(pickBxData<CertCategory>),
|
|
create: (body: Record<string, unknown>) =>
|
|
request.post(`${base}/categories`, body).then(pickBxData<CertCategory>),
|
|
update: (id: string, body: Record<string, unknown>) =>
|
|
request.put(`${base}/categories/${id}`, body).then(pickBxData<CertCategory>),
|
|
remove: (id: string) =>
|
|
request.delete(`${base}/categories/${id}`).then(pickBxData<{ ok?: boolean }>)
|
|
}
|
|
|
|
export interface CertCategory {
|
|
id: string
|
|
categoryName: string
|
|
categoryCode?: string | null
|
|
status: string
|
|
products?: Array<{ id?: string; productId: string; scopeStatus?: string }>
|
|
}
|
|
|
|
export const certQuotaRuleApi = {
|
|
get: () =>
|
|
request
|
|
.get(`${base}/rule-config`)
|
|
.then(pickBxData<{ effectiveAt: string | null; configured: boolean }>),
|
|
save: (effectiveAt: string | null) =>
|
|
request
|
|
.put(`${base}/rule-config`, { effectiveAt })
|
|
.then(pickBxData<{ effectiveAt: string | null; configured: boolean }>)
|
|
}
|
|
|
|
export const planQuotaApi = {
|
|
get: (id: string, arrangeCheck = false) =>
|
|
request
|
|
.get(
|
|
`${base}/plans/${id}/usage`,
|
|
arrangeCheck ? { params: { arrangeCheck: true } } : undefined
|
|
)
|
|
.then(pickBxData<PlanQuota>),
|
|
context: (id: string, body: QuotaConfiguration & QuotaPreviewVehicle) =>
|
|
request.post(`${base}/plans/${id}/context`, body).then(pickBxData<QuotaContext>),
|
|
preview: (id: string, body: QuotaConfiguration & QuotaPreviewVehicle) =>
|
|
request.post(`${base}/plans/${id}/preview`, body).then(pickBxData<QuotaPreview>),
|
|
save: (id: string, body: QuotaConfiguration) =>
|
|
request.put(`${base}/plans/${id}/configuration`, body).then(pickBxData<QuotaConfiguration>)
|
|
}
|
|
|
|
export interface QuotaPreviewVehicle {
|
|
targetLicensePlate?: string
|
|
targetTrailerNo?: string | null
|
|
}
|
|
|
|
export interface CertTotalAdjustment {
|
|
operationId: string
|
|
totalQty: string
|
|
reason: string
|
|
sourceNote: string
|
|
evidenceUrls: string
|
|
}
|
|
export const quotaAdjustmentApi = {
|
|
logs: (kind: CertType, id: string) =>
|
|
request.get(`${base}/${kind}/${id}/logs`).then(pickBxData<Record<string, any>[]>),
|
|
total: (kind: CertType, id: string, body: CertTotalAdjustment) =>
|
|
request.post(`${base}/${kind}/${id}/total-adjust`, body)
|
|
}
|
|
|
|
export type QuotaQuantity = string | number
|
|
export interface CertificateContractBinding {
|
|
editLocked?: boolean
|
|
id?: string
|
|
contractId: string
|
|
linkStatus?: 'active' | 'inactive'
|
|
products: ScopeProduct[]
|
|
scopeRevision?: number
|
|
remark?: string | null
|
|
orderNum?: number | null
|
|
}
|
|
export interface StatisticsPage<T> {
|
|
list: T[]
|
|
total: number
|
|
page: number
|
|
pageSize: number
|
|
}
|
|
export interface ContractStatisticsSummary extends ContractOption {
|
|
certCount: number
|
|
}
|
|
export interface CertificateStatisticsSummary {
|
|
id: string
|
|
certId: string
|
|
certType: CertType
|
|
certNo: string
|
|
productNameOnCert: string
|
|
quantityUnit: string
|
|
totalQty: QuotaQuantity
|
|
reservedQty: QuotaQuantity
|
|
usedQty: QuotaQuantity
|
|
availableQty: QuotaQuantity
|
|
status: string
|
|
validFrom: string | null
|
|
validTo: string | null
|
|
salePartyName?: string | null
|
|
buyPartyName?: string | null
|
|
validUseText?: string | null
|
|
}
|
|
export interface StatisticsProduct {
|
|
id: string
|
|
contractProductId: string
|
|
productId: string
|
|
productName: string
|
|
model: string | null
|
|
unit: string | null
|
|
bindingId: string | null
|
|
scopeId: string | null
|
|
scopeStatus: string | null
|
|
linkStatus: string | null
|
|
reservedQty: QuotaQuantity
|
|
usedQty: QuotaQuantity
|
|
}
|
|
export interface StatisticsBinding {
|
|
bindingId: string | null
|
|
linkStatus: string | null
|
|
scopeRevision: number | null
|
|
referenced?: boolean
|
|
contractReservedQty: QuotaQuantity
|
|
contractUsedQty: QuotaQuantity
|
|
products: StatisticsProduct[]
|
|
}
|
|
export interface ContractStatistics extends ContractOption {
|
|
certificates: (CertificateStatisticsSummary & StatisticsBinding & { productNames: string[] })[]
|
|
}
|
|
export interface StatisticsUsageItem {
|
|
partyName?: string | null
|
|
address?: string | null
|
|
id: string
|
|
usageId: string
|
|
certType: CertType
|
|
certId: string
|
|
planId: string
|
|
planNo: string
|
|
stopType: string
|
|
stopId: string
|
|
stopProductId: string
|
|
planProductId: string
|
|
contractId: string
|
|
contractProductId: string
|
|
productName: string
|
|
quantityUnit: string
|
|
reservedQty: QuotaQuantity
|
|
usedQty: QuotaQuantity
|
|
itemStatus: string
|
|
}
|
|
export interface CertificateStatistics extends CertificateStatisticsSummary {
|
|
contracts: (ContractOption & StatisticsBinding)[]
|
|
usageItems: StatisticsUsageItem[]
|
|
openingUsedQty: QuotaQuantity
|
|
unattributedUsedQty: QuotaQuantity
|
|
}
|
|
export interface StatisticsQuery {
|
|
contractNo?: string
|
|
contractName?: string
|
|
contractTypeIn?: string
|
|
certCountOp?: string
|
|
certCount?: string
|
|
certCountMin?: string
|
|
certCountMax?: string
|
|
certNo?: string
|
|
keyword?: string
|
|
page?: number
|
|
pageSize?: number
|
|
certType?: CertType
|
|
certTypeIn?: string
|
|
productNameOnCert?: string
|
|
quantityUnitIn?: string
|
|
statusIn?: string
|
|
validFromFrom?: string
|
|
validFromTo?: string
|
|
validToFrom?: string
|
|
validToTo?: string
|
|
totalQtyOp?: string
|
|
totalQty?: string
|
|
totalQtyMin?: string
|
|
totalQtyMax?: string
|
|
reservedQtyOp?: string
|
|
reservedQty?: string
|
|
reservedQtyMin?: string
|
|
reservedQtyMax?: string
|
|
usedQtyOp?: string
|
|
usedQty?: string
|
|
usedQtyMin?: string
|
|
usedQtyMax?: string
|
|
availableQtyOp?: string
|
|
availableQty?: string
|
|
availableQtyMin?: string
|
|
availableQtyMax?: string
|
|
salePartyName?: string
|
|
buyPartyName?: string
|
|
validUseText?: string
|
|
expiryAlertIn?: string
|
|
}
|
|
export const quotaStatisticsApi = {
|
|
contracts: (params: StatisticsQuery) =>
|
|
request
|
|
.get(`${base}/statistics/contracts`, { params })
|
|
.then(pickBxData<StatisticsPage<ContractStatisticsSummary>>),
|
|
certificates: (params: StatisticsQuery) =>
|
|
request
|
|
.get(`${base}/statistics/certificates`, { params })
|
|
.then(pickBxData<StatisticsPage<CertificateStatisticsSummary>>),
|
|
contract: (id: string) =>
|
|
request.get(`${base}/contracts/${id}/statistics`).then(pickBxData<ContractStatistics>),
|
|
certificate: (kind: CertType, id: string) =>
|
|
request.get(`${base}/${kind}/${id}/statistics`).then(pickBxData<CertificateStatistics>)
|
|
}
|