This commit is contained in:
xuqssq
2024-12-28 17:32:16 +08:00
parent b97cdd4685
commit 579cc76f83
5 changed files with 256 additions and 179 deletions

View File

@@ -22,12 +22,15 @@ import { v4 as uuidv4 } from "uuid";
import { supabase } from "@/config/supabase";
import { supabaseService } from "@/hooks/supabaseService";
const { Text } = Typography;
import { defaultSymbol, formatExchangeRate } from "@/utils/exchange_rate";
const SectionList = ({
form,
isView,
formValues,
type,
currentCurrency = "TWD",
taxRate,
setTaxRate,
}) => {
const [editingSectionIndex, setEditingSectionIndex] = useState(null);
const [editingSectionName, setEditingSectionName] = useState("");
@@ -36,33 +39,6 @@ const SectionList = ({
const [loading, setLoading] = useState(false);
const [units, setUnits] = useState([]);
const [loadingUnits, setLoadingUnits] = useState(false);
const CURRENCY_SYMBOLS = {
CNY: "¥",
TWD: "NT$",
USD: "$",
};
const calculateItemAmount = (quantity, price) => {
const safeQuantity = Number(quantity) || 0;
const safePrice = Number(price) || 0;
return safeQuantity * safePrice;
};
const calculateSectionTotal = (items = []) => {
if (!Array.isArray(items)) return 0;
return items.reduce((sum, item) => {
if (!item) return sum;
return sum + calculateItemAmount(item.quantity, item.price);
}, 0);
};
const formatCurrency = (amount) => {
const safeAmount = Number(amount) || 0;
return `${CURRENCY_SYMBOLS[currentCurrency] || "NT$"}${safeAmount.toLocaleString("zh-TW", {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
};
const fetchAvailableSections = async () => {
try {
@@ -236,7 +212,7 @@ const SectionList = ({
{item.name}
</span>
<span className="text-sm text-gray-500 ml-2">
{formatCurrency(item.price)}
{formatExchangeRate(currentCurrency, item.price)}
</span>
</div>
))}
@@ -250,7 +226,8 @@ const SectionList = ({
<div className="mt-4 pt-4 border-t flex justify-between items-center">
<span className="text-sm text-gray-600">总金额</span>
<span className="text-base font-medium text-blue-500">
{formatCurrency(
{formatExchangeRate(
currentCurrency,
(section.attributes.items || []).reduce(
(sum, item) =>
sum + (item.price * (item.quantity || 1) || 0),
@@ -323,7 +300,7 @@ const SectionList = ({
return (
<div className="text-right">
<span className="text-gray-500">
{formatCurrency(subtotal, currentCurrency)}
{formatExchangeRate(currentCurrency, subtotal)}
</span>
</div>
);
@@ -345,13 +322,12 @@ const SectionList = ({
<span className="text-gray-500">
小计总额
<span className="text-blue-500 font-medium ml-2">
{formatCurrency(total, currentCurrency)}
{formatExchangeRate(currentCurrency, total)}
</span>
</span>
</div>
);
});
return (
<>
<Form.List name="sections">
@@ -442,127 +418,137 @@ const SectionList = ({
<div>描述/备注</div>
<div>单位</div>
<div className="text-center">数量</div>
<div className="text-center">单价</div>
<div className="text-center">
单价({defaultSymbol})
</div>
<div className="text-right">小计</div>
<div></div>
</div>
{itemFields.map((itemField, itemIndex) => (
<div
key={itemField.key}
className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-4 items-start"
>
<Form.Item
{...itemField}
name={[itemField.name, "name"]}
className="!mb-0"
{itemFields.map((itemField, itemIndex) => {
const { key, ...restItemField } = itemField;
return (
<div
key={key}
className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-4 items-start"
>
<Input placeholder="服务项目名称" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "description"]}
className="!mb-0"
>
<Input placeholder="请输入描述/备注" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Select
placeholder="选择单位"
loading={loadingUnits}
showSearch
allowClear
style={{ minWidth: "120px" }}
options={units.map((unit) => ({
label: unit.attributes.name,
value: unit.attributes.name,
}))}
onDropdownVisibleChange={(open) => {
if (open) fetchUnits();
}}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "12px 0" }} />
<div style={{ padding: "4px" }}>
<Input.Search
placeholder="输入新单位名称"
enterButton={<PlusOutlined />}
onSearch={async (value) => {
if (!value.trim()) return;
if (
await handleAddUnit(value.trim())
) {
const currentItems =
form.getFieldValue([
"sections",
field.name,
"items",
]);
currentItems[itemField.name].unit =
value.trim();
form.setFieldValue(
["sections", field.name, "items"],
currentItems
);
}
}}
/>
</div>
</>
)}
<Form.Item
{...restItemField}
name={[itemField.name, "name"]}
className="!mb-0"
>
<Input placeholder="服务项目名称" />
</Form.Item>
<Form.Item
{...restItemField}
name={[itemField.name, "description"]}
className="!mb-0"
>
<Input placeholder="请输入描述/备注" />
</Form.Item>
<Form.Item
{...restItemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Select
placeholder="选择单位"
loading={loadingUnits}
showSearch
allowClear
style={{ minWidth: "120px" }}
options={units.map((unit) => ({
label: unit.attributes.name,
value: unit.attributes.name,
}))}
onDropdownVisibleChange={(open) => {
if (open) fetchUnits();
}}
dropdownRender={(menu) => (
<>
{menu}
<Divider style={{ margin: "12px 0" }} />
<div style={{ padding: "4px" }}>
<Input.Search
placeholder="输入新单位名称"
enterButton={<PlusOutlined />}
onSearch={async (value) => {
if (!value.trim()) return;
if (
await handleAddUnit(value.trim())
) {
const currentItems =
form.getFieldValue([
"sections",
field.name,
"items",
]);
currentItems[
itemField.name
].unit = value.trim();
form.setFieldValue(
[
"sections",
field.name,
"items",
],
currentItems
);
}
}}
/>
</div>
</>
)}
/>
</Form.Item>
<Form.Item
{...restItemField}
name={[itemField.name, "quantity"]}
className="!mb-0"
>
<InputNumber
placeholder="数量"
min={0}
className="w-full"
/>
</Form.Item>
<Form.Item
{...restItemField}
name={[itemField.name, "price"]}
className="!mb-0"
>
<InputNumber
placeholder="单价"
min={0}
className="w-full"
/>
</Form.Item>
<ItemSubtotal
quantity={
formValues?.sections?.[sectionIndex]?.items?.[
itemIndex
]?.quantity
}
price={
formValues?.sections?.[sectionIndex]?.items?.[
itemIndex
]?.price
}
currentCurrency={currentCurrency}
/>
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "quantity"]}
className="!mb-0"
>
<InputNumber
placeholder="数量"
min={0}
className="w-full"
/>
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "price"]}
className="!mb-0"
>
<InputNumber
placeholder="单价"
min={0}
className="w-full"
/>
</Form.Item>
<ItemSubtotal
quantity={
formValues?.sections?.[sectionIndex]?.items?.[
itemIndex
]?.quantity
}
price={
formValues?.sections?.[sectionIndex]?.items?.[
itemIndex
]?.price
}
currentCurrency={currentCurrency}
/>
{!isView && itemFields.length > 1 && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeItem(itemField.name)}
className="flex items-center justify-center"
/>
)}
</div>
))}
{!isView && itemFields.length > 1 && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeItem(itemField.name)}
className="flex items-center justify-center"
/>
)}
</div>
);
})}
{!isView && (
<Button
@@ -586,21 +572,74 @@ const SectionList = ({
))}
</div>
{!isView && (
<div className="mt-6 flex justify-center">
<Button
type="dashed"
onClick={() => {
setTemplateModalVisible(true);
fetchAvailableSections();
}}
icon={<PlusOutlined />}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
>
新建模块
</Button>
<div className="flex items-center justify-center mt-6">
{!isView && (
<div className="w-full flex justify-center">
<Button
type="dashed"
onClick={() => {
setTemplateModalVisible(true);
fetchAvailableSections();
}}
icon={<PlusOutlined />}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
>
新建模块
</Button>
</div>
)}
<div className="flex w-fit flex-shrink-0 flex-col gap-1">
<span className="text-gray-500 flex items-center">
税前总额
<span className="text-blue-500 font-medium ml-2">
{formatExchangeRate(
currentCurrency,
formValues?.sections?.reduce((sum, section) => {
return (
sum +
section.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0)
);
}, 0)
)}
</span>
</span>
<div className="text-gray-500 flex items-center">
税率
<div className="text-blue-500 font-medium ml-2">
<InputNumber
suffix="%"
style={{ width: 120 }}
min={0}
max={100}
value={taxRate}
onChange={(value) => setTaxRate(value)}
/>
</div>
</div>
<span className="text-gray-500 flex items-center">
税后总额
<span className="text-blue-500 font-medium ml-2">
{formatExchangeRate(
currentCurrency,
formValues?.sections?.reduce((sum, section) => {
return (
sum +
section.items.reduce((sum, item) => {
return sum + item.price * item.quantity;
}, 0)
);
}, 0) *
(1 + taxRate / 100)
)}
</span>
</span>
</div>
)}
</div>
<Modal
title={<h3 className="text-lg font-medium">选择模版</h3>}
@@ -619,5 +658,4 @@ const SectionList = ({
);
};
export default SectionList;