ERPTurbo_Client/packages/app-client/src/components/purchase/module/OrderCost.tsx
shenyifei cbde9caac1 feat(purchase): 优化采购订单费用管理功能
- 移除调试日志输出
- 新增 requireQuantityAndPrice 字段支持
- 调整页面样式间距统一为 p-2.5
- 交换工头垫付与产地垫付显示逻辑
- 完善费用弹窗编辑功能,支持数量单价分别输入
- 增加新手引导 tour 功能
- 优化订单成本列表更新逻辑,避免重复项
- 导出新增的费用 section 组件
- 替换 LaborInfoSection为 WorkerAdvanceSection
- 引入 ProductionAdvanceSection 组件
2025-11-17 10:43:18 +08:00

511 lines
17 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { View } from "@tarojs/components";
import { Icon } from "@/components";
import { forwardRef, useEffect, useImperativeHandle, useState } from "react";
import { business } from "@/services";
import { Button, Checkbox, Input, Toast } from "@nutui/nutui-react-taro";
import { CostItem, SupplierVO } from "@/types/typings";
import { generateShortId } from "@/utils/generateShortId";
// 定义ref暴露的方法接口
export interface OrderCostRef {
validate: () => boolean;
}
export interface IOrderCostProps {
value: CostItem[];
supplierVO: SupplierVO;
onChange?: (costItemList: CostItem[]) => void;
onAdd: () => void;
}
export default forwardRef<OrderCostRef, IOrderCostProps>(
function OrderCost(IOrderCostProps, ref) {
const { value, supplierVO, onChange, onAdd } = IOrderCostProps;
const [costItemVOList, setCostItemVOList] = useState<CostItem[]>();
// 总的工头姓名状态
const [principal, setPrincipal] = useState<string>("");
// 工头姓名错误状态
const [principalError, setPrincipalError] = useState<boolean>(false);
const init = async () => {
const { data } = await business.costItem.listCostItem({
costItemListQry: {
status: true,
showInEntry: true,
},
});
const costItemList = data.data;
if (costItemList) {
// 人工辅料选中
const initialList =
costItemList
?.filter(
(item) =>
item.costType === "HUMAN_COST" ||
item.costType === "PACKAGING_MATERIALS",
)
.map((item) => {
// 查找是否在purchaseOrder中已有该item的记录
const existingItem = value?.find(
(costItem) => costItem.itemId === item.itemId,
);
return {
orderCostId: existingItem
? existingItem.orderCostId
: generateShortId(),
itemId: item.itemId,
name: item.name,
price: item.price,
unit: item.unit,
selected: existingItem ? existingItem.selected : false,
count: existingItem ? existingItem.count : 1,
payerType: existingItem ? existingItem.payerType : undefined,
principal: existingItem ? existingItem.principal : "",
costType: item.costType,
requireQuantityAndPrice: existingItem
? existingItem.requireQuantityAndPrice
: false,
};
}) || [];
setCostItemVOList([
...(initialList || []),
...(value
?.filter(
(item) =>
item.costType === "WORKER_ADVANCE" ||
item.costType === "PRODUCTION_ADVANCE",
)
.map((item) => {
return {
orderCostId: item.orderCostId,
itemId: item.itemId,
name: item.name,
price: item.price,
unit: item.unit,
selected: item.selected,
count: item.count,
payerType: item.payerType,
principal: item.principal,
costType: item.costType,
requireQuantityAndPrice: item.requireQuantityAndPrice,
};
}) || []),
]);
// 初始化总的工头姓名(如果有启用且费用承担方为"我方"的项目)
const enabledUsItems = initialList.filter(
(item) =>
item.selected &&
item.payerType === "US" &&
item.costType === "HUMAN_COST",
);
if (enabledUsItems.length > 0 && enabledUsItems[0].principal) {
setPrincipal(enabledUsItems[0].principal || "");
}
}
};
// 当传入的value发生变化时重新初始化列表
useEffect(() => {
init().then();
}, []);
// 当内部状态发生变化时,通知父组件更新
useEffect(() => {
if (costItemVOList && onChange) {
// 更新所有启用且费用承担方为"我方"的人工费用项的工头姓名
const updatedList = costItemVOList.map((item) => {
if (
item.costType === "HUMAN_COST" &&
item.selected &&
item.payerType === "US"
) {
return { ...item, principal };
}
return item;
});
onChange(updatedList);
}
}, [costItemVOList, principal]);
// 错误状态
const [countError, setCountError] = useState<{ [key: string]: boolean }>(
{},
);
const [payerTypeError, setPayerTypeError] = useState<{
[key: string]: boolean;
}>({});
// 设置人工项目选择状态
const setArtificialSelect = (id: string, selected: boolean) => {
if (!costItemVOList) return;
const newList = costItemVOList.map((item) =>
item.orderCostId === id ? { ...item, selected } : item,
);
setCostItemVOList(newList);
};
// 处理数量变化
const handleCountChange = (id: string, value: number) => {
if (!costItemVOList) return;
const newList = costItemVOList.map((item) =>
item.orderCostId === id ? { ...item, count: value } : item,
);
setCostItemVOList(newList);
// 校验数量
if (costItemVOList.find((item) => item.orderCostId === id)?.selected) {
validateCount(id, value);
}
};
// 校验数量
const validateCount = (id: string, value: number) => {
const isValid = value > 0;
setCountError((prev) => ({
...prev,
[id]: !isValid,
}));
return isValid;
};
// 校验费用承担方
const validatePayerType = (id: string, value: CostItem["payerType"]) => {
const isValid = value === "US" || value === "OTHER";
setPayerTypeError((prev) => ({
...prev,
[id]: !isValid,
}));
return isValid;
};
// 校验工头姓名
const validatePrincipal = (value: string) => {
const isValid = value.trim().length > 0;
setPrincipalError(!isValid);
return isValid;
};
// 处理费用承担方变化
const handlePayerTypeChange = (
id: string,
value: CostItem["payerType"],
) => {
if (!costItemVOList) return;
const newList = costItemVOList.map((item) =>
item.orderCostId === id ? { ...item, payerType: value } : item,
);
setCostItemVOList(newList);
// 校验费用承担方
if (costItemVOList.find((item) => item.orderCostId === id)?.selected) {
validatePayerType(id, value);
}
};
// 处理工头姓名变化
const handlePrincipalChange = (value: string) => {
setPrincipal(value);
// 如果有启用且费用承担方为"我方"的项目,则校验工头姓名
const enabledUsItems = costItemVOList?.filter(
(item) =>
item.costType === "HUMAN_COST" &&
item.selected &&
item.payerType === "US",
);
if (enabledUsItems && enabledUsItems.length > 0) {
validatePrincipal(value);
}
};
// 失去焦点时校验数量
const handleCountBlur = (id: string, value: number) => {
validateCount(id, value);
};
// 失去焦点时校验工头姓名
const handlePrincipalBlur = (value: string) => {
validatePrincipal(value);
};
// 对外暴露的校验方法
const validate = () => {
if (!costItemVOList) return true;
let isValid = true;
costItemVOList.forEach((item) => {
if (item.selected) {
// 校验数量
if (!validateCount(item.orderCostId, item.count)) {
isValid = false;
}
if (item.costType === "HUMAN_COST") {
// 校验费用承担方
if (!validatePayerType(item.orderCostId, item.payerType)) {
isValid = false;
}
}
}
});
// 校验总的工头姓名(如果有启用且费用承担方为"我方"的项目)
const enabledUsItems = costItemVOList.filter(
(item) =>
item.costType === "HUMAN_COST" &&
item.selected &&
item.payerType === "US",
);
if (enabledUsItems.length > 0) {
if (!validatePrincipal(principal)) {
isValid = false;
}
}
if (!isValid) {
Toast.show("toast", {
icon: "fail",
title: "提示",
content: "请完善人工信息后再进行下一步操作",
});
}
return isValid;
};
useImperativeHandle(ref, () => ({
validate,
}));
// 获取指定类型的项目列表
const getItemsByCostType = (costType: string) => {
return costItemVOList?.filter((item) => item.costType === costType) || [];
};
// 渲染项目列表
const renderItemList = (items: CostItem[], type: string) => {
return items.map((item) => (
<View key={item.orderCostId}>
<View className={"flex flex-col gap-2.5 rounded-lg bg-white p-2.5"}>
<View className={"flex flex-row justify-between"}>
<View className="block text-sm font-normal text-[#000000]">
{item.name}
</View>
<Checkbox
className={"flex flex-row items-center"}
checked={item.selected}
onChange={(checked) => {
setArtificialSelect(item.orderCostId, checked);
}}
>
<View className={"text-sm font-normal text-[#000000]"}>
</View>
</Checkbox>
</View>
{item.selected && type === "HUMAN_COST" && (
<View>
{/* 费用承担方改为按钮形式参考OrderPackage中的样式 */}
<View className="flex items-center justify-between">
<View className="flex-shrink-0 text-sm">:</View>
<View className="flex gap-2">
<Button
size="small"
type={item.payerType === "US" ? "primary" : "default"}
onClick={() => {
handlePayerTypeChange(item.orderCostId, "US");
}}
>
</Button>
<Button
size="small"
type={item.payerType === "OTHER" ? "primary" : "default"}
onClick={() => {
handlePayerTypeChange(item.orderCostId, "OTHER");
}}
>
</Button>
</View>
</View>
{payerTypeError[item.orderCostId] && item.selected && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
)}
{item.selected && (
<View>
<View className="flex items-center justify-between gap-2">
<View className="flex-shrink-0 text-sm">:</View>
<View className="flex items-center">
<View
className="rounded-l-button flex !size-12 flex-none items-center justify-center bg-gray-200 text-sm"
onClick={() => {
const count = item.count || 1;
handleCountChange(
item.orderCostId,
Math.max(1, count - 1),
);
}}
>
<Icon name="minus" size={20} />
</View>
<View
className={`flex h-12 w-full shrink items-center border-4 border-gray-200`}
>
<Input
type="number"
value={item.count.toString()}
align={"center"}
placeholder={"用了多少"}
onChange={(value) => {
handleCountChange(item.orderCostId, Number(value));
}}
formatter={(value) =>
Math.max(1, Number(value)).toString()
}
onBlur={() =>
handleCountBlur(item.orderCostId, item.count)
}
/>
</View>
<View
className="rounded-r-button flex !size-12 flex-none items-center justify-center bg-gray-200 text-sm"
onClick={() => {
const count = item.count || 1;
handleCountChange(item.orderCostId, count + 1);
}}
>
<Icon name="plus" size={20} />
</View>
<View className={"ml-2.5 text-sm text-gray-500"}>
{item.unit}
</View>
</View>
</View>
{countError[item.orderCostId] && item.selected && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
)}
</View>
</View>
));
};
// 检查是否有人工费用项被启用且费用承担方为"我方"
const shouldShowPrincipalInput = () => {
return (
costItemVOList?.some(
(item) =>
item.costType === "HUMAN_COST" &&
item.selected &&
item.payerType === "US",
) || false
);
};
return (
<View className="flex flex-1 flex-col gap-2.5 bg-[#D1D5DB] p-2.5 pt-2.5">
<View className={"flex flex-1 flex-col gap-2.5"}>
<View className="text-sm font-bold"></View>
<View className="flex items-center rounded-lg border border-blue-200 bg-blue-50 p-2.5">
<Icon
className={"mr-1"}
name="circle-info"
color={"var(--color-blue-700)"}
size={18}
/>
<View className={"text-sm text-blue-700"}></View>
</View>
{renderItemList(getItemsByCostType("HUMAN_COST"), "HUMAN_COST")}
{/* 总的工头姓名输入框 */}
{shouldShowPrincipalInput() && (
<View>
<View className="mb-1 text-sm font-medium"></View>
<View
className={`flex h-12 items-center rounded-md px-3 ${principalError ? "border-2 border-red-500 bg-red-100" : "border-2 border-gray-300 bg-white"}`}
>
<Input
className="text-base"
type="text"
placeholder={"工头的名字"}
value={principal}
onChange={(value) => {
handlePrincipalChange(value);
}}
onBlur={() => handlePrincipalBlur(principal)}
/>
</View>
{principalError && (
<View className="mt-1 text-xs text-red-500">
</View>
)}
</View>
)}
</View>
<View className={"flex flex-1 flex-col gap-2.5"}>
<View className="text-sm font-bold"></View>
<View className="flex items-center rounded-lg border border-blue-200 bg-blue-50 p-2.5">
<Icon
className={"mr-1"}
name="circle-info"
color={"var(--color-blue-700)"}
size={18}
/>
<View className={"text-sm text-blue-700"}></View>
</View>
{renderItemList(
getItemsByCostType("PACKAGING_MATERIALS"),
"PACKAGING_MATERIALS",
)}
</View>
{/*<View className={"mb-2.5"}>*/}
{/* <View className="mb-2.5 text-sm font-bold">空纸箱费用</View>*/}
{/* {renderItemList(getItemsByCostType("OTHER_COST"), "OTHER_COST")}*/}
{/*</View>*/}
{/*<View className={"mb-2.5"}>*/}
{/* <View className="mb-2.5 text-sm font-bold">空礼盒费用</View>*/}
{/* {renderItemList(getItemsByCostType("OTHER_COST"), "OTHER_COST")}*/}
{/*</View>*/}
{/* 只有当用户选择"否"时才显示添加按钮 */}
{!supplierVO.isLast && (
<Button
icon={<Icon name={"plus"} size={20} />}
type={"primary"}
size={"xlarge"}
fill={"outline"}
block
className="border-primary text-primary flex w-full items-center justify-center !border-4 !bg-white"
onClick={() => {
onAdd();
}}
>
<View></View>
</Button>
)}
</View>
);
},
);