服务模块

This commit is contained in:
‘Liammcl’
2024-12-28 15:51:26 +08:00
parent 4f0e7be806
commit d021f39b04
8 changed files with 623 additions and 895 deletions

View File

@@ -18,16 +18,12 @@ import {
ArrowLeftOutlined,
SaveOutlined,
DeleteOutlined,
CloseOutlined,
EditOutlined,
CheckOutlined,
} from "@ant-design/icons";
import { supabase } from "@/config/supabase";
import { useNavigate, useParams, useSearchParams } from "react-router-dom";
import { v4 as uuidv4 } from "uuid";
const { TextArea } = Input;
const { Title, Text } = Typography;
import SectionList from '@/components/SectionList'
const { Title } = Typography;
// 添加货币符号映射
const CURRENCY_SYMBOLS = {
@@ -47,7 +43,7 @@ const QuotationForm = () => {
const [dataSource, setDataSource] = useState([{ id: Date.now() }]);
const [totalAmount, setTotalAmount] = useState(0);
const [loading, setLoading] = useState(false);
const [currentCurrency, setCurrentCurrency] = useState("CNY");
const [currentCurrency, setCurrentCurrency] = useState("TWD");
const [customers, setCustomers] = useState([]);
const [selectedCustomers, setSelectedCustomers] = useState([]);
const [formValues, setFormValues] = useState({});
@@ -737,304 +733,28 @@ const QuotationForm = () => {
</div>
</Card>
{/* 报价单细卡片 */}
<Form.List name="sections">
{(fields, { add, remove }) => (
<>
<div className="space-y-4">
{fields.map((field, sectionIndex) => (
<Card
key={`section-${field.key}`}
className="shadow-sm rounded-lg"
type="inner"
title={
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
{editingSectionIndex === sectionIndex ? (
<div className="flex items-center gap-2">
<Input
value={editingSectionName}
onChange={(e) =>
setEditingSectionName(e.target.value)
}
onPressEnter={handleSectionNameSave}
autoFocus
className="w-48"
/>
<Button
type="link"
icon={<CheckOutlined />}
onClick={handleSectionNameSave}
className="text-green-500 hover:text-green-600"
/>
<Button
type="link"
icon={<CloseOutlined />}
onClick={handleSectionNameCancel}
className="text-red-500 hover:text-red-600"
/>
</div>
) : (
<div className="flex items-center gap-2">
<span className="w-1 h-4 bg-purple-500 rounded-full" />
<Text
strong
className="text-lg dark:text-gray-200"
>
{form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
]) || `服务类<EFBFBD><EFBFBD> ${sectionIndex + 1}`}
</Text>
{(!id || isEdit) && (
<Button
type="link"
icon={<EditOutlined />}
onClick={() =>
handleSectionNameEdit(
sectionIndex,
form.getFieldValue([
"sections",
sectionIndex,
"sectionName",
]) || `服务类型 ${sectionIndex + 1}`
)
}
className="text-gray-400 hover:text-blue-500"
/>
)}
</div>
)}
</div>
</div>
}
>
<Form.List name={[field.name, "items"]}>
{(itemFields, { add: addItem, remove: removeItem }) => (
<>
{/* 表头 */}
<div className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-2 text-gray-500 px-2">
<div>项目明细</div>
<div>描述/备注</div>
<div>单位</div>
<div className="text-center">数量</div>
<div className="text-center">单价</div>
<div className="text-right">小计</div>
<div>操作</div>
</div>
{itemFields.map((itemField, itemIndex) => (
<div
key={`${sectionIndex}-${itemIndex}`}
className="grid grid-cols-[3fr_4fr_1fr_1fr_2fr_1fr_40px] gap-4 mb-4 items-start"
>
<Form.Item
{...itemField}
name={[itemField.name, "productName"]}
className="!mb-0"
>
<Input placeholder="服务项目名称" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "note"]}
className="!mb-0"
>
<Input placeholder="请输入描述/备注" />
</Form.Item>
<Form.Item
{...itemField}
name={[itemField.name, "unit"]}
className="!mb-0"
>
<Input placeholder="单位" />
</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>
<div className="text-right">
<span className="text-gray-500">
{formatCurrency(
calculateItemAmount(
formValues?.sections?.[sectionIndex]
?.items?.[itemIndex]?.quantity,
formValues?.sections?.[sectionIndex]
?.items?.[itemIndex]?.price
)
)}
</span>
</div>
{!isView && (
<Button
type="text"
danger
icon={<DeleteOutlined />}
onClick={() => removeItem(itemField.name)}
className="flex items-center justify-center"
/>
)}
</div>
))}
{!isView && (
<Button
type="dashed"
onClick={() =>
handleAddItem(addItem, sectionIndex)
}
icon={<PlusOutlined />}
className="w-full hover:border-blue-400 hover:text-blue-500 mb-4"
>
添加服务项目
</Button>
)}
<div className="flex justify-end border-t pt-4">
<span className="text-gray-500">
小计总额
<span className="text-blue-500 font-medium ml-2">
{formatCurrency(
calculateSectionTotal(
formValues?.sections?.[sectionIndex]
?.items
)
)}
</span>
</span>
</div>
</>
)}
</Form.List>
</Card>
))}
</div>
{/* Add section button */}
{!isView && (
<div className="mt-6 flex justify-center">
<Button
type="dashed"
onClick={handleAddSection}
icon={<PlusOutlined />}
className="w-1/3 border-2 hover:border-blue-400 hover:text-blue-500"
>
新建小节
</Button>
</div>
)}
{/* 总金额统计 */}
<div className="mt-6 space-y-4 pt-4 border-t">
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600 font-medium">税前总计:</span>
<span className="text-2xl font-semibold text-blue-600">
{formatCurrency(calculateTotalAmount(formValues?.sections))}
</span>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600">税率:</span>
<div style={{ width: '150px' }}>
<Input
value={taxRate}
suffix="%"
onChange={(e) => {
const value = e.target.value.replace(/[^\d]/g, '');
setTaxRate(Number(value) || 0);
}}
/>
</div>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600 font-medium">税后总计:</span>
<span className="text-xl font-semibold text-blue-600">
{formatCurrency(afterTaxAmount)}
</span>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600">折扣价:</span>
<div style={{ width: '150px' }}>
<Input
value={discount}
prefix={CURRENCY_SYMBOLS[currentCurrency]}
onChange={(e) => {
const value = e.target.value.replace(/[^\d]/g, '');
setDiscount(Number(value) || 0);
}}
/>
</div>
</div>
<div className="flex justify-end items-center space-x-4">
<span className="text-gray-600 font-medium">最终金额:</span>
<span className="text-2xl font-semibold text-blue-600">
{formatCurrency(discount || afterTaxAmount)}
</span>
</div>
</div>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-purple-500 rounded-full" />
<span>补充说明</span>
</span>
}
bordered={false}
>
<Form.Item name="description" className="mb-0">
<TextArea
rows={4}
placeholder="请输入补充说明信息"
className="rounded-md hover:border-purple-400 focus:border-purple-500"
/>
</Form.Item>
</Card>
</>
)}
</Form.List>
<Card
className="shadow-sm rounded-lg"
type="inner"
title={
<span className="flex items-center space-x-2 text-gray-700">
<span className="w-1 h-4 bg-blue-500 rounded-full" />
<span>服务明细</span>
</span>
}
bordered={false}
>
<SectionList
type="quotation"
form={form}
isView={isView}
formValues={formValues}
currentCurrency={currentCurrency}
/>
</Card>
</Form>
</Card>
<Modal
title={
<h3 className="text-lg font-medium text-gray-900 dark:text-gray-100">
选择小节模版
</h3>
}
open={templateModalVisible}
onCancel={() => setTemplateModalVisible(false)}
footer={null}
width={800}
className="dark:bg-gray-800"
closeIcon={
<CloseOutlined className="text-gray-500 dark:text-gray-400" />
}
>
{renderTemplateModalContent()}
</Modal>
</div>
);
};