|
@@ -0,0 +1,904 @@
|
|
|
|
|
+import React, { useState, useEffect } from 'react';
|
|
|
|
|
+import { Button, Table, Space, Modal, Form, Input, Select, message, List, Avatar, Progress, Tag, Timeline, DatePicker, Switch, Dropdown, Menu } from 'antd';
|
|
|
|
|
+import type { MenuProps } from 'antd';
|
|
|
|
|
+import { CloseOutlined } from '@ant-design/icons';
|
|
|
|
|
+import dayjs from 'dayjs';
|
|
|
|
|
+import { WorkOrderAPI } from './api/work_orders.ts';
|
|
|
|
|
+import { DeviceInstanceAPI } from './api/device_instance.ts';
|
|
|
|
|
+import { Uploader } from './components_uploader.tsx';
|
|
|
|
|
+import { WorkOrderPriority, WorkOrderStatus } from '../../client/share/monitorTypes.ts';
|
|
|
|
|
+import type { WorkOrder, WorkOrderSettings, DeadlineInfo } from '../../client/share/monitorTypes.ts';
|
|
|
|
|
+
|
|
|
|
|
+const { Column } = Table;
|
|
|
|
|
+const { Option } = Select;
|
|
|
|
|
+const { TextArea } = Input;
|
|
|
|
|
+
|
|
|
|
|
+export function WorkOrdersPage() {
|
|
|
|
|
+ const [workOrders, setWorkOrders] = useState<WorkOrder[]>([]);
|
|
|
|
|
+ const [settings, setSettings] = useState<WorkOrderSettings>();
|
|
|
|
|
+ const [loading, setLoading] = useState(false);
|
|
|
|
|
+ const [modalVisible, setModalVisible] = useState(false);
|
|
|
|
|
+ const [currentOrder, setCurrentOrder] = useState<Partial<WorkOrder>>();
|
|
|
|
|
+ const [categories, setCategories] = useState<string[]>([]);
|
|
|
|
|
+ const [devices, setDevices] = useState<any[]>([]);
|
|
|
|
|
+ const [attachments, setAttachments] = useState<any[]>([]);
|
|
|
|
|
+ const [comments, setComments] = useState<any[]>([]);
|
|
|
|
|
+ const [commentContent, setCommentContent] = useState('');
|
|
|
|
|
+ const [form] = Form.useForm();
|
|
|
|
|
+ const [historyVisible, setHistoryVisible] = useState(false);
|
|
|
|
|
+ const [statusHistory, setStatusHistory] = useState<any[]>([]);
|
|
|
|
|
+ const [deadlineInfo, setDeadlineInfo] = useState<DeadlineInfo>();
|
|
|
|
|
+ const [autoDispatchVisible, setAutoDispatchVisible] = useState(false);
|
|
|
|
|
+ const [autoDispatchForm] = Form.useForm();
|
|
|
|
|
+ const [detailModalVisible, setDetailModalVisible] = useState(false);
|
|
|
|
|
+ const [currentDetail, setCurrentDetail] = useState('');
|
|
|
|
|
+
|
|
|
|
|
+ useEffect(() => {
|
|
|
|
|
+ // 开发环境下生成模拟数据
|
|
|
|
|
+ if (process.env.NODE_ENV === 'development') {
|
|
|
|
|
+ const mockOrders = [
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'mock-1',
|
|
|
|
|
+ title: '设备网络故障',
|
|
|
|
|
+ order_no: `WO-${dayjs().format('YYYYMMDD')}-1001`,
|
|
|
|
|
+ device_name: '网络交换机1',
|
|
|
|
|
+ problem_desc: '设备无法连接网络',
|
|
|
|
|
+ priority: WorkOrderPriority.URGENT,
|
|
|
|
|
+ creator_id: 'system',
|
|
|
|
|
+ creator_name: '系统管理员',
|
|
|
|
|
+ deadline: dayjs().add(1, 'day').toISOString(),
|
|
|
|
|
+ created_at: dayjs().toISOString(),
|
|
|
|
|
+ updated_at: dayjs().toISOString(),
|
|
|
|
|
+ problem_type: '网络',
|
|
|
|
|
+ status: WorkOrderStatus.PENDING,
|
|
|
|
|
+ feedback: '',
|
|
|
|
|
+ attachments: []
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'mock-2',
|
|
|
|
|
+ title: '服务器硬件故障',
|
|
|
|
|
+ order_no: `WO-${dayjs().format('YYYYMMDD')}-1002`,
|
|
|
|
|
+ device_name: '服务器A',
|
|
|
|
|
+ problem_desc: '硬盘故障需要更换',
|
|
|
|
|
+ priority: WorkOrderPriority.IMPORTANT,
|
|
|
|
|
+ creator_id: 'system',
|
|
|
|
|
+ creator_name: '系统管理员',
|
|
|
|
|
+ deadline: dayjs().add(2, 'day').toISOString(),
|
|
|
|
|
+ created_at: dayjs().toISOString(),
|
|
|
|
|
+ updated_at: dayjs().toISOString(),
|
|
|
|
|
+ problem_type: '硬件',
|
|
|
|
|
+ status: WorkOrderStatus.PROCESSING,
|
|
|
|
|
+ feedback: '已订购新硬盘',
|
|
|
|
|
+ attachments: []
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'mock-3',
|
|
|
|
|
+ title: '软件系统升级',
|
|
|
|
|
+ order_no: `WO-${dayjs().format('YYYYMMDD')}-1003`,
|
|
|
|
|
+ device_name: '办公电脑',
|
|
|
|
|
+ problem_desc: '需要升级到最新版本',
|
|
|
|
|
+ priority: WorkOrderPriority.NORMAL,
|
|
|
|
|
+ creator_id: 'system',
|
|
|
|
|
+ creator_name: '系统管理员',
|
|
|
|
|
+ deadline: dayjs().add(3, 'day').toISOString(),
|
|
|
|
|
+ created_at: dayjs().toISOString(),
|
|
|
|
|
+ updated_at: dayjs().toISOString(),
|
|
|
|
|
+ problem_type: '软件',
|
|
|
|
|
+ status: WorkOrderStatus.CLOSED,
|
|
|
|
|
+ feedback: '已完成升级',
|
|
|
|
|
+ attachments: []
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'mock-4',
|
|
|
|
|
+ title: '打印机维护',
|
|
|
|
|
+ order_no: `WO-${dayjs().format('YYYYMMDD')}-1004`,
|
|
|
|
|
+ device_name: '办公室打印机',
|
|
|
|
|
+ problem_desc: '定期维护保养',
|
|
|
|
|
+ priority: WorkOrderPriority.NORMAL,
|
|
|
|
|
+ creator_id: 'system',
|
|
|
|
|
+ creator_name: '系统管理员',
|
|
|
|
|
+ deadline: dayjs().add(4, 'day').toISOString(),
|
|
|
|
|
+ created_at: dayjs().toISOString(),
|
|
|
|
|
+ updated_at: dayjs().toISOString(),
|
|
|
|
|
+ problem_type: '其他',
|
|
|
|
|
+ status: WorkOrderStatus.PENDING,
|
|
|
|
|
+ feedback: '',
|
|
|
|
|
+ attachments: []
|
|
|
|
|
+ },
|
|
|
|
|
+ {
|
|
|
|
|
+ id: 'mock-5',
|
|
|
|
|
+ title: '数据库优化',
|
|
|
|
|
+ order_no: `WO-${dayjs().format('YYYYMMDD')}-1005`,
|
|
|
|
|
+ device_name: '数据库服务器',
|
|
|
|
|
+ problem_desc: '查询性能优化',
|
|
|
|
|
+ priority: WorkOrderPriority.IMPORTANT,
|
|
|
|
|
+ creator_id: 'system',
|
|
|
|
|
+ creator_name: '系统管理员',
|
|
|
|
|
+ deadline: dayjs().add(5, 'day').toISOString(),
|
|
|
|
|
+ created_at: dayjs().toISOString(),
|
|
|
|
|
+ updated_at: dayjs().toISOString(),
|
|
|
|
|
+ problem_type: '软件',
|
|
|
|
|
+ status: WorkOrderStatus.PROCESSING,
|
|
|
|
|
+ feedback: '正在优化索引',
|
|
|
|
|
+ attachments: []
|
|
|
|
|
+ }
|
|
|
|
|
+ ];
|
|
|
|
|
+ setWorkOrders(mockOrders);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ }
|
|
|
|
|
+ fetchSettings();
|
|
|
|
|
+ fetchCategories();
|
|
|
|
|
+ fetchDevices();
|
|
|
|
|
+ }, []);
|
|
|
|
|
+
|
|
|
|
|
+ const [searchParams, setSearchParams] = useState({
|
|
|
|
|
+ status: undefined,
|
|
|
|
|
+ problemType: undefined,
|
|
|
|
|
+ keyword: undefined,
|
|
|
|
|
+ startDate: undefined,
|
|
|
|
|
+ endDate: undefined
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ const fetchData = async () => {
|
|
|
|
|
+ setLoading(true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await WorkOrderAPI.getList(searchParams);
|
|
|
|
|
+ setWorkOrders(result.data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取工单列表失败');
|
|
|
|
|
+ } finally {
|
|
|
|
|
+ setLoading(false);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSearch = (values: any) => {
|
|
|
|
|
+ setSearchParams({
|
|
|
|
|
+ status: values.status,
|
|
|
|
|
+ problemType: values.problemType,
|
|
|
|
|
+ keyword: values.keyword,
|
|
|
|
|
+ startDate: values.dateRange?.[0]?.toISOString(),
|
|
|
|
|
+ endDate: values.dateRange?.[1]?.toISOString()
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleReset = () => {
|
|
|
|
|
+ setSearchParams({
|
|
|
|
|
+ status: undefined,
|
|
|
|
|
+ problemType: undefined,
|
|
|
|
|
+ keyword: undefined,
|
|
|
|
|
+ startDate: undefined,
|
|
|
|
|
+ endDate: undefined
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchSettings = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await WorkOrderAPI.getSettings();
|
|
|
|
|
+ setSettings(result.data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取工单设置失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchCategories = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await WorkOrderAPI.getCategories();
|
|
|
|
|
+ setCategories(result.data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取分类列表失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchDevices = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await DeviceInstanceAPI.getDeviceInstances();
|
|
|
|
|
+ setDevices(result.data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取设备列表失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchComments = async (id: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await WorkOrderAPI.getComments(id);
|
|
|
|
|
+ setComments(result.data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取评论失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const fetchStatusHistory = async (id: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await WorkOrderAPI.getStatusHistory(id);
|
|
|
|
|
+ setStatusHistory(result.data);
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取状态历史失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const checkDeadline = async (order: WorkOrder) => {
|
|
|
|
|
+ if (!order.deadline) return;
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ const result = await WorkOrderAPI.getDeadline(order.id);
|
|
|
|
|
+ const { remaining_hours, is_overdue } = result.data;
|
|
|
|
|
+
|
|
|
|
|
+ let color = 'green';
|
|
|
|
|
+ let text = '进行中';
|
|
|
|
|
+ let progress = 100;
|
|
|
|
|
+
|
|
|
|
|
+ if (is_overdue) {
|
|
|
|
|
+ color = 'red';
|
|
|
|
|
+ text = '已超时';
|
|
|
|
|
+ progress = 0;
|
|
|
|
|
+ } else if (remaining_hours < 24) {
|
|
|
|
|
+ color = 'orange';
|
|
|
|
|
+ text = `即将到期 (剩余${remaining_hours}小时)`;
|
|
|
|
|
+ progress = Math.max(10, Math.min(90, remaining_hours * 4));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ setDeadlineInfo({
|
|
|
|
|
+ color,
|
|
|
|
|
+ text,
|
|
|
|
|
+ progress: Number(progress),
|
|
|
|
|
+ remainingTime: `${remaining_hours}小时`,
|
|
|
|
|
+ isOverdue: remaining_hours < 0,
|
|
|
|
|
+ });
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('获取时限信息失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleCreate = () => {
|
|
|
|
|
+ setCurrentOrder({});
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleEdit = async (record: WorkOrder) => {
|
|
|
|
|
+ setCurrentOrder(record);
|
|
|
|
|
+ form.setFieldsValue(record);
|
|
|
|
|
+ setModalVisible(true);
|
|
|
|
|
+ checkDeadline(record);
|
|
|
|
|
+ if (record.id) {
|
|
|
|
|
+ await fetchComments(record.id);
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleSubmit = async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const values = await form.validateFields();
|
|
|
|
|
+ if (currentOrder?.id) {
|
|
|
|
|
+ await WorkOrderAPI.update(currentOrder.id, values);
|
|
|
|
|
+ message.success('更新工单成功');
|
|
|
|
|
+ } else {
|
|
|
|
|
+ await WorkOrderAPI.create(values);
|
|
|
|
|
+ message.success('创建工单成功');
|
|
|
|
|
+ }
|
|
|
|
|
+ setModalVisible(false);
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('操作失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleStatusChange = async (id: string, status: string) => {
|
|
|
|
|
+ Modal.confirm({
|
|
|
|
|
+ title: '确认状态变更',
|
|
|
|
|
+ content: (
|
|
|
|
|
+ <Form form={form}>
|
|
|
|
|
+ <Form.Item name="comment" label="变更备注" rules={[{required: true}]}>
|
|
|
|
|
+ <Input.TextArea placeholder="请输入状态变更原因" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ ),
|
|
|
|
|
+ onOk: async (close) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const values = await form.validateFields();
|
|
|
|
|
+ await WorkOrderAPI.changeStatus(
|
|
|
|
|
+ id,
|
|
|
|
|
+ status,
|
|
|
|
|
+ 'current_user', // TODO: 替换为实际用户
|
|
|
|
|
+ values.comment
|
|
|
|
|
+ );
|
|
|
|
|
+ message.success('状态更新成功');
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ close();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('状态更新失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const renderStatusActions = (record: WorkOrder) => {
|
|
|
|
|
+ const statusOptions = settings?.statusOptions || [];
|
|
|
|
|
+ const currentStatusIndex = statusOptions.indexOf(record.status);
|
|
|
|
|
+ const nextStatus = statusOptions[currentStatusIndex + 1];
|
|
|
|
|
+ const prevStatus = statusOptions[currentStatusIndex - 1];
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ {prevStatus && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ onClick={() => handleStatusChange(record.id, prevStatus)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 回退到{prevStatus}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {nextStatus && (
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ size="small"
|
|
|
|
|
+ onClick={() => handleStatusChange(record.id, nextStatus)}
|
|
|
|
|
+ >
|
|
|
|
|
+ 推进到{nextStatus}
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {!nextStatus && !prevStatus && (
|
|
|
|
|
+ <span>无可用操作</span>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ );
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleShowHistory = async (id: string) => {
|
|
|
|
|
+ await fetchStatusHistory(id);
|
|
|
|
|
+ setHistoryVisible(true);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleAssign = async (id: string, assignee: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await WorkOrderAPI.assign(id, assignee);
|
|
|
|
|
+ message.success('分配成功');
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('分配失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleUploadSuccess = (fileUrl: string, fileInfo: any) => {
|
|
|
|
|
+ if (currentOrder?.id) {
|
|
|
|
|
+ setAttachments(prev => [...prev, {
|
|
|
|
|
+ id: fileInfo.id,
|
|
|
|
|
+ url: fileUrl,
|
|
|
|
|
+ name: fileInfo.original_filename
|
|
|
|
|
+ }]);
|
|
|
|
|
+ message.success('附件上传成功');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleAddComment = async () => {
|
|
|
|
|
+ if (!currentOrder?.id) {
|
|
|
|
|
+ message.error('请先保存工单');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (!commentContent.trim()) {
|
|
|
|
|
+ message.error('评论内容不能为空');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ if (commentContent.length > 500) {
|
|
|
|
|
+ message.error('评论内容不能超过500字');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 简单敏感词过滤
|
|
|
|
|
+ const bannedWords = ['敏感词1', '敏感词2', '敏感词3'];
|
|
|
|
|
+ if (bannedWords.some(word => commentContent.includes(word))) {
|
|
|
|
|
+ message.error('评论包含不允许的内容');
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ await WorkOrderAPI.addComment(currentOrder.id, commentContent);
|
|
|
|
|
+ setCommentContent('');
|
|
|
|
|
+ await fetchComments(currentOrder.id);
|
|
|
|
|
+ message.success('评论添加成功');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('评论添加失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleAccept = async (id: string) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await WorkOrderAPI.changeStatus(id, '处理中', 'current_user', '工单已受理');
|
|
|
|
|
+ message.success('工单受理成功');
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('受理失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleReassign = async (id: string) => {
|
|
|
|
|
+ Modal.confirm({
|
|
|
|
|
+ title: '改派工单',
|
|
|
|
|
+ content: (
|
|
|
|
|
+ <Select placeholder="选择新的处理人" style={{ width: '100%' }}>
|
|
|
|
|
+ <Option value="user1">用户1</Option>
|
|
|
|
|
+ <Option value="user2">用户2</Option>
|
|
|
|
|
+ <Option value="user3">用户3</Option>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ ),
|
|
|
|
|
+ onOk: async (close) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await WorkOrderAPI.assign(id, 'new_assignee'); // TODO: 替换为实际选择的值
|
|
|
|
|
+ message.success('工单改派成功');
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ close();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('改派失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ const handleClose = async (id: string) => {
|
|
|
|
|
+ Modal.confirm({
|
|
|
|
|
+ title: '关闭工单',
|
|
|
|
|
+ content: (
|
|
|
|
|
+ <Form form={form}>
|
|
|
|
|
+ <Form.Item name="feedback" label="处理结果" rules={[{required: true}]}>
|
|
|
|
|
+ <TextArea placeholder="请输入处理结果反馈" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ ),
|
|
|
|
|
+ onOk: async (close) => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const values = await form.validateFields();
|
|
|
|
|
+ await WorkOrderAPI.changeStatus(
|
|
|
|
|
+ id,
|
|
|
|
|
+ '已关闭',
|
|
|
|
|
+ 'current_user',
|
|
|
|
|
+ values.feedback
|
|
|
|
|
+ );
|
|
|
|
|
+ message.success('工单已关闭');
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ close();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('关闭失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ return (
|
|
|
|
|
+ <div>
|
|
|
|
|
+ <div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button type="primary" onClick={handleCreate}>
|
|
|
|
|
+ 新建工单
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button type="primary" onClick={() => setAutoDispatchVisible(true)}>
|
|
|
|
|
+ 自动派工
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ onClick={async () => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ const data = await WorkOrderAPI.exportList(searchParams);
|
|
|
|
|
+ const url = window.URL.createObjectURL(new Blob([data]));
|
|
|
|
|
+ const link = document.createElement('a');
|
|
|
|
|
+ link.href = url;
|
|
|
|
|
+ link.setAttribute('download', `工单列表_${dayjs().format('YYYYMMDD')}.xlsx`);
|
|
|
|
|
+ document.body.appendChild(link);
|
|
|
|
|
+ link.click();
|
|
|
|
|
+ document.body.removeChild(link);
|
|
|
|
|
+ message.success('导出成功');
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('导出失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ }}
|
|
|
|
|
+ >
|
|
|
|
|
+ 工单导出
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ </div>
|
|
|
|
|
+
|
|
|
|
|
+ <Form layout="inline" onFinish={handleSearch} style={{ marginBottom: 16 }}>
|
|
|
|
|
+ <Form.Item name="status" label="工单状态">
|
|
|
|
|
+ <Select style={{ width: 120 }} allowClear>
|
|
|
|
|
+ {Object.values(WorkOrderStatus).map(status => (
|
|
|
|
|
+ <Option key={status} value={status}>{status}</Option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="problemType" label="问题分类">
|
|
|
|
|
+ <Select style={{ width: 120 }} allowClear>
|
|
|
|
|
+ {categories.map(category => (
|
|
|
|
|
+ <Option key={category} value={category}>{category}</Option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="keyword" label="关键字">
|
|
|
|
|
+ <Input placeholder="请输入关键字" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="dateRange" label="时间范围">
|
|
|
|
|
+ <DatePicker.RangePicker />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item>
|
|
|
|
|
+ <Button type="primary" htmlType="submit">
|
|
|
|
|
+ 查询
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ <Button style={{ marginLeft: 8 }} onClick={handleReset}>
|
|
|
|
|
+ 重置
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+
|
|
|
|
|
+ <Table dataSource={workOrders} loading={loading} rowKey="id">
|
|
|
|
|
+ <Column title="工单编号" dataIndex="order_no" key="order_no" />
|
|
|
|
|
+ <Column title="设备名称" dataIndex="device_name" key="device_name" />
|
|
|
|
|
+ <Column title="问题描述" dataIndex="problem_desc" key="problem_desc" ellipsis />
|
|
|
|
|
+ <Column title="故障等级" dataIndex="priority" key="priority" />
|
|
|
|
|
+ <Column title="创建人" dataIndex="creator_name" key="creator_name" />
|
|
|
|
|
+ <Column
|
|
|
|
|
+ title="截止日期"
|
|
|
|
|
+ dataIndex="deadline"
|
|
|
|
|
+ key="deadline"
|
|
|
|
|
+ render={(deadline) => deadline ? dayjs(deadline).format('YYYY-MM-DD') : '-'}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Column
|
|
|
|
|
+ title="创建日期"
|
|
|
|
|
+ dataIndex="created_at"
|
|
|
|
|
+ key="created_at"
|
|
|
|
|
+ render={(date) => dayjs(date).format('YYYY-MM-DD')}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Column title="问题分类" dataIndex="problem_type" key="problem_type" />
|
|
|
|
|
+ <Column
|
|
|
|
|
+ title="状态"
|
|
|
|
|
+ dataIndex="status"
|
|
|
|
|
+ key="status"
|
|
|
|
|
+ render={(status, record: WorkOrder) => (
|
|
|
|
|
+ <Space>
|
|
|
|
|
+ <span>{status}</span>
|
|
|
|
|
+ {deadlineInfo && record.id === currentOrder?.id && (
|
|
|
|
|
+ <Tag color={deadlineInfo.color}>{deadlineInfo.text}</Tag>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Column
|
|
|
|
|
+ title="结果反馈"
|
|
|
|
|
+ dataIndex="feedback"
|
|
|
|
|
+ key="feedback"
|
|
|
|
|
+ render={(feedback) => (
|
|
|
|
|
+ feedback ? (
|
|
|
|
|
+ <a onClick={() => {
|
|
|
|
|
+ setCurrentDetail(feedback);
|
|
|
|
|
+ setDetailModalVisible(true);
|
|
|
|
|
+ }}>详情</a>
|
|
|
|
|
+ ) : '-'
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Column
|
|
|
|
|
+ title="附件"
|
|
|
|
|
+ dataIndex="attachments"
|
|
|
|
|
+ key="attachments"
|
|
|
|
|
+ render={(attachments) => attachments?.length > 0 ? `${attachments.length}个` : '无'}
|
|
|
|
|
+ />
|
|
|
|
|
+ <Column
|
|
|
|
|
+ title="操作"
|
|
|
|
|
+ key="action"
|
|
|
|
|
+ render={(_, record: WorkOrder) => (
|
|
|
|
|
+ <Space size="middle">
|
|
|
|
|
+ <Dropdown
|
|
|
|
|
+ overlay={
|
|
|
|
|
+ <Menu>
|
|
|
|
|
+ {record.status === '待受理' && (
|
|
|
|
|
+ <Menu.Item key="accept" onClick={() => handleAccept(record.id)}>
|
|
|
|
|
+ 受理
|
|
|
|
|
+ </Menu.Item>
|
|
|
|
|
+ )}
|
|
|
|
|
+ {record.status === '处理中' && (
|
|
|
|
|
+ <Menu.Item key="reassign" onClick={() => handleReassign(record.id)}>
|
|
|
|
|
+ 改派
|
|
|
|
|
+ </Menu.Item>
|
|
|
|
|
+ )}
|
|
|
|
|
+ <Menu.Item
|
|
|
|
|
+ key="close"
|
|
|
|
|
+ onClick={() => handleClose(record.id)}
|
|
|
|
|
+ danger
|
|
|
|
|
+ >
|
|
|
|
|
+ 关闭
|
|
|
|
|
+ </Menu.Item>
|
|
|
|
|
+ </Menu>
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <Button type="primary" size="small">操作</Button>
|
|
|
|
|
+ </Dropdown>
|
|
|
|
|
+ <Button type="link" size="small" onClick={() => handleShowHistory(record.id)}>
|
|
|
|
|
+ 流程
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </Space>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ </Table>
|
|
|
|
|
+
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
+ <div>{currentOrder?.id ? '工单详情' : '新建工单'}</div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ icon={<CloseOutlined />}
|
|
|
|
|
+ onClick={() => setModalVisible(false)}
|
|
|
|
|
+ style={{ marginRight: -16 }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ visible={modalVisible}
|
|
|
|
|
+ onOk={handleSubmit}
|
|
|
|
|
+ onCancel={() => setModalVisible(false)}
|
|
|
|
|
+ width={800}
|
|
|
|
|
+ footer={
|
|
|
|
|
+ <div style={{ textAlign: 'center' }}>
|
|
|
|
|
+ <Button key="submit" type="primary" onClick={handleSubmit}>
|
|
|
|
|
+ 确定
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ closable={false}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form form={form} layout="vertical">
|
|
|
|
|
+ <Form.Item name="order_no" label="工单编号" rules={[{ required: true }]}>
|
|
|
|
|
+ <Input placeholder="自动生成" disabled />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="device_id" label="设备" rules={[{ required: true }]}>
|
|
|
|
|
+ <Select showSearch optionFilterProp="children">
|
|
|
|
|
+ {devices.map(device => (
|
|
|
|
|
+ <Option key={device.id} value={device.id}>
|
|
|
|
|
+ {device.name}
|
|
|
|
|
+ </Option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="problem_desc" label="问题描述" rules={[{ required: true }]}>
|
|
|
|
|
+ <TextArea rows={4} />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="priority" label="故障等级" rules={[{ required: true }]}>
|
|
|
|
|
+ <Select>
|
|
|
|
|
+ <Option value="紧急">紧急</Option>
|
|
|
|
|
+ <Option value="高">高</Option>
|
|
|
|
|
+ <Option value="中">中</Option>
|
|
|
|
|
+ <Option value="低">低</Option>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="deadline" label="截止日期" rules={[{ required: true }]}>
|
|
|
|
|
+ <DatePicker style={{ width: '100%' }} />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="problem_type" label="问题分类">
|
|
|
|
|
+ <Select>
|
|
|
|
|
+ {categories.map(category => (
|
|
|
|
|
+ <Option key={category} value={category}>
|
|
|
|
|
+ {category}
|
|
|
|
|
+ </Option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="feedback" label="结果反馈">
|
|
|
|
|
+ <TextArea rows={2} />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item label="附件">
|
|
|
|
|
+ <Uploader
|
|
|
|
|
+ onSuccess={handleUploadSuccess}
|
|
|
|
|
+ onError={(error) => message.error(`上传失败: ${error.message}`)}
|
|
|
|
|
+ onProgress={(percent) => (
|
|
|
|
|
+ <Progress percent={percent} status="active" />
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ {attachments.length > 0 && (
|
|
|
|
|
+ <List
|
|
|
|
|
+ dataSource={attachments}
|
|
|
|
|
+ renderItem={item => (
|
|
|
|
|
+ <List.Item>
|
|
|
|
|
+ <a href={item.url} target="_blank" rel="noopener noreferrer">
|
|
|
|
|
+ {item.name}
|
|
|
|
|
+ </a>
|
|
|
|
|
+ </List.Item>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+
|
|
|
|
|
+ {currentOrder?.id && (
|
|
|
|
|
+ <div style={{ marginTop: 24 }}>
|
|
|
|
|
+ <h3>评论</h3>
|
|
|
|
|
+ <List
|
|
|
|
|
+ className="comment-list"
|
|
|
|
|
+ itemLayout="horizontal"
|
|
|
|
|
+ dataSource={comments}
|
|
|
|
|
+ renderItem={item => (
|
|
|
|
|
+ <List.Item>
|
|
|
|
|
+ <List.Item.Meta
|
|
|
|
|
+ avatar={<Avatar>{item.author.charAt(0)}</Avatar>}
|
|
|
|
|
+ title={item.author}
|
|
|
|
|
+ description={item.content}
|
|
|
|
|
+ />
|
|
|
|
|
+ <div>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</div>
|
|
|
|
|
+ </List.Item>
|
|
|
|
|
+ )}
|
|
|
|
|
+ />
|
|
|
|
|
+ <TextArea
|
|
|
|
|
+ rows={4}
|
|
|
|
|
+ value={commentContent}
|
|
|
|
|
+ onChange={(e) => setCommentContent(e.target.value)}
|
|
|
|
|
+ placeholder="输入评论内容"
|
|
|
|
|
+ />
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ onClick={handleAddComment}
|
|
|
|
|
+ style={{ marginTop: 16 }}
|
|
|
|
|
+ >
|
|
|
|
|
+ 提交评论
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={`工单流程记录 (共${statusHistory.length}条)`}
|
|
|
|
|
+ visible={historyVisible}
|
|
|
|
|
+ onCancel={() => setHistoryVisible(false)}
|
|
|
|
|
+ footer={null}
|
|
|
|
|
+ width={800}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div style={{ maxHeight: '60vh', overflowY: 'auto' }}>
|
|
|
|
|
+ <Timeline mode="alternate">
|
|
|
|
|
+ {statusHistory.map(item => (
|
|
|
|
|
+ <Timeline.Item
|
|
|
|
|
+ key={item.id}
|
|
|
|
|
+ color={
|
|
|
|
|
+ item.status_to === '已完成' ? 'green' :
|
|
|
|
|
+ item.status_to === '已取消' ? 'red' : 'blue'
|
|
|
|
|
+ }
|
|
|
|
|
+ >
|
|
|
|
|
+ <div style={{ padding: '8px 16px', background: '#f9f9f9', borderRadius: 4 }}>
|
|
|
|
|
+ <strong>{item.status_from} → {item.status_to}</strong>
|
|
|
|
|
+ <div style={{ marginTop: 8 }}>
|
|
|
|
|
+ <Tag color="geekblue">{item.operator}</Tag>
|
|
|
|
|
+ <Tag>{dayjs(item.created_at).format('YYYY-MM-DD HH:mm')}</Tag>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {item.comment && (
|
|
|
|
|
+ <div style={{ marginTop: 8 }}>
|
|
|
|
|
+ <p style={{ marginBottom: 0 }}><strong>备注:</strong></p>
|
|
|
|
|
+ <p style={{ marginTop: 4 }}>{item.comment}</p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ )}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Timeline.Item>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Timeline>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
+ <div>
|
|
|
|
|
+ 自动派工
|
|
|
|
|
+ <Switch
|
|
|
|
|
+ style={{ marginLeft: 16 }}
|
|
|
|
|
+ checkedChildren="开"
|
|
|
|
|
+ unCheckedChildren="关"
|
|
|
|
|
+ defaultChecked
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ icon={<CloseOutlined />}
|
|
|
|
|
+ onClick={() => setAutoDispatchVisible(false)}
|
|
|
|
|
+ style={{ marginRight: -16 }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ open={autoDispatchVisible}
|
|
|
|
|
+ footer={null}
|
|
|
|
|
+ width={600}
|
|
|
|
|
+ closable={false}
|
|
|
|
|
+ onCancel={() => setAutoDispatchVisible(false)}
|
|
|
|
|
+ >
|
|
|
|
|
+ <Form form={autoDispatchForm} layout="vertical">
|
|
|
|
|
+ <Form.Item name="device_type" label="设备分类">
|
|
|
|
|
+ <Select placeholder="全部类型设备" allowClear>
|
|
|
|
|
+ {categories.map(category => (
|
|
|
|
|
+ <Option key={category} value={category}>{category}</Option>
|
|
|
|
|
+ ))}
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="problem_desc" label="问题描述">
|
|
|
|
|
+ <TextArea rows={3} placeholder="设备名称+告警故障" />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="priority" label="故障等级" initialValue="中">
|
|
|
|
|
+ <Select>
|
|
|
|
|
+ <Option value="紧急">紧急</Option>
|
|
|
|
|
+ <Option value="高">高</Option>
|
|
|
|
|
+ <Option value="中">中</Option>
|
|
|
|
|
+ <Option value="低">低</Option>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item name="assignee" label="处理人" initialValue="admin">
|
|
|
|
|
+ <Select>
|
|
|
|
|
+ <Option value="admin">admin</Option>
|
|
|
|
|
+ <Option value="user1">用户1</Option>
|
|
|
|
|
+ <Option value="user2">用户2</Option>
|
|
|
|
|
+ <Option value="user3">用户3</Option>
|
|
|
|
|
+ </Select>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item
|
|
|
|
|
+ name="deadline"
|
|
|
|
|
+ label="截止时间"
|
|
|
|
|
+ initialValue={dayjs().add(2, 'day')}
|
|
|
|
|
+ >
|
|
|
|
|
+ <DatePicker style={{ width: '100%' }} />
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ <Form.Item>
|
|
|
|
|
+ <div style={{ textAlign: 'center', marginTop: 24 }}>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="primary"
|
|
|
|
|
+ onClick={() => {
|
|
|
|
|
+ autoDispatchForm.validateFields()
|
|
|
|
|
+ .then(async values => {
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (!values.problem_desc) {
|
|
|
|
|
+ values.problem_desc = `设备${values.device_type || '全部'}告警故障`;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ await WorkOrderAPI.create({
|
|
|
|
|
+ ...values,
|
|
|
|
|
+ title: '自动派工工单',
|
|
|
|
|
+ creator_id: 'system',
|
|
|
|
|
+ creator_name: '系统自动派工',
|
|
|
|
|
+ status: '待受理'
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ message.success('自动派工成功');
|
|
|
|
|
+ setAutoDispatchVisible(false);
|
|
|
|
|
+ fetchData();
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ message.error('自动派工失败');
|
|
|
|
|
+ }
|
|
|
|
|
+ })
|
|
|
|
|
+ .catch(info => {
|
|
|
|
|
+ console.log('Validate Failed:', info);
|
|
|
|
|
+ });
|
|
|
|
|
+ }}
|
|
|
|
|
+ style={{ width: 120 }}
|
|
|
|
|
+ >
|
|
|
|
|
+ 确认
|
|
|
|
|
+ </Button>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Form.Item>
|
|
|
|
|
+ </Form>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+
|
|
|
|
|
+ <Modal
|
|
|
|
|
+ title={
|
|
|
|
|
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
+ <div>工单详情</div>
|
|
|
|
|
+ <Button
|
|
|
|
|
+ type="text"
|
|
|
|
|
+ icon={<CloseOutlined />}
|
|
|
|
|
+ onClick={() => setDetailModalVisible(false)}
|
|
|
|
|
+ style={{ marginRight: -16 }}
|
|
|
|
|
+ />
|
|
|
|
|
+ </div>
|
|
|
|
|
+ }
|
|
|
|
|
+ visible={detailModalVisible}
|
|
|
|
|
+ onCancel={() => setDetailModalVisible(false)}
|
|
|
|
|
+ footer={null}
|
|
|
|
|
+ width={600}
|
|
|
|
|
+ closable={false}
|
|
|
|
|
+ >
|
|
|
|
|
+ <div style={{ padding: 16 }}>
|
|
|
|
|
+ {currentDetail}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </Modal>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ );
|
|
|
|
|
+}
|