diff --git a/src/components/PrintView/index.jsx b/src/components/PrintView/index.jsx deleted file mode 100644 index cc2769a..0000000 --- a/src/components/PrintView/index.jsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { Table } from 'antd'; - -const PrintView = ({ data }) => { - const columns = [ - { - title: '序号', - dataIndex: 'index', - width: 60, - render: (_, __, index) => index + 1, - }, - { - title: '名称', - dataIndex: 'productName', - width: '40%', - }, - { - title: '数量', - dataIndex: 'quantity', - width: '20%', - align: 'right', - render: (value) => value?.toLocaleString('zh-CN', { minimumFractionDigits: 2 }), - }, - { - title: '单价', - dataIndex: 'price', - width: '20%', - align: 'right', - render: (value) => value?.toLocaleString('zh-CN', { minimumFractionDigits: 2 }), - }, - { - title: '小计', - width: '20%', - align: 'right', - render: (_, record) => ( - (record.quantity * record.price)?.toLocaleString('zh-CN', { minimumFractionDigits: 2 }) - ), - }, - ]; - - return ( -
-
-

{data.attributes.quataName}

-
报价单号:{data.id}
-
- -
-
- 客户公司 - {data.attributes.companyName} -
-
- 供应商 - {data.attributes.supplierName} -
-
- 报价日期 - - {new Date(data.created_at).toLocaleDateString('zh-CN')} - -
-
- 报价有效期 - - {new Date(Date.now() + 30*24*60*60*1000).toLocaleDateString('zh-CN')} - -
-
- -
-
报价明细
- index} - bordered - summary={() => ( - - - - 总计({data.attributes.currency}): - - - - {data.attributes.totalAmount?.toLocaleString('zh-CN', { - minimumFractionDigits: 2, - style: 'currency', - currency: data.attributes.currency - })} - - - - - )} - /> - - - {data.attributes.description && ( -
-
补充说明
-
{data.attributes.description}
-
- )} - -
-
注意事项:
-
    -
  • 本报价单有效期为30天
  • -
  • 最终解释权归本公司所有
  • -
  • 如有疑问请及时联系我们
  • -
-
- - ); -}; - -export default PrintView; \ No newline at end of file diff --git a/src/components/SectionList/index.jsx b/src/components/SectionList/index.jsx index 7b26b56..97411f4 100644 --- a/src/components/SectionList/index.jsx +++ b/src/components/SectionList/index.jsx @@ -328,6 +328,49 @@ const SectionList = ({ ); }); + const addSectionFn = async (field) => { + try { + await form.validateFields(['sections', field.name]); + + const allValues = form.getFieldsValue(true); + const currentSection = allValues.sections[field.name]; + + if (!currentSection) { + throw new Error('未找到section数据'); + } + + const templateData = { + type: 'sections', + attributes: { + name: currentSection.sectionName, + template_type: type, + items: currentSection.items.map(item => ({ + name: item.name, + description: item.description, + quantity: item.quantity, + price: item.price, + unit: item.unit + })) + }, + schema_version: 1 + }; + + console.log('准备保存的数据:', templateData); + + const { error } = await supabase + .from('resources') + .insert([templateData]); + + if (error) throw error; + message.success('保存模板成功'); + + fetchAvailableSections(); + + } catch (error) { + message.error('保存模板失败'); + console.error(error); + } + }; return ( <> @@ -400,12 +443,21 @@ const SectionList = ({ )} {!isView && ( - + + , , ]} width={900} - className="template-modal dark:bg-gray-800" + className="template-modal" > {loading ? (
@@ -405,58 +408,62 @@ const QuotationPage = () => { ) : templates.length === 0 ? ( ) : ( -
+
{getTemplatesByCategory().map((group, groupIndex) => ( -
-
-

+
+
+

{group.name} - + ({group.templates.length})

-
+
{group.templates.map((template) => (
handleTemplateSelect(template.id)} className={` - relative p-4 rounded-xl cursor-pointer transition-all duration-200 + relative p-6 rounded-xl cursor-pointer transition-all duration-200 ${ selectedTemplateId === template.id - ? "ring-2 ring-blue-500 bg-blue-50/40 dark:bg-blue-900/40" - : "hover:bg-gray-50 dark:hover:bg-gray-700/50 border border-gray-200 dark:border-gray-700 shadow-sm hover:shadow-md" + ? "ring-2 ring-blue-500 bg-blue-50/40" + : "hover:shadow-lg border border-gray-200 hover:border-blue-200" } - dark:bg-gray-800 + transform hover:-translate-y-1 `} > -
+
-

+

{template.attributes.templateName}

-

+

{template.attributes.description || "暂无描述"}

-
- ¥{template.attributes.totalAmount?.toLocaleString()} -
-
+
{template.attributes.sections.map((section, index) => (
- + {section.sectionName} - + {section.items.length}项
@@ -465,10 +472,10 @@ const QuotationPage = () => {
{selectedTemplateId === template.id && ( -
-
+
+
diff --git a/src/pages/company/quotation/view/index.jsx b/src/pages/company/quotation/view/index.jsx index df84693..333b4bf 100644 --- a/src/pages/company/quotation/view/index.jsx +++ b/src/pages/company/quotation/view/index.jsx @@ -143,13 +143,11 @@ const QuotationPreview = () => { } >
- {/* 报价单标题 */}
{attributes.quataName} 创建日期:{new Date(quotation.created_at).toLocaleDateString()}
- {/* 基本信息 */}
基本信息
@@ -168,12 +166,11 @@ const QuotationPreview = () => {
- {/* 报价明细 */} {attributes.sections?.map((section, sIndex) => (
-
+
- {section.sectionName} +

{section.sectionName}

diff --git a/src/pages/company/service/detail/components/TaskTemplate.jsx b/src/pages/company/service/detail/components/TaskTemplate.jsx index f3b5578..c6a9b38 100644 --- a/src/pages/company/service/detail/components/TaskTemplate.jsx +++ b/src/pages/company/service/detail/components/TaskTemplate.jsx @@ -11,9 +11,7 @@ const TaskTemplate = ({ id, isView, onCancel,isEdit }) => { sections: [{ items: [{}] }], }); const [categories, setCategories] = useState([]); - useEffect(() => { - if (id) { fetchServiceTemplate(); } diff --git a/src/pages/company/service/index.jsx b/src/pages/company/service/index.jsx index edd8bb5..1048e2f 100644 --- a/src/pages/company/service/index.jsx +++ b/src/pages/company/service/index.jsx @@ -11,6 +11,8 @@ import { Divider, Input, InputNumber, + Select, + DatePicker, } from "antd"; import { PlusOutlined, @@ -26,6 +28,9 @@ import { import { useNavigate } from "react-router-dom"; import { supabaseService } from "@/hooks/supabaseService"; import TemplateTypeModal from "@/components/TemplateTypeModal"; +import { useCallback } from "react"; +import { useMemo } from "react"; +import dayjs from "dayjs"; const ServicePage = () => { const [loading, setLoading] = useState(false); @@ -34,9 +39,14 @@ const ServicePage = () => { const [editingKey, setEditingKey] = useState(""); const [isModalOpen, setIsModalOpen] = useState(false); const [editingItem, setEditingItem] = useState(null); + const [loadingUnits, setloadingUnits] = useState(false); const navigate = useNavigate(); - - // 模板类型配置 + const [units, setUnits] = useState([]); + const unitsFilter = useMemo(() => (type) => + units.filter(v => + v.attributes.template_type === type || v.attributes.template_type === "common" + ) + , [units]); const TEMPLATE_TYPES = { quotation: { label: "报价单模板", @@ -78,17 +88,38 @@ const ServicePage = () => { setData(services || []); } catch (error) { console.error("获取服务模板失败:", error); - message.error("获取服务模板失败"); + message.error("获取服���模板失败"); } finally { setLoading(false); } }; - + useEffect(() => { + fetchUnits(); + }, []); useEffect(() => { fetchServices(); }, [selectedType]); - // 删除服务模板 + const fetchUnits = async () => { + setloadingUnits(true); + try { + const { data: units } = await supabaseService.select("resources", { + filter: { + type: { eq: "units" }, + }, + order: { + column: "created_at", + ascending: false, + }, + }); + setUnits(units || []); + } catch (error) { + message.error("获取单位列表失败"); + console.error(error); + } finally { + setloadingUnits(false); + } + }; const handleDeleteService = async (serviceId) => { try { await supabaseService.delete("resources", { id: serviceId }); @@ -224,301 +255,420 @@ const ServicePage = () => { } }; - const table_warp=(record,section)=>{ - - switch(record.attributes.template_type){ - case 'quotation': -return [ - { - title: "名称", - dataIndex: "name", - key: "name", - width: "15%", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - name: e.target.value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "状态", - dataIndex: "unit", - key: "unit", - width: "10%", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - unit: e.target.value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "单价", - dataIndex: "price", - key: "price", - width: "10%", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - price: value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "数量", - dataIndex: "quantity", - key: "quantity", - width: "10%", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - quantity: value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "描述", - dataIndex: "description", - key: "description", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - description: e.target.value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "操作", - key: "action", - width: 150, - render: (_, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return ( - - {isEditing ? ( - <> - handleSave(record, section.key, index)} - /> - { - setEditingKey(""); - setEditingItem(null); - }} - /> - - ) : ( - <> - { - setEditingKey(`${record.id}-${section.key}-${index}`); - setEditingItem(item); // 初始化编辑项 - }} - /> - handleDeleteItem(record, section.key, index)} - > - - - - )} - - ); - }, - }, -] -case 'task': - return [ - { - title: "名称", - dataIndex: "name", - key: "name", - width: "15%", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - name: e.target.value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "执行状态", - dataIndex: "unit", - key: "unit", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - unit: e.target.value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - - { - title: "开始时间", - dataIndex: "timeRange", - key: "startTime", - render: (timeRange) => timeRange?.[0] || '-', - }, - { - title: "结束时间", - dataIndex: "timeRange", - key: "endTime", - render: (timeRange) => timeRange?.[1] || '-', - }, - { - title: "描述", - dataIndex: "description", - key: "description", - render: (text, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return isEditing ? ( - { - setEditingItem((prev) => ({ - ...prev || item, - description: e.target.value - })); - }} - /> - ) : ( - {text} - ); - }, - }, - { - title: "操作", - key: "action", - render: (_, item, index) => { - const isEditing = editingKey === `${record.id}-${section.key}-${index}`; - return ( - - {isEditing ? ( - <> - handleSave(record, section.key, index)} - /> - { - setEditingKey(""); - setEditingItem(null); + const table_warp = (record, section) => { + switch (record.attributes.template_type) { + case "quotation": + return [ + { + title: "名称", + dataIndex: "name", + key: "name", + width: "15%", + render: (text, item, index) => { + const isEditing = + editingKey === `${record.id}-${section.key}-${index}`; + return isEditing ? ( + { + setEditingItem((prev) => ({ + ...(prev || item), + name: e.target.value, + })); }} /> - - ) : ( - <> - {/* { - setEditingKey(`${record.id}-${section.key}-${index}`); - setEditingItem(item); // 初始化编辑项 + ) : ( + {text} + ); + }, + }, + { + title: "单位", + dataIndex: "unit", + key: "unit", + width: "10%", + render: (text, item, index) => { + const isEditing = + editingKey === `${record.id}-${section.key}-${index}`; + return isEditing ? ( + { + setEditingItem((prev) => ({ + ...(prev || item), + description: e.target.value, + })); + }} + /> + ) : ( + {text} + ); + }, + }, + { + title: "操作", + key: "action", + width: 150, + render: (_, item, index) => { + const isEditing = + editingKey === `${record.id}-${section.key}-${index}`; + return ( + + {isEditing ? ( + <> + handleSave(record, section.key, index)} + /> + { + setEditingKey(""); + setEditingItem(null); + }} + /> + + ) : ( + <> + { + setEditingKey(`${record.id}-${section.key}-${index}`); + setEditingItem(item); // 初始化编辑项 + }} + /> + + handleDeleteItem(record, section.key, index) + } + > + + + + )} + + ); + }, + }, + ]; + case "task": + return [ + { + title: "名称", + dataIndex: "name", + key: "name", + width: "15%", + render: (text, item, index) => { + const isEditing = + editingKey === `${record.id}-${section.key}-${index}`; + return isEditing ? ( + { + setEditingItem((prev) => ({ + ...(prev || item), + name: e.target.value, + })); + }} + /> + ) : ( + {text} + ); + }, + }, + { + title: "执行状态", + dataIndex: "unit", + key: "unit", + render: (text, item, index) => { + const isEditing = + editingKey === `${record.id}-${section.key}-${index}`; + return isEditing ? ( + { + setEditingItem((prev) => ({ + ...(prev || item), + description: e.target.value, + })); + }} + /> + ) : ( + {text} + ); + }, + }, + { + title: "操作", + key: "action", + render: (_, item, index) => { + const isEditing = + editingKey === `${record.id}-${section.key}-${index}`; + return ( + + {isEditing ? ( + <> + handleSave(record, section.key, index)} + /> + { + setEditingKey(""); + setEditingItem(null); + }} + /> + + ) : ( + <> + { + setEditingKey(`${record.id}-${section.key}-${index}`); + setEditingItem(item); // 初始化编辑项 + }} + /> + + handleDeleteItem(record, section.key, index) + } + > + + + + )} + + ); + }, + }, + ]; + case "project": + default: + []; + } + }; const expandedRowRender = (record) => { - return (
{record.attributes.sections.map((section) => (
-

{section.sectionName}

+

+ {section.sectionName} +

handleDeleteSection(record, section.key)} @@ -527,12 +677,12 @@ case 'task':

))} @@ -708,7 +858,7 @@ case 'task':
{ - const [data, setData] = useState([]); - const [loading, setLoading] = useState(false); - const [editingKey, setEditingKey] = useState(''); - const [form] = Form.useForm(); -const [loadingUnits,setLoadingUnits]=useState(false) -const [units,setUnit]=useState([]) -const [formValues, setFormValues] = useState({}); - const fetchSections = async () => { - setLoading(true); - try { - - const { data: sections } = await supabaseService.select('resources', { - filter: { - 'type': { eq: 'sections' }, - 'attributes->>template_type': {eq:activeType} - }, - order: { - column: 'created_at', - ascending: false - } - }); - - setData(sections || []); - } catch (error) { - message.error('获取模块数据失败'); - console.error(error); - } finally { - setLoading(false); +export default function SectionComponent({ activeType }) { + const renderFn = (type) => { + switch (type) { + case "quotation": + return ; + case "task": + return ; + default: + return
; } }; - const fetchUnits = async () => { - setLoadingUnits(true); - try { - const { data: units } = await supabaseService.select("resources", { - filter: { - type: { eq: "units" }, - "attributes->>template_type": { in: `(${activeType},common)` }, - }, - order: { - column: "created_at", - ascending: false, - }, - }); - setUnit(units || []); - } catch (error) { - message.error("获取单位列表失败"); - console.error(error); - } finally { - setLoadingUnits(false); - } - }; - useEffect(() => { - fetchSections(); - - }, [activeType]); - - const handleAdd = () => { - const newData = { - id: Date.now().toString(), - attributes: { - name: '', - template_type: activeType, - items: [{ - key: uuidv4(), - name: '', - description: '', - quantity: 1, - price: 0, - unit: '' - }] - }, - isNew: true - }; - setData([newData, ...data]); - setEditingKey(newData.id); - form.setFieldsValue(newData.attributes); - }; - - const handleSave = async (record) => { - try { - const values = await form.validateFields(); - const items = form.getFieldValue(['items']) || []; - - // 验证items数组 - if (!items.length || !items.some(item => item.name)) { - message.error('请至少添加一个有效的服务项目'); - return; - } - - if (record.isNew) { - await supabaseService.insert('resources', { - type: 'sections', - attributes: { - name: values.name, - template_type: activeType, - items: items.filter(item => item.name), // 只保存有名称的项目 - }, - schema_version: 1 - }); - } else { - await supabaseService.update('resources', - { id: record.id }, - { - attributes: { - name: values.name, - template_type: activeType, - items: items.filter(item => item.name), - }, - updated_at: new Date().toISOString() - } - ); - } - - message.success('保存成功'); - setEditingKey(''); - fetchSections(); - } catch (error) { - message.error('保存失败'); - console.error(error); - } - }; - - const handleDelete = async (record) => { - try { - await supabaseService.delete('resources', { id: record.id }); - message.success('删除成功'); - fetchSections(); - } catch (error) { - message.error('删除失败'); - console.error(error); - } - }; - const handleAddUnit = async (unitName) => { - try { - const { error } = await supabase.from("resources").insert([ - { - type: "units", - attributes: { - name: unitName, - template_type: activeType, - }, - schema_version: 1, - }, - ]); - - if (error) throw error; - message.success("新增单位成功"); - fetchUnits(); - return true; - } catch (error) { - message.error("新增单位失败"); - console.error(error); - return false; - } - }; - const handleValuesChange = (changedValues, allValues) => { - setFormValues(allValues); - }; - const columns = [ - { - title: '模块名称', - dataIndex: ['attributes', 'name'], - width: 200, - render: (text, record) => { - const isEditing = record.id === editingKey; - return isEditing ? ( - - - - ) : ( - {text} - ); - }, - }, - { - title: '服务项目', - dataIndex: ['attributes', 'items'], - render: (items, record) => { - const isEditing = record.id === editingKey; - if (isEditing) { - return ( - - {(fields, { add, remove }) => ( -
- {fields.map((field, index) => { - const items = formValues.items || []; - const currentItem = items[field.name] || {}; - const subtotal = (currentItem.quantity || 0) * (currentItem.price || 0); - - return ( - -
- - - - -
`共 ${total} 条`, - className: "px-4" - }} - className="rounded-lg" - /> - - - - ); -}; - -export default SectionsManagement; \ No newline at end of file + return <>{renderFn(activeType)}; +} \ No newline at end of file diff --git a/src/pages/company/service/itemsManange/sections/quotation.jsx b/src/pages/company/service/itemsManange/sections/quotation.jsx new file mode 100644 index 0000000..19e4ca4 --- /dev/null +++ b/src/pages/company/service/itemsManange/sections/quotation.jsx @@ -0,0 +1,465 @@ +import React, { useState, useEffect } from 'react'; +import { + Table, + Button, + Form, + Input, + Space, + message, + Popconfirm, + Select, + Divider, + InputNumber, + Card, + Typography +} from 'antd'; +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; +import { supabaseService } from '@/hooks/supabaseService'; +import { v4 as uuidv4 } from 'uuid'; + +const { Text } = Typography; +const TYPE = 'quotation' +const SectionsManagement = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [editingKey, setEditingKey] = useState(''); + const [form] = Form.useForm(); +const [loadingUnits,setLoadingUnits]=useState(false) +const [units,setUnit]=useState([]) +const [formValues, setFormValues] = useState({}); + const fetchSections = async () => { + setLoading(true); + try { + + const { data: sections } = await supabaseService.select('resources', { + filter: { + 'type': { eq: 'sections' }, + 'attributes->>template_type': {eq:TYPE} + }, + order: { + column: 'created_at', + ascending: false + } + }); + + setData(sections || []); + } catch (error) { + message.error('获取模块数据失败'); + console.error(error); + } finally { + setLoading(false); + } + }; + const fetchUnits = async () => { + setLoadingUnits(true); + try { + const { data: units } = await supabaseService.select("resources", { + filter: { + type: { eq: "units" }, + "attributes->>template_type": { in: `(${TYPE},common)` }, + }, + order: { + column: "created_at", + ascending: false, + }, + }); + setUnit(units || []); + } catch (error) { + message.error("获取单位列表失败"); + console.error(error); + } finally { + setLoadingUnits(false); + } + }; + useEffect(() => { + fetchSections(); + + }, [TYPE]); + + const handleAdd = () => { + const newData = { + id: Date.now().toString(), + attributes: { + name: '', + template_type: TYPE, + items: [{ + key: uuidv4(), + name: '', + description: '', + quantity: 1, + price: 0, + unit: '' + }] + }, + isNew: true + }; + setData([newData, ...data]); + setEditingKey(newData.id); + form.setFieldsValue(newData.attributes); + }; + + const handleSave = async (record) => { + try { + const values = await form.validateFields(); + const items = form.getFieldValue(['items']) || []; + + // 验证items数组 + if (!items.length || !items.some(item => item.name)) { + message.error('请至少添加一个有效的服务项目'); + return; + } + + if (record.isNew) { + await supabaseService.insert('resources', { + type: 'sections', + attributes: { + name: values.name, + template_type: TYPE, + items: items.filter(item => item.name), // 只保存有名称的项目 + }, + schema_version: 1 + }); + } else { + await supabaseService.update('resources', + { id: record.id }, + { + attributes: { + name: values.name, + template_type: TYPE, + items: items.filter(item => item.name), + }, + updated_at: new Date().toISOString() + } + ); + } + + message.success('保存成功'); + setEditingKey(''); + fetchSections(); + } catch (error) { + message.error('保存失败'); + console.error(error); + } + }; + + const handleDelete = async (record) => { + try { + await supabaseService.delete('resources', { id: record.id }); + message.success('删除成功'); + fetchSections(); + } catch (error) { + message.error('删除失败'); + console.error(error); + } + }; + const handleAddUnit = async (unitName) => { + try { + const { error } = await supabase.from("resources").insert([ + { + type: "units", + attributes: { + name: unitName, + template_type: TYPE, + }, + schema_version: 1, + }, + ]); + + if (error) throw error; + message.success("新增单位成功"); + fetchUnits(); + return true; + } catch (error) { + message.error("新增单位失败"); + console.error(error); + return false; + } + }; + const handleValuesChange = (changedValues, allValues) => { + setFormValues(allValues); + }; + const columns = [ + { + title: '模块名称', + dataIndex: ['attributes', 'name'], + width: 200, + render: (text, record) => { + const isEditing = record.id === editingKey; + return isEditing ? ( + + + + ) : ( + {text} + ); + }, + }, + { + title: '服务项目', + dataIndex: ['attributes', 'items'], + render: (items, record) => { + const isEditing = record.id === editingKey; + if (isEditing) { + return ( + + {(fields, { add, remove }) => ( +
+ {fields.map((field, index) => { + const items = formValues.items || []; + const currentItem = items[field.name] || {}; + const subtotal = (currentItem.quantity || 0) * (currentItem.price || 0); + + return ( + +
+ + + + +
`共 ${total} 条`, + className: "px-4" + }} + className="rounded-lg" + /> + + + + ); +}; + +export default SectionsManagement; \ No newline at end of file diff --git a/src/pages/company/service/itemsManange/sections/task.jsx b/src/pages/company/service/itemsManange/sections/task.jsx new file mode 100644 index 0000000..4d2387f --- /dev/null +++ b/src/pages/company/service/itemsManange/sections/task.jsx @@ -0,0 +1,455 @@ +import React, { useState, useEffect } from 'react'; +import { + Table, + Button, + Form, + Input, + Space, + message, + Popconfirm, + Select, + Divider, + DatePicker, + Card, + Typography +} from 'antd'; +import { PlusOutlined, DeleteOutlined } from '@ant-design/icons'; +import { supabaseService } from '@/hooks/supabaseService'; +import { v4 as uuidv4 } from 'uuid'; +import dayjs from 'dayjs'; + +const { Text } = Typography; +const TYPE = 'task'; + +const TaskSections = () => { + const [data, setData] = useState([]); + const [loading, setLoading] = useState(false); + const [editingKey, setEditingKey] = useState(''); + const [form] = Form.useForm(); + const [loadingUnits, setLoadingUnits] = useState(false); + const [units, setUnit] = useState([]); + const [formValues, setFormValues] = useState({}); + + const fetchSections = async () => { + setLoading(true); + try { + const { data: sections } = await supabaseService.select('resources', { + filter: { + 'type': { eq: 'sections' }, + 'attributes->>template_type': { eq: TYPE } + }, + order: { + column: 'created_at', + ascending: false + } + }); + setData(sections || []); + } catch (error) { + message.error('获取模块数据失败'); + console.error(error); + } finally { + setLoading(false); + } + }; + + const fetchUnits = async () => { + setLoadingUnits(true); + try { + const { data: units } = await supabaseService.select("resources", { + filter: { + type: { eq: "units" }, + "attributes->>template_type": { in: `(${TYPE},common)` }, + }, + order: { + column: "created_at", + ascending: false, + }, + }); + setUnit(units || []); + } catch (error) { + message.error("获取状态列表失败"); + console.error(error); + } finally { + setLoadingUnits(false); + } + }; + + useEffect(() => { + fetchSections(); + fetchUnits(); + }, []); + + const handleAdd = () => { + const newData = { + id: Date.now().toString(), + attributes: { + name: '', + template_type: TYPE, + items: [{ + key: uuidv4(), + name: '', + description: '', + timeRange: null, + unit: '' + }] + }, + isNew: true + }; + setData([newData, ...data]); + setEditingKey(newData.id); + form.setFieldsValue(newData.attributes); + }; + + const handleSave = async (record) => { + try { + const values = await form.validateFields(); + const items = form.getFieldValue(['items']) || []; + + if (!items.length || !items.some(item => item.name)) { + message.error('请至少添加一个有效的任务项目'); + return; + } + + const formattedItems = items.filter(item => item.name).map(item => ({ + ...item, + timeRange: item.timeRange ? [ + dayjs(item.timeRange[0]).format('YYYY-MM-DD HH:mm'), + dayjs(item.timeRange[1]).format('YYYY-MM-DD HH:mm') + ] : null + })); + + if (record.isNew) { + await supabaseService.insert('resources', { + type: 'sections', + attributes: { + name: values.name, + template_type: TYPE, + items: formattedItems, + }, + schema_version: 1 + }); + } else { + await supabaseService.update('resources', + { id: record.id }, + { + attributes: { + name: values.name, + template_type: TYPE, + items: formattedItems, + }, + updated_at: new Date().toISOString() + } + ); + } + + message.success('保存成功'); + setEditingKey(''); + fetchSections(); + } catch (error) { + message.error('保存失败'); + console.error(error); + } + }; + + const handleDelete = async (record) => { + try { + await supabaseService.delete('resources', { id: record.id }); + message.success('删除成功'); + fetchSections(); + } catch (error) { + message.error('删除失败'); + console.error(error); + } + }; + + const handleAddUnit = async (unitName) => { + try { + await supabaseService.insert("resources", { + type: "units", + attributes: { + name: unitName, + template_type: TYPE, + }, + schema_version: 1, + }); + + message.success("新增状态成功"); + fetchUnits(); + return true; + } catch (error) { + message.error("新增状态失败"); + console.error(error); + return false; + } + }; + + const handleValuesChange = (changedValues, allValues) => { + setFormValues(allValues); + }; + + const columns = [ + { + title: '模块名称', + dataIndex: ['attributes', 'name'], + width: 200, + render: (text, record) => { + const isEditing = record.id === editingKey; + return isEditing ? ( + + + + ) : ( + {text} + ); + }, + }, + { + title: '任务项目', + dataIndex: ['attributes', 'items'], + render: (items, record) => { + const isEditing = record.id === editingKey; + if (isEditing) { + return ( + + {(fields, { add, remove }) => ( +
+ {fields.map((field, index) => ( + +
+ + + + + + + + + + + + +
+ +
`共 ${total} 条`, + className: "px-4" + }} + className="rounded-lg" + /> + + + + ); +}; + +export default TaskSections; diff --git a/src/pages/company/task/detail/index.jsx b/src/pages/company/task/detail/index.jsx new file mode 100644 index 0000000..a3adb6c --- /dev/null +++ b/src/pages/company/task/detail/index.jsx @@ -0,0 +1,420 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { + Form, + Input, + Select, + Button, + Space, + Card, + Typography, + message, + DatePicker, +} from "antd"; +import { + ArrowLeftOutlined, + SaveOutlined, +} from "@ant-design/icons"; +import { supabase } from "@/config/supabase"; +import { useNavigate, useParams, useSearchParams } from "react-router-dom"; +import { v4 as uuidv4 } from "uuid"; +import TaskList from '@/components/TaskList'; +import dayjs from 'dayjs'; +import {supabaseService} from '@/hooks/supabaseService' + +const { Title } = Typography; +const TYPE = "task"; + +export default function TaskForm() { + const { id } = useParams(); + const [searchParams] = useSearchParams(); + const isEdit = searchParams.get("edit") === "true"; + const templateId = searchParams.get("templateId"); + const isView = id && !isEdit; + const [form] = Form.useForm(); + const navigate = useNavigate(); + const [loading, setLoading] = useState(false); + const [customers, setCustomers] = useState([]); + const [formValues, setFormValues] = useState({}); + const [units, setUnits] = useState([]); + const [loadingUnits,setLoadingUnits]=useState(false) + const fetchUnits = async () => { + setLoadingUnits(true); + try { + const { data: units } = await supabaseService.select("resources", { + filter: { + type: { eq: "units" }, + "attributes->>template_type": { in: `(${TYPE})` }, + }, + order: { + column: "created_at", + ascending: false, + }, + }); + + setUnits(units || []); + } catch (error) { + message.error("获取单位列表失败"); + console.error(error); + } finally { + setLoadingUnits(false); + } + }; + + useEffect(() => { + fetchUnits(); + }, []); + const initialValues = { + sections: [ + { + key: uuidv4(), + sectionName: "任务类型 1", + items: [ + { + key: uuidv4(), + name: "", + description: "", + timeRange: null, + unit: "未开始", + }, + ], + }, + ], + status: "未开始", + timeRange: null, + }; + + // 处理表单值变化 + const handleValuesChange = (changedValues, allValues) => { + setFormValues(allValues); + }; + + // 获取任务详情 + const fetchTaskDetail = async () => { + try { + setLoading(true); + const { data, error } = await supabase + .from("resources") + .select("*") + .eq("id", id) + .single(); + + if (error) throw error; + + if (data?.attributes) { + const formData = { + taskName: data.attributes.taskName, + customers: data.attributes.customers.map((customer) => customer.id) || [], + description: data.attributes.description, + sections: data.attributes.sections.map((section) => ({ + key: uuidv4(), + sectionName: section.sectionName, + items: section.items.map((item) => ({ + key: uuidv4(), + name: item.name, + description: item.description || "", + timeRange: item.timeRange, + unit: item.unit || "未开始", + })), + })), + status: data.attributes.status || "未开始", + timeRange: data.attributes.timeRange + ? [dayjs(data.attributes.timeRange[0]), dayjs(data.attributes.timeRange[1])] + : null, + }; + + form.setFieldsValue(formData); + setFormValues(formData); + } + } catch (error) { + console.error("获取任务详情失败:", error); + message.error("获取任务详情失败"); + } finally { + setLoading(false); + } + }; + + // 获取模板数据 + const fetchTemplateData = async () => { + try { + setLoading(true); + const { data: template, error } = await supabase + .from("resources") + .select("*") + .eq("type", "serviceTemplate") + .eq("id", templateId) + .single(); + + if (error) throw error; + + if (template?.attributes) { + const taskData = { + taskName: template.attributes.templateName, + description: template.attributes.description, + sections: template.attributes.sections.map((section) => ({ + key: uuidv4(), + sectionName: section.sectionName, + items: section.items.map((item) => ({ + key: uuidv4(), + name: item.name, + description: item.description, + timeRange: null, + unit: "未开始", + })), + })), + }; + form.setFieldsValue(taskData); + setFormValues(taskData); + } + } catch (error) { + console.error("获取模板数据失败:", error); + message.error("获取模板数据失败"); + } finally { + setLoading(false); + } + }; + + // 获取客户列表 + const fetchCustomers = async () => { + try { + const { data, error } = await supabase + .from("resources") + .select("*") + .eq("type", "customer"); + + if (error) throw error; + setCustomers(data || []); + } catch (error) { + console.error("获取客户列表失败:", error); + } + }; + + // 保存任务 + const onFinish = async (values) => { + try { + setLoading(true); + const taskData = { + type: "task", + attributes: { + taskName: values.taskName, + customers: customers + .filter(customer => values.customers.includes(customer.id)) + .map(customer => ({ + id: customer.id, + name: customer.attributes.name + })), + description: values.description, + sections: values.sections.map((section) => ({ + sectionName: section.sectionName, + items: section.items.map((item) => ({ + name: item.name, + description: item.description, + timeRange: item.timeRange, + unit: item.unit, + })), + })), + status: values.status, + timeRange: values.timeRange + ? [ + values.timeRange[0].format('YYYY-MM-DD'), + values.timeRange[1].format('YYYY-MM-DD') + ] + : null, + }, + }; + + let result; + if (id) { + result = await supabase + .from("resources") + .update(taskData) + .eq("id", id) + .select(); + } else { + result = await supabase + .from("resources") + .insert([taskData]) + .select(); + } + + if (result.error) throw result.error; + + message.success("保存成功"); + navigate("/company/task"); + } catch (error) { + console.error("保存失败:", error); + message.error("保存失败"); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchCustomers(); + }, []); + + useEffect(() => { + if (id) { + fetchTaskDetail(); + } else if (templateId) { + fetchTemplateData(); + } else { + form.setFieldsValue(initialValues); + setFormValues(initialValues); + } + }, [id, templateId]); + + return ( +
+ +
+ + {id ? (isEdit ? "编辑任务" : "查看任务") : "新建任务"} + + + {id + ? isEdit + ? "请修改任务信息" + : "任务详情" + : "请填写任务信息"} + +
+ + + {!isView && ( + + )} + +
+ } + > + + + + 基本信息 + + } + bordered={false} + > +
+ 任务名称} + rules={[{ required: true, message: "请输入任务名称" }]} + > + + + + 客户名称} + rules={[{ required: true, message: "请选择至少一个客户" }]} + > + ({ + label: unit.attributes.name, + value: unit.attributes.name, + }))} + /> + + + 时间范围} + rules={[{ required: true, message: "请选择时间范围" }]} + > + { + if (dates) { + form.setFieldValue('timeRange', [ + dayjs(dates[0]), + dayjs(dates[1]) + ]); + } else { + form.setFieldValue('timeRange', null); + } + }} + /> + +
+
+ + + + 任务流程 + + } + bordered={false} + > + + + + + + ); +} diff --git a/src/pages/company/task/index.jsx b/src/pages/company/task/index.jsx index 2ea31ed..e3b0baf 100644 --- a/src/pages/company/task/index.jsx +++ b/src/pages/company/task/index.jsx @@ -1,44 +1,467 @@ -import React from 'react'; -import { Card, Table, Button } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; +import React, { useEffect, useState } from "react"; +import { + Card, + Table, + Button, + message, + Popconfirm, + Tag, + Space, + Spin, + Modal, + Empty, + Typography, +} from "antd"; +import { + PlusOutlined, + EditOutlined, + DeleteOutlined, + EyeOutlined, + CopyOutlined, + FileAddOutlined, + AppstoreOutlined, +} from "@ant-design/icons"; +import { useResources } from "@/hooks/resource/useResource"; +import { useNavigate } from "react-router-dom"; +import { supabase } from "@/config/supabase"; +import dayjs from 'dayjs'; const TaskPage = () => { + const navigate = useNavigate(); + const [pagination, setPagination] = useState({ current: 1, pageSize: 10 }); + const [sorter, setSorter] = useState({ + field: "created_at", + order: "descend", + }); + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedTemplateId, setSelectedTemplateId] = useState(null); + const [templates, setTemplates] = useState([]); + const [loading, setLoading] = useState(false); + + const { + resources: tasks, + loading: loadingTasks, + total, + fetchResources: fetchTasks, + deleteResource: deleteTask, + } = useResources(pagination, sorter, "task"); + + useEffect(() => { + fetchTasks(); + }, []); + const columns = [ { - title: '任务名称', - dataIndex: 'name', - key: 'name', + title: "任务名称", + dataIndex: ["attributes", "taskName"], + key: "taskName", + ellipsis: true, + className: "dark:text-gray-200", }, { - title: '负责人', - dataIndex: 'assignee', - key: 'assignee', + title: "开始时间", + dataIndex: ["attributes", "timeRange"], + key: "startTime", + render: (timeRange) => ( + + {timeRange?.[0] || '-'} + + ), }, { - title: '截止日期', - dataIndex: 'dueDate', - key: 'dueDate', + title: "结束时间", + dataIndex: ["attributes", "timeRange"], + key: "endTime", + render: (timeRange) => ( + + {timeRange?.[1] || '-'} + + ), }, { - title: '状态', - dataIndex: 'status', - key: 'status', + title: "状态", + dataIndex: ["attributes", "status"], + key: "status", + render: (status) => ( + + {status || '未设置'} + + ), + }, + { + title: "创建时间", + dataIndex: "created_at", + key: "created_at", + sorter: true, + render: (text) => ( + + {dayjs(text).format('YYYY-MM-DD HH:mm')} + + ), + }, + { + title: "操作", + key: "action", + fixed: "right", + render: (_, record) => ( + + + + + handleDelete(record.id)} + okText="确定" + cancelText="取消" + okButtonProps={{ danger: true }} + > + + + + ), }, ]; + // 获取模板列表 + const fetchTemplates = async () => { + try { + setLoading(true); + const { data: templates, error } = await supabase + .from("resources") + .select("*") + .eq("type", "serviceTemplate") + .eq("attributes->>template_type", "task") + .order("created_at", { ascending: false }); + + if (error) throw error; + setTemplates(templates); + } catch (error) { + console.error("获取任务模板失败:", error); + message.error("获取任务模板失败"); + } finally { + setLoading(false); + } + }; + + // 当模态框打开时获取模板 + useEffect(() => { + if (isModalVisible) { + fetchTemplates(); + } + }, [isModalVisible]); + + // 处理模板选择 + const handleTemplateSelect = (templateId) => { + setSelectedTemplateId(templateId); + }; + + // 处理确认选择 + const handleConfirm = () => { + if (selectedTemplateId) { + navigate(`/company/taskInfo?templateId=${selectedTemplateId}`); + } else { + navigate("/company/taskInfo"); + } + setIsModalVisible(false); + setSelectedTemplateId(null); + }; + + // 复制任务 + const copyItem = async (record) => { + try { + setLoading(true); + const newAttributes = JSON.parse(JSON.stringify(record.attributes)); + + // 修改任务名称,添加"副本"标识 + newAttributes.taskName = `${newAttributes.taskName} (副本)`; + + const { data, error } = await supabase + .from("resources") + .insert([ + { + type: "task", + attributes: newAttributes, + }, + ]); + + if (error) throw error; + + message.success("复制成功"); + fetchTasks(); // 刷新列表 + } catch (error) { + console.error("复制任务失败:", error); + message.error("复制失败:" + error.message); + } finally { + setLoading(false); + } + }; + + // 删除任务 + const handleDelete = async (id) => { + try { + await deleteTask(id); + fetchTasks(); + } catch (error) { + message.error("删除失败:" + error.message); + } + }; + + // 表格变化处理 + const handleTableChange = (pagination, filters, sorter) => { + setPagination(pagination); + setSorter({ + field: sorter.field || "created_at", + order: sorter.order || "descend", + }); + fetchTasks({ + current: pagination.current, + pageSize: pagination.pageSize, + field: sorter.field, + order: sorter.order, + }); + }; + + // 获取模板分类 + const getTemplatesByCategory = () => { + const groups = new Map(); + + // 添加未分类组 + groups.set("uncategorized", { + name: "未分类", + templates: templates.filter( + (t) => !t.attributes.category || t.attributes.category.length === 0 + ), + }); + + // 按分类分组 + templates.forEach((template) => { + if (template.attributes.category) { + template.attributes.category.forEach((cat) => { + if (!groups.has(cat.id)) { + groups.set(cat.id, { + name: cat.name, + templates: [], + }); + } + groups.get(cat.id).templates.push(template); + }); + } + }); + + // 返回非空分组 + return Array.from(groups.values()).filter( + (group) => group.templates.length > 0 + ); + }; + return ( - - - - } - > -
- + } + > +
`共 ${total} 条记录`, + }} + /> + + + {/* 模板选择弹窗 */} + + 选择任务模板 + + } + open={isModalVisible} + onCancel={() => { + setIsModalVisible(false); + setSelectedTemplateId(null); + }} + footer={[ + , + , + ]} + width={900} + className="dark:bg-gray-800" + > + {loading ? ( +
+ +
+ ) : templates.length === 0 ? ( + 暂无可用模板 + } /> + ) : ( +
+ {getTemplatesByCategory().map((group, groupIndex) => ( +
+
+

+ {group.name} + + ({group.templates.length}) + +

+
+ +
+ {group.templates.map((template) => ( +
handleTemplateSelect(template.id)} + className={` + relative p-6 rounded-xl cursor-pointer transition-all duration-200 + ${ + selectedTemplateId === template.id + ? "ring-2 ring-blue-500 bg-blue-50/40 dark:bg-blue-900/40" + : "hover:shadow-lg border border-gray-200 dark:border-gray-700 hover:border-blue-200 dark:hover:border-blue-700" + } + transform hover:-translate-y-1 dark:bg-gray-800 + `} + > +
+
+

+ {template.attributes.templateName} +

+

+ {template.attributes.description || "暂无描述"} +

+
+
+ +
+ {template.attributes.sections.map((section, index) => ( +
+
+ + {section.sectionName} + + + {section.items.length}项 + +
+
+ ))} +
+ + {selectedTemplateId === template.id && ( +
+
+ + + +
+
+ )} +
+ ))} +
+
+ ))} +
+ )} +
+ ); }; diff --git a/src/pages/marketing/communication/send/index.jsx b/src/pages/marketing/communication/send/index.jsx deleted file mode 100644 index b5f461f..0000000 --- a/src/pages/marketing/communication/send/index.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import { Card, Steps, Form, Button, Select, DatePicker, Input } from 'antd'; - -const { Step } = Steps; -const { Option } = Select; - -const CommunicationSend = () => { - const [form] = Form.useForm(); - - return ( - - - -
- - - - - - - - - - - - - - - - -
- ); -}; - -export default CommunicationSend; \ No newline at end of file diff --git a/src/pages/marketing/communication/tasks/index.jsx b/src/pages/marketing/communication/tasks/index.jsx deleted file mode 100644 index 18bfb0d..0000000 --- a/src/pages/marketing/communication/tasks/index.jsx +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import { Card, Table, Button, Tag } from 'antd'; -import { PlusOutlined } from '@ant-design/icons'; - -const CommunicationTasks = () => { - const columns = [ - { - title: '任务名称', - dataIndex: 'name', - key: 'name', - }, - { - title: '负责人', - dataIndex: 'assignee', - key: 'assignee', - }, - { - title: '截止日期', - dataIndex: 'dueDate', - key: 'dueDate', - }, - { - title: '状态', - dataIndex: 'status', - key: 'status', - render: status => ( - - {status} - - ), - }, - ]; - - return ( - - - - } - > -
- - ); -}; - -export default CommunicationTasks; \ No newline at end of file diff --git a/src/routes/routes.js b/src/routes/routes.js index b16b4c1..0fb34f9 100644 --- a/src/routes/routes.js +++ b/src/routes/routes.js @@ -35,13 +35,7 @@ const companyRoutes = [ icon: "file", roles: ["ADMIN", "OWNER"], }, - { - path: "task", - component: lazy(() => import("@/pages/resource/resourceTask")), - name: "任务管理", - icon: "appstore", - roles: ["OWNER"], - }, + { path: "quotaInfo/:id?", // 添加可选的 id 参数 hidden: true, @@ -49,6 +43,21 @@ const companyRoutes = [ name: "报价单详情", icon: "file", roles: ["ADMIN", "OWNER"], + }, + { + path: "task", + component: lazy(() => import("@/pages/company/task")), + name: "任务管理", + icon: "appstore", + roles: ["OWNER"], + }, + { + path: "taskInfo/:id?", + hidden:true, + component: lazy(() => import("@/pages/company/task/detail")), + name: "任务管理详情", + icon: "appstore", + roles: ["OWNER"], }, { path: "serviceTemplate", @@ -149,14 +158,14 @@ export const generateRoutes = (role) => { children: companyRoutes.filter((route) => route.roles.includes(role)), roles: ["ADMIN", "OWNER"], }, - { - path: "marketing", - component: lazy(() => import("@/pages/marketing")), - name: "行销中心", - icon: "shopping", - children: marketingRoutes.filter((route) => route.roles.includes(role)), - roles: ["ADMIN", "OWNER"], - }, + // { + // path: "marketing", + // component: lazy(() => import("@/pages/marketing")), + // name: "行销中心", + // icon: "shopping", + // children: marketingRoutes.filter((route) => route.roles.includes(role)), + // roles: ["ADMIN", "OWNER"], + // }, // { // path: "role",