update customer package
This commit is contained in:
@@ -84,30 +84,28 @@
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import PackageAPI from "@/api/PackageAPI";
|
||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { CustomerPackageItem, GaoCustomerPackage } from "@/types/Package";
|
||||
import type { OrderFulfillmentItemRequest } from "@/types/Fulfillment";
|
||||
import type { SalesOrder, SalesOrderItem } from "@/types/SalesOrder";
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import type { GaoOrderFulfillmentItemRequest } from "@/types/Fulfillment";
|
||||
import type { GaoSalesOrderItem, SalesOrder } from "@/types/SalesOrder";
|
||||
import type { PhotoItem } from "@/types/Photo";
|
||||
import type { Attachment } from "timi-web-api";
|
||||
import { createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
|
||||
import { comparePackageQuantity, createPackageRequestNo, getPackageQuantityText, normalizePackageQuantity } from "@/utils/package";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
import { uploadPhotoList } from "@/utils/photo";
|
||||
import { getProductPhotoURL } from "@/utils/product";
|
||||
|
||||
type SourceItem = SalesOrderItem | CustomerPackageItem;
|
||||
defineOptions({ name: "OrderFulfillmentCreate" });
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const navBarStore = useNavBarStore();
|
||||
const userStore = useUserStore();
|
||||
const order = ref<SalesOrder>();
|
||||
const customerPackage = ref<GaoCustomerPackage>();
|
||||
const quantityMap = reactive<Record<string, string>>({});
|
||||
const deliveredAtText = ref("");
|
||||
const draftTime = ref("");
|
||||
@@ -117,12 +115,14 @@ const requestNo = ref("");
|
||||
const isSaving = ref(false);
|
||||
const photoItems = ref<PhotoItem[]>([]);
|
||||
const photoUrlList = computed(() => photoItems.value.map(item => item.url));
|
||||
const isPackage = computed(() => !!customerPackage.value);
|
||||
const sourceItems = computed<SourceItem[]>(() => customerPackage.value?.itemList || order.value?.itemList || []);
|
||||
const isPackage = computed(() => order.value?.orderType === "PACKAGE");
|
||||
const expectedItemType = computed(() => isPackage.value ? "PACKAGE_ENTITLEMENT" : "DIRECT_PRODUCT");
|
||||
const sourceItems = computed<GaoSalesOrderItem[]>(() => (order.value?.itemList || []).filter(item => item.itemType === expectedItemType.value));
|
||||
const canOperateStock = computed(() => userStore.hasPermissions(permissionCodes.productStock.create));
|
||||
const canFulfillByPayment = computed(() => order.value?.paymentStatus === "PAID" || (order.value?.paymentStatus === "PARTIALLY_PAID" && canOperateStock.value));
|
||||
|
||||
async function loadData(): Promise<void> {
|
||||
const salesOrderId = String(route.query.salesOrderId || "");
|
||||
const customerPackageId = String(route.query.customerPackageId || "");
|
||||
if (!salesOrderId) {
|
||||
Message.warning("请从订单详情或客户套餐详情进入履约创建");
|
||||
await router.replace("/management/order-fulfillments");
|
||||
@@ -130,26 +130,20 @@ async function loadData(): Promise<void> {
|
||||
}
|
||||
try {
|
||||
order.value = await SalesOrderAPI.detail(salesOrderId);
|
||||
if (customerPackageId) customerPackage.value = await PackageAPI.customerPackageDetail(customerPackageId);
|
||||
} catch (error) {
|
||||
Message.error(getErrorMessage(error, "履约来源加载失败"));
|
||||
}
|
||||
}
|
||||
|
||||
function getAvailableQuantity(item: SourceItem): string {
|
||||
if (isPackage.value) {
|
||||
return getPackageQuantityText((item as CustomerPackageItem).availableQuantity);
|
||||
}
|
||||
const orderItem = item as SalesOrderItem;
|
||||
const availableQuantity = Math.max(0, Number(orderItem.quantity || 0) - Number(orderItem.reservedQuantity || 0) - Number(orderItem.fulfilledQuantity || 0));
|
||||
return getPackageQuantityText(availableQuantity.toFixed(3));
|
||||
function getAvailableQuantity(item: GaoSalesOrderItem): string {
|
||||
return getPackageQuantityText(item.availableQuantity);
|
||||
}
|
||||
|
||||
function getItemDescription(item: SourceItem): string {
|
||||
function getItemDescription(item: GaoSalesOrderItem): string {
|
||||
return ` · 剩余 ${getAvailableQuantity(item)}`;
|
||||
}
|
||||
|
||||
function fillAllQuantity(item: SourceItem): void {
|
||||
function fillAllQuantity(item: GaoSalesOrderItem): void {
|
||||
if (item.id) {
|
||||
quantityMap[item.id] = getAvailableQuantity(item);
|
||||
}
|
||||
@@ -178,6 +172,14 @@ function validate(): boolean {
|
||||
Message.warning("请从订单详情或客户套餐详情进入履约创建");
|
||||
return false;
|
||||
}
|
||||
if (order.value.status !== "ACTIVE") {
|
||||
Message.warning("当前订单不是进行中状态,不能创建履约");
|
||||
return false;
|
||||
}
|
||||
if (!canFulfillByPayment.value) {
|
||||
Message.warning("订单尚未满足履约收款条件");
|
||||
return false;
|
||||
}
|
||||
if (!buildAttachments().length) {
|
||||
Message.warning("履约必须上传至少一张照片");
|
||||
return false;
|
||||
@@ -188,10 +190,15 @@ function validate(): boolean {
|
||||
return false;
|
||||
}
|
||||
for (const item of selected) {
|
||||
if (!normalizePackageQuantity(quantityMap[item.id || ""])) {
|
||||
const quantity = normalizePackageQuantity(quantityMap[item.id || ""]);
|
||||
if (!quantity) {
|
||||
Message.warning("履约数量必须是正数,最多三位小数");
|
||||
return false;
|
||||
}
|
||||
if (0 < comparePackageQuantity(quantity, item.availableQuantity ?? "0")) {
|
||||
Message.warning(`${item.productNameSnapshot || "产品"} 的可履约数量不足`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -201,10 +208,10 @@ async function save(): Promise<void> {
|
||||
if (!requestNo.value) requestNo.value = createPackageRequestNo();
|
||||
isSaving.value = true;
|
||||
try {
|
||||
const itemList: OrderFulfillmentItemRequest[] = sourceItems.value
|
||||
const itemList: GaoOrderFulfillmentItemRequest[] = sourceItems.value
|
||||
.filter(item => !!item.id && quantityMap[item.id])
|
||||
.map(item => ({
|
||||
...(isPackage.value ? { sourceCustomerPackageItemId: item.id } : { sourceOrderItemId: item.id }),
|
||||
sourceOrderItemId: item.id!,
|
||||
quantity: normalizePackageQuantity(quantityMap[item.id || ""])!
|
||||
}));
|
||||
const result = await FulfillmentAPI.create({
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
<template v-if="fulfillment">
|
||||
<t-cell-group title="履约单信息" theme="card" v-entrance-transition>
|
||||
<t-cell title="销售订单" arrow @click="toOrder" />
|
||||
<t-cell title="履约类型" :note="getFulfillmentTypeText(fulfillment.fulfillmentType)" />
|
||||
<t-cell title="订单项类型" :note="getSalesOrderItemTypeText(sourceItemType)" />
|
||||
<t-cell title="状态"><template #note><t-tag :content="getFulfillmentStatusText(fulfillment.status)" variant="light-outline" :theme="getStatusTheme(fulfillment.status)" /></template></t-cell>
|
||||
<t-cell title="履约时间">
|
||||
<template #note>
|
||||
@@ -52,22 +52,22 @@
|
||||
<t-cell v-if="!fulfillment.itemList?.length" title="暂无履约明细" />
|
||||
</t-cell-group>
|
||||
<section
|
||||
v-if="(isWaitingStock && canVerify) || (isPending && (canVerify || canCreate)) || (isVerified && canRevoke)"
|
||||
v-if="(!isOrderCancelled && isWaitingStock && canVerify) || (!isOrderCancelled && isPending && (canVerify || canCreate)) || (isVerified && canRevoke)"
|
||||
class="actions"
|
||||
v-entrance-transition="140"
|
||||
>
|
||||
<t-button
|
||||
v-if="isWaitingStock && canVerify"
|
||||
v-if="!isOrderCancelled && isWaitingStock && canVerify"
|
||||
theme="primary"
|
||||
content="准备库存"
|
||||
@click="prepareStock"
|
||||
/>
|
||||
<div v-if="isPending && canVerify" class="pair">
|
||||
<div v-if="!isOrderCancelled && isPending && canVerify" class="pair">
|
||||
<t-button theme="primary" content="核销履约" @click="verify" />
|
||||
<t-button theme="danger" content="拒绝履约" @click="openReason('reject')" />
|
||||
</div>
|
||||
<t-button
|
||||
v-if="isPending && canCreate"
|
||||
v-if="!isOrderCancelled && isPending && canCreate"
|
||||
theme="default"
|
||||
content="撤销履约"
|
||||
@click="openReason('cancel')"
|
||||
@@ -119,16 +119,18 @@ import { BottomPopup } from "timi-tdesign-mobile";
|
||||
import { PassTimeLabel } from "timi-web";
|
||||
import { useUserStore } from "timi-web-api";
|
||||
import FulfillmentAPI from "@/api/FulfillmentAPI";
|
||||
import SalesOrderAPI from "@/api/SalesOrderAPI";
|
||||
import PhotoScroller from "@/components/PhotoScroller.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { OrderFulfillment } from "@/types/Fulfillment";
|
||||
import type { GaoSalesOrderItemType, SalesOrder } from "@/types/SalesOrder";
|
||||
import type { PhotoItem } from "@/types/Photo";
|
||||
import type { Attachment } from "timi-web-api";
|
||||
import { createPackageRequestNo } from "@/utils/package";
|
||||
import { getAttachmentReadURL } from "@/utils/attachment";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { getProductPhotoURL } from "@/utils/product";
|
||||
import { getFulfillmentStatusText, getFulfillmentTypeText, getStatusTheme } from "@/utils/salesOrder";
|
||||
import { getFulfillmentStatusText, getSalesOrderItemTypeText, getStatusTheme } from "@/utils/salesOrder";
|
||||
import { Message } from "@/utils/message";
|
||||
import { permissionCodes } from "@/utils/permission";
|
||||
import { uploadPhotoList } from "@/utils/photo";
|
||||
@@ -140,6 +142,7 @@ const router = useRouter();
|
||||
const navBarStore = useNavBarStore();
|
||||
const userStore = useUserStore();
|
||||
const fulfillment = ref<OrderFulfillment>();
|
||||
const order = ref<SalesOrder>();
|
||||
const loading = ref(false);
|
||||
const operating = ref(false);
|
||||
const reasonVisible = ref(false);
|
||||
@@ -163,14 +166,25 @@ const fulfillmentPhotoURLList = computed(() => fulfillmentPhotoItems.value.map(i
|
||||
const isWaitingStock = computed(() => fulfillment.value?.status === "WAITING_STOCK");
|
||||
const isPending = computed(() => fulfillment.value?.status === "PENDING");
|
||||
const isVerified = computed(() => fulfillment.value?.status === "VERIFIED");
|
||||
const isOrderCancelled = computed(() => order.value?.status === "CANCELLED");
|
||||
const canVerify = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.verify));
|
||||
const canCreate = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.create));
|
||||
const canRevoke = computed(() => userStore.hasPermissions(permissionCodes.orderFulfillment.revoke));
|
||||
const sourceItemType = computed<GaoSalesOrderItemType | undefined>(() => {
|
||||
const sourceOrderItemId = fulfillment.value?.itemList?.[0]?.sourceOrderItemId;
|
||||
return order.value?.itemList?.find(item => item.id === sourceOrderItemId)?.itemType;
|
||||
});
|
||||
async function loadData(): Promise<void> {
|
||||
const id = String(route.params.id || "");
|
||||
if (!id) return;
|
||||
loading.value = true;
|
||||
try { fulfillment.value = await FulfillmentAPI.detail(id); }
|
||||
try {
|
||||
const result = await FulfillmentAPI.detail(id);
|
||||
fulfillment.value = result;
|
||||
order.value = result.salesOrderId && userStore.hasPermissions(permissionCodes.salesOrder.read)
|
||||
? await SalesOrderAPI.detail(result.salesOrderId)
|
||||
: undefined;
|
||||
}
|
||||
catch (error) { Message.error(getErrorMessage(error, "履约单加载失败")); }
|
||||
finally { loading.value = false; }
|
||||
}
|
||||
@@ -194,9 +208,10 @@ function getCurrentRouteDepth(): number {
|
||||
}
|
||||
async function prepareStock(): Promise<void> {
|
||||
if (!fulfillment.value?.id || operating.value) return;
|
||||
if (isOrderCancelled.value) { Message.warning("已取消订单不能准备库存"); return; }
|
||||
operating.value = true;
|
||||
try {
|
||||
await FulfillmentAPI.prepareStock({ fulfillmentId: fulfillment.value.id });
|
||||
fulfillment.value = await FulfillmentAPI.prepareStock({ fulfillmentId: fulfillment.value.id, reason: "库存已备齐" });
|
||||
Message.success("库存准备已提交");
|
||||
await loadData();
|
||||
} catch (error) { Message.error(getErrorMessage(error, "库存准备失败")); }
|
||||
@@ -204,9 +219,10 @@ async function prepareStock(): Promise<void> {
|
||||
}
|
||||
async function verify(): Promise<void> {
|
||||
if (!fulfillment.value?.id || operating.value) return;
|
||||
if (isOrderCancelled.value) { Message.warning("已取消订单不能核销履约"); return; }
|
||||
operating.value = true;
|
||||
try {
|
||||
await FulfillmentAPI.verify({ fulfillmentId: fulfillment.value.id, reason: "" });
|
||||
fulfillment.value = await FulfillmentAPI.verify({ fulfillmentId: fulfillment.value.id, reason: "" });
|
||||
Message.success("履约已核销");
|
||||
await loadData();
|
||||
} catch (error) { Message.error(getErrorMessage(error, "履约核销失败")); }
|
||||
@@ -218,8 +234,9 @@ async function submitReason(): Promise<void> {
|
||||
operating.value = true;
|
||||
try {
|
||||
const payload = { fulfillmentId: fulfillment.value.id, reason: reason.value.trim() };
|
||||
if (reasonAction.value === "reject") await FulfillmentAPI.reject(payload);
|
||||
else await FulfillmentAPI.cancel(payload);
|
||||
fulfillment.value = reasonAction.value === "reject"
|
||||
? await FulfillmentAPI.reject(payload)
|
||||
: await FulfillmentAPI.cancel(payload);
|
||||
reasonVisible.value = false;
|
||||
Message.success(reasonAction.value === "reject" ? "履约已拒绝" : "履约已撤销");
|
||||
await loadData();
|
||||
@@ -236,7 +253,7 @@ async function submitRevoke(): Promise<void> {
|
||||
if (!revokeRequestNo.value) revokeRequestNo.value = createPackageRequestNo();
|
||||
operating.value = true;
|
||||
try {
|
||||
await FulfillmentAPI.revoke({
|
||||
fulfillment.value = await FulfillmentAPI.revoke({
|
||||
fulfillmentId: fulfillment.value.id,
|
||||
requestNo: revokeRequestNo.value,
|
||||
reason: revokeReason.value.trim(),
|
||||
|
||||
@@ -2,12 +2,6 @@
|
||||
<div v-can="permissionCodes.orderFulfillment.read" class="fulfillment-list">
|
||||
<div class="filters">
|
||||
<t-dropdown-menu class="menus">
|
||||
<t-dropdown-item
|
||||
v-model="type"
|
||||
label="履约类型"
|
||||
:options="typeOptions"
|
||||
@change="selectType"
|
||||
/>
|
||||
<t-dropdown-item
|
||||
v-model="status"
|
||||
label="履约状态"
|
||||
@@ -47,7 +41,7 @@ import FulfillmentAPI from "@/api/FulfillmentAPI";
|
||||
import NavBarActionMenu from "@/components/NavBarActionMenu.vue";
|
||||
import OrderFulfillmentListCell from "@/pages/fulfillment/components/OrderFulfillmentListCell.vue";
|
||||
import { useNavBarStore } from "@/store/navBarStore";
|
||||
import type { FulfillmentStatus, FulfillmentType, OrderFulfillment } from "@/types/Fulfillment";
|
||||
import type { GaoFulfillmentStatus, OrderFulfillment } from "@/types/Fulfillment";
|
||||
import type { NavBarAction } from "@/types/NavBarAction";
|
||||
import { getErrorMessage } from "@/utils/error";
|
||||
import { Message } from "@/utils/message";
|
||||
@@ -76,8 +70,7 @@ const actions = computed<NavBarAction[]>(() => [
|
||||
action: toCustomerPackageList
|
||||
}
|
||||
]);
|
||||
const type = ref<"ALL" | FulfillmentType>("ALL");
|
||||
const status = ref<"ALL" | FulfillmentStatus>(getInitialStatus(route.query.status));
|
||||
const status = ref<"ALL" | GaoFulfillmentStatus>(getInitialStatus(route.query.status));
|
||||
const list = ref<OrderFulfillment[]>([]);
|
||||
const pageIndex = ref(0);
|
||||
const total = ref(0);
|
||||
@@ -85,11 +78,6 @@ const loading = ref(false);
|
||||
const refreshing = ref(false);
|
||||
const noMore = ref(false);
|
||||
const pageSize = 15;
|
||||
const typeOptions = [
|
||||
{ label: "全部类型", value: "ALL" },
|
||||
{ label: "套餐权益", value: "PACKAGE_ENTITLEMENT" },
|
||||
{ label: "直接产品", value: "DIRECT_PRODUCT" }
|
||||
];
|
||||
const statusOptions = [
|
||||
{ label: "全部状态", value: "ALL" },
|
||||
{ label: "等待库存", value: "WAITING_STOCK" },
|
||||
@@ -104,10 +92,10 @@ function getQueryText(value: unknown): string {
|
||||
return Array.isArray(value) ? "" : typeof value === "string" ? value : "";
|
||||
}
|
||||
|
||||
function getInitialStatus(value: unknown): "ALL" | FulfillmentStatus {
|
||||
function getInitialStatus(value: unknown): "ALL" | GaoFulfillmentStatus {
|
||||
const statusValue = getQueryText(value);
|
||||
if (["WAITING_STOCK", "PENDING", "VERIFIED", "REJECTED", "CANCELLED", "REVOKED"].includes(statusValue)) {
|
||||
return statusValue as FulfillmentStatus;
|
||||
return statusValue as GaoFulfillmentStatus;
|
||||
}
|
||||
return "ALL";
|
||||
}
|
||||
@@ -128,7 +116,6 @@ async function fetchList(reset: boolean): Promise<void> {
|
||||
index: pageIndex.value,
|
||||
size: pageSize,
|
||||
salesOrderId: getQueryText(route.query.salesOrderId) || undefined,
|
||||
fulfillmentType: type.value === "ALL" ? undefined : type.value,
|
||||
status: status.value === "ALL" ? undefined : status.value
|
||||
});
|
||||
const rows = result.list || [];
|
||||
@@ -159,16 +146,9 @@ async function loadMore(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function selectType(value: DropdownValue): Promise<void> {
|
||||
if (!Array.isArray(value)) {
|
||||
type.value = String(value) as "ALL" | FulfillmentType;
|
||||
await fetchList(true);
|
||||
}
|
||||
}
|
||||
|
||||
async function selectStatus(value: DropdownValue): Promise<void> {
|
||||
if (!Array.isArray(value)) {
|
||||
status.value = String(value) as "ALL" | FulfillmentStatus;
|
||||
status.value = String(value) as "ALL" | GaoFulfillmentStatus;
|
||||
await fetchList(true);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user