diff --git a/README.md b/README.md index 9ff40f3..1f070c7 100644 --- a/README.md +++ b/README.md @@ -7,27 +7,39 @@ Global Highlights Hub 海外供应商平台 2. 运行开发环境:npm run dev 或者 start.bat 3. 打包代码:npm run build 或者 build.bat +## 版本设置 +npm version [ | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git] -## 相关文档 -需求文档 https://www.kdocs.cn/l/csZrIZlpuF2i -dayjs https://dayjs.gitee.io/docs/zh-CN/manipulate/start-of -antd https://ant-design.antgroup.com/components/upload-cn#uploadfile -反馈表案例 https://www.chinahighlights.com/customerservice/feedback/PostTourSurveyFormToWLGH.asp?LGC=1&COLI_SN=988185&MEI_SN=954295&Email=jennroth18@hotmail.com&ToC=0&ShowType=&page_class=4&dei_sn=28&country=30,490 -国内供应商平台 http://p.mycht.cn/index.aspx -文档预览 https://github.com/cyntler/react-doc-viewer -wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start -pdf生成 https://github.com/ivmarcos/react-to-pdf -react-pdf https://react-pdf.org +npm version premajor --no-git-tag-version +1.0.0 -> 2.0.0-0 +--preid beta | alpha | rc +npm version prerelease --preid beta --no-git-tag-version +2.0.0-0 -> 2.0.0-1 -> 2.0.0-2 ..n -> 2.0.0-n +npm version patch --no-git-tag-version +2.0.0-n -> 2.0.0 -## 阿里云OSS -Bucket 名称:global-highlights-hub -Endpoint:oss-cn-hongkong.aliyuncs.com -global-highlights-hub.oss-cn-hongkong.aliyuncs.com +"push:tag": "npm version patch && git.exe push --progress "origin" main:main" +"push:tag": "npm version patch && git push origin master" +## 相关文档 +需求文档 https://www.kdocs.cn/l/csZrIZlpuF2i +dayjs https://dayjs.gitee.io/docs/zh-CN/manipulate/start-of +antd https://ant-design.antgroup.com/components/upload-cn#uploadfile +反馈表案例 https://www.chinahighlights.com/customerservice/feedback/PostTourSurveyFormToWLGH.asp?LGC=1&COLI_SN=988185&MEI_SN=954295&Email=jennroth18@hotmail.com&ToC=0&ShowType=&page_class=4&dei_sn=28&country=30,490 +国内供应商平台 http://p.mycht.cn/index.aspx +文档预览 https://github.com/cyntler/react-doc-viewer +wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start +pdf生成 https://github.com/ivmarcos/react-to-pdf +react-pdf https://react-pdf.org + +## 阿里云OSS +Bucket 名称:global-highlights-hub +Endpoint:oss-cn-hongkong.aliyuncs.com +global-highlights-hub.oss-cn-hongkong.aliyuncs.com -反馈表测试链接 -http://202.103.68.111:5173/feedback/330948 +反馈表测试链接 +http://202.103.68.111:5173/feedback/330948 --- diff --git a/doc/RBAC 权限.sql b/doc/RBAC 权限.sql index 6e70db9..c5d308d 100644 --- a/doc/RBAC 权限.sql +++ b/doc/RBAC 权限.sql @@ -45,22 +45,44 @@ VALUES ('技术研发部') INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) VALUES ('所有权限', '*', 'system') INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) -VALUES ('最新团计划', '/reservation/newest', 'oversea') +VALUES ('管理账号', '/account/management', 'system') INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) -VALUES ('账单', '/invoice', 'oversea') +VALUES ('新增账号', '/account/new', 'system') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('禁用账号', '/account/disable', 'system') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('重置密码', '/account/reset-password', 'system') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('管理角色', '/account/role-new', 'system') + +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('所有海外功能', '/oversea/all', 'oversea') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('所有国内功能', '/domestic/all', 'domestic') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('所有机票功能', '/air-ticket/all', 'air-ticket') +-- 价格管理 +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('管理产品', '/products/*', 'products') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('审核信息', '/products/info/audit', 'products') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('录入信息', '/products/info/put', 'products') INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) -VALUES ('账号权限管理', '/account/management', 'system') +VALUES ('审核价格', '/products/offer/audit', 'products') INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) -VALUES ('新增角色', '/account/new-role', 'system') +VALUES ('录入价格', '/products/offer/put', 'products') + +-- 默认页面 +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('最新计划', 'route=/reservation/newest', 'page') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('机票订票', 'route=/airticket', 'page') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('产品管理(客服)', 'route=/products', 'page') +INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) +VALUES ('产品管理(供应商)', 'route=/products/edit', 'page') INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) VALUES (1, 1) -INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) -VALUES (6, 2) -INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) -VALUES (6, 3) -INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) -VALUES (6, 4) -INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id]) -VALUES (6, 5) diff --git a/doc/价格管理平台.bmpr b/doc/价格管理平台.bmpr index e1065f7..6f91c6d 100644 Binary files a/doc/价格管理平台.bmpr and b/doc/价格管理平台.bmpr differ diff --git a/index.html b/index.html index 83cb221..c316114 100644 --- a/index.html +++ b/index.html @@ -14,13 +14,6 @@ 100%{-webkit-transform:translate(150px)} } - -
diff --git a/package.json b/package.json index dd39be7..ac1a9b2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { - "name": "global.highlights.hub", + "name": "global-highlights-hub", "private": true, - "version": "2.0.0", + "version": "2.0.0-alpha.0", "type": "module", "scripts": { "dev": "vite", diff --git a/public/locales/en/account.json b/public/locales/en/account.json index 7abb5cf..e8ea251 100644 --- a/public/locales/en/account.json +++ b/public/locales/en/account.json @@ -8,5 +8,29 @@ "CurrentPassword": "Please input your password.", "NewPassword": "Please input your new password.", "ReenterPassword": "Please reenter your password." - } + }, + "createdOn": "Created on", + "action": "Action", + "action.edit": "Edit", + "action.enable": "Enable", + "action.disable": "Disable", + "action.enable.title": "Do you want to enable account?", + "action.disable.title": "Do you want to disable account?", + "action.resetPassword": "Reset Password", + "action.resetPassword.tile": "Do you want to reset password?", + + "accountList": "Account List", + "newAccount": "New Account", + "detail": "Detail", + "username": "Username", + "realname": "Realname", + "travelAgency": "Travel Agency", + "travelAgencyName": "Travel Agency Name", + "email": "Email", + "lastLogin": "Last Login", + + "roleList": "Role List", + "newRole": "New Role", + "roleName": "Role Name", + "permission": "Permission" } \ No newline at end of file diff --git a/public/locales/en/common.json b/public/locales/en/common.json index a513e2a..b228ae8 100644 --- a/public/locales/en/common.json +++ b/public/locales/en/common.json @@ -10,7 +10,9 @@ "Confirm": "Confirm", "Close": "Close", "Save": "Save", + "New": "New", "Edit": "Edit", + "Audit": "Audit", "Delete": "Delete", "Add": "Add", "View": "View", @@ -19,9 +21,27 @@ "Upload": "Upload", "preview": "Preview", "Total": "Total", + "Action": "Action", + "Import": "Import", + "Export": "Export", + "Copy": "Copy", + + "sureCancel": "Are you sure to cancel?", + "sureDelete":"Are you sure to delete?", + "Yes": "Yes", + + "Success": "Success", + "Failed": "Failed", + + "All": "All", + + "Table": { + "Total": "Total {{total}} items" + }, "Login": "Login", "Username": "Username", + "Realname": "Realname", "Password": "Password", "ChangePassword": "Change password", @@ -45,13 +65,32 @@ "lastThreeMonth": "Last Three Month", "thisYear": "This Year" }, + "weekdays": { + "1": "Monday", + "2": "Tuesday", + "3": "Wednesday", + "4": "Thursday", + "5": "Friday", + "6": "Saturday", + "7": "Sunday" + }, + "weekdaysShort": { + "1": "Mon", + "2": "Tue", + "3": "Wed", + "4": "Thu", + "5": "Fri", + "6": "Sat", + "7": "Sun" + }, "menu": { "Reservation": "Reservation", "Invoice": "Invoice", "Feedback": "Feedback", "Notice": "Notice", "Report": "Report", - "Airticket": "AirTicket" + "Airticket": "AirTicket", + "Products": "Products" }, "Validation": { "Title": "Notification", diff --git a/public/locales/en/group.json b/public/locales/en/group.json index 4286b80..2083c6e 100644 --- a/public/locales/en/group.json +++ b/public/locales/en/group.json @@ -1,6 +1,7 @@ { "ArrivalDate": "Arrival Date", "RefNo": "Reference number", + "unconfirmed": "Unconfirmed", "Pax": "Pax", "Status": "Status", "City": "City", diff --git a/public/locales/en/products.json b/public/locales/en/products.json new file mode 100644 index 0000000..384e1f5 --- /dev/null +++ b/public/locales/en/products.json @@ -0,0 +1,118 @@ +{ + "ProductType": "Product Type", + "type": { + "Experience": "Experience", + "Car": "Transport Services", + "Guide": "Guide Services", + "Package": "Package Tour", + "Attractions": "Attractions", + "Meals": "Meals", + "Extras": "Extras", + "UltraService": "Ultra Service" + }, + "EditComponents": { + "info": "Product Information", + "Quotation": "Quotation", + "Extras": "Components" + }, + "auditState": { + "New": "New", + "Pending": "Pending", + "Approved": "Approved", + "Rejected": "Rejected", + "Published": "Published" + }, + "auditStateAction": { + "New": "New", + "Pending": "Pending", + "Approved": "Approve", + "Rejected": "Reject", + "Published": "Publish" + }, + "PriceUnit": { + "0": "Person", + "1": "Group", + "title": "Price Unit" + }, + "Status": "Status", + "State": "State", + + "Title": "Title", + "Vendor": "Vendor", + "AuState": "Audit State", + "CreatedBy": "Created By", + "CreateDate": "Create Date", + "AuditedBy": "Audited By", + "AuditDate": "Audit Date", + "OpenHours": "Open Hours", + "Duration": "Duration", + "KM": "KM", + "RecommendsRate": "RecommendsRate", + "OpenWeekdays": "Open Weekdays", + "DisplayToC": "Display To C", + "Dept": "Dept", + + "productProject": "Product project", + "Code": "Code", + "City": "City", + "Remarks": "Remarks", + "tourTime": "Tour time", + "recommendationRate": "Recommends rate", + "Name": "Name", + "Price":"Price", + "Description":"Description", + "supplierQuotation": "Supplier quotation", + "addQuotation": "Add quotation", + "bindingProducts": "Binding products", + "addBinding": "Add binding", + + "adultPrice": "Adult price", + "childrenPrice": "Child price", + "currency": "Currency", + "Types": "Type", + "number": "Number", + "validityPeriod":"Validity period", + "operation": "Operation", + "price": "Price", + "weekends":"Weekends", + + "Quotation": "Quotation", + "Offer": "Offer", + + "Unit": "Unit", + "GroupSize": "Group Size", + "UseDates": "Use Dates", + + "Weekdays": "Weekdays", + "OnWeekdays": "On Weekdays: ", + "Unlimited": "Unlimited", + + "UseYear": "Use Year", + + "AgeType": { + "Type": "Age Type", + "Adult": "Adult", + "Child": "Child" + }, + + "CopyFormMsg": { + "Source": "Source ", + "target": "Target ", + "requiredVendor": "Please pick a target vendor", + "requiredTypes": "Please select product types", + "requiredDept": "Please pick a owner department" + }, + + "quotationTable": { + "Adult": "Adult", + "Child": "Child", + "Currency": "Currency", + "Unit": "Unit", + "GroupSize": "Group Size", + "UseDates": "Use Dates", + "Operation": "Operation", + "Weekdays": "Weekdays" + }, + + "#": "Products" +} diff --git a/public/locales/zh/account.json b/public/locales/zh/account.json index a7caf37..b38946c 100644 --- a/public/locales/zh/account.json +++ b/public/locales/zh/account.json @@ -8,5 +8,29 @@ "CurrentPassword": "请输入密码。", "NewPassword": "请输入新密码。", "ReenterPassword": "请重复输入密码。" - } + }, + "createdOn": "创建时间", + "action": "操作", + "action.edit": "编辑", + "action.enable": "启用", + "action.disable": "禁用", + "action.enable.title": "确定启用该账号吗?", + "action.disable.title": "确定禁用该账号吗?", + "action.resetPassword": "重置密码", + "action.resetPassword.tile": "确定重置账号密码吗?", + + "accountList": "管理账号", + "newAccount": "新增账号", + "detail": "详细信息", + "username": "用户名", + "realname": "姓名", + "travelAgency": "供应商", + "travelAgencyName": "供应商名称", + "email": "邮箱地址", + "lastLogin": "最后登陆时间", + + "roleList": "管理角色", + "newRole": "新增角色", + "roleName": "角色名称", + "permission": "权限" } \ No newline at end of file diff --git a/public/locales/zh/common.json b/public/locales/zh/common.json index 59c417e..dd6e73e 100644 --- a/public/locales/zh/common.json +++ b/public/locales/zh/common.json @@ -10,7 +10,9 @@ "Confirm": "确认", "Close": "关闭", "Save": "保存", + "New": "新增", "Edit": "编辑", + "Audit": "审核", "Delete": "删除", "Add": "添加", "View": "查看", @@ -19,9 +21,27 @@ "Upload": "上传", "preview": "预览", "Total": "总数", + "Action": "操作", + "Import": "导入", + "Export": "导出", + "Copy": "复制", + + "sureCancel": "确定取消?", + "sureDelete":"确定删除?", + "Yes": "是", + + "Success": "成功", + "Failed": "失败", + + "All": "所有", + + "Table": { + "Total": "共 {{total}} 条" + }, "Login": "登录", "Username": "账号", + "Realname": "姓名", "Password": "密码", "ChangePassword": "修改密码", @@ -45,13 +65,32 @@ "lastThreeMonth": "前三个月", "thisYear": "今年" }, + "weekdays": { + "1": "周一", + "2": "周二", + "3": "周三", + "4": "周四", + "5": "周五", + "6": "周六", + "7": "周日" + }, + "weekdaysShort": { + "1": "一", + "2": "二", + "3": "三", + "4": "四", + "5": "五", + "6": "六", + "7": "日" + }, "menu": { "Reservation": "团预订", "Invoice": "账单", "Feedback": "反馈表", "Notice": "通知", "Report": "质量评分", - "Airticket": "机票订票" + "Airticket": "机票订票", + "Products": "产品管理" }, "Validation": { "Title": "温馨提示", diff --git a/public/locales/zh/group.json b/public/locales/zh/group.json index 2f25071..5b18f89 100644 --- a/public/locales/zh/group.json +++ b/public/locales/zh/group.json @@ -1,6 +1,7 @@ { "ArrivalDate": "抵达日期", "RefNo": "团号", + "unconfirmed": "未确认", "Pax": "人数", "Status": "状态", "City": "城市", @@ -11,6 +12,5 @@ "ConfirmationDate": "确认日期", "ConfirmationDetails": "确认信息", "PNR": "旅客订座记录", - "#": "#" } diff --git a/public/locales/zh/products.json b/public/locales/zh/products.json new file mode 100644 index 0000000..c8b56a4 --- /dev/null +++ b/public/locales/zh/products.json @@ -0,0 +1,118 @@ +{ + "ProductType": "项目类型", + "type": { + "Experience": "综费", + "Car": "车费", + "Guide": "导游", + "Package": "包价线路", + "Attractions": "景点", + "Meals": "餐费", + "Extras": "附加项目", + "UltraService": "超公里" + }, + "EditComponents": { + "info": "产品信息", + "Quotation": "报价", + "Extras": "绑定项目" + }, + "auditState": { + "New": "新增", + "Pending": "待审核", + "Approved": "已审核", + "Rejected": "未通过", + "Published": "已发布" + }, + "auditStateAction": { + "New": "新增", + "Pending": "待审核", + "Approved": "审核通过", + "Rejected": "审核拒绝", + "Published": "审核发布" + }, + "PriceUnit": { + "0": "每人", + "1": "每团", + "title": "报价单位" + }, + "Status": "状态", + "State": "状态", + + "Title": "名称", + "Vendor": "供应商", + "AuState": "审核状态", + "CreatedBy": "提交人员", + "CreateDate": "提交时间", + "AuditedBy": "审核人员", + "AuditDate": "审核时间", + + "OpenHours": "游览时间", + "Duration": "游览时长", + "KM": "公里数", + "RecommendsRate": "推荐指数", + "OpenWeekdays": "周开放日", + "DisplayToC": "报价信显示", + "Dept": "小组", + + + "productProject": "产品项目", + "Code": "简码", + "City": "城市", + "Remarks": "备注", + "tourTime": "游览时间", + "recommendationRate": "推荐指数", + "Name":"名称", + "Price":"价格", + "Description":"描述", + "supplierQuotation": "供应商报价", + "addQuotation": "添加报价", + "bindingProducts": "绑定产品", + "addBinding": "添加绑定", + + "adultPrice": "成人价", + "childrenPrice": "儿童价", + "currency": "币种", + "Types": "类型", + "number": "人等", + "validityPeriod":"有效期", + "operation": "操作", + "price": "价格", + + "Quotation": "报价", + "Offer": "报价", + "Unit": "单位", + "GroupSize": "人等", + "UseDates": "使用日期", + + "Weekdays": "周末", + "OnWeekdays": "周: ", + "Unlimited": "不限", + + "UseYear": "年份", + + "AgeType": { + "Type": "人群", + "Adult": "成人", + "Child": "儿童" + }, + + "CopyFormMsg": { + "Source": "源", + "target": "目标", + "requiredVendor": "请选择目标供应商", + "requiredTypes": "请选择产品类型", + "requiredDept": "请选择所属小组" + }, + + "quotationTable": { + "Adult": "成人", + "Child": "儿童", + "Currency": "币种", + "Unit": "类型", + "GroupSize": "人等", + "UseDates": "使用日期", + "Weekdays": "周末", + "Operation": "Operation" + }, + + "#": "产品" +} diff --git a/src/assets/global.css b/src/assets/global.css index ca0ad13..cc5d96f 100644 --- a/src/assets/global.css +++ b/src/assets/global.css @@ -1,23 +1,6 @@ @import 'tailwindcss/base'; @import 'tailwindcss/components'; @import 'tailwindcss/utilities'; - -.logo { - float: left; - height: 36px; - margin: 16px 24px 16px 0; - background: rgba(255, 255, 255, 0.3); -} - -.reservation-highlight { - color: rgba(255, 255, 255, 1); - background-color: rgba(255, 0, 0, 0.6); -} - -#error-page { - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - width: 100%; +.ant-table-wrapper.border-collapse table { + border-collapse: collapse; } diff --git a/src/components/AuditStateSelector.jsx b/src/components/AuditStateSelector.jsx new file mode 100644 index 0000000..db4c77d --- /dev/null +++ b/src/components/AuditStateSelector.jsx @@ -0,0 +1,12 @@ +import { Select } from 'antd'; +import { useProductsAuditStates } from '@/hooks/useProductsSets'; + +const AuditStateSelector = ({ ...props }) => { + const states = useProductsAuditStates(); + return ( + <> + + {/* {_show_all ? ( + + 所有小组 + + ) : ( + '' + )} */} +
+ ); + +}; +export default DeptSelector; diff --git a/src/components/ProductsTypesSelector.jsx b/src/components/ProductsTypesSelector.jsx new file mode 100644 index 0000000..ca61815 --- /dev/null +++ b/src/components/ProductsTypesSelector.jsx @@ -0,0 +1,23 @@ +import { Select } from 'antd'; +import { useProductsTypes } from '@/hooks/useProductsSets'; +import { useTranslation } from 'react-i18next'; + +import { fetchJSON } from '@/utils/request'; +import { HT_HOST } from '@/config'; + +//供应商列表 +export const fetchVendorList = async (q) => { + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q }) + return errcode !== 0 ? [] : result +} + +const ProductsTypesSelector = ({...props}) => { + const productsTypes = useProductsTypes(); + const { t } = useTranslation(); + return ( + <> + + , + fieldProps?.keyword?.col || 6 + ), item( 'referenceNo', - 1, + 99, - + , - fieldProps?.referenceNo?.col || 4 + fieldProps?.referenceNo?.col || 6 ), item( 'PNR', - 2, - - + 99, + + , fieldProps?.PNR?.col || 4 ), @@ -153,7 +203,6 @@ function getFields(props) { 99, + , fieldProps?.username?.col || 4 ), + /** + * + */ item( - 'realname', - 4, - - + 'year', + 99, + + , - fieldProps?.realname?.col || 4 + fieldProps?.year?.col || 3 + ), + item( + 'agency', + 99, + + + , + fieldProps?.agency?.col || 6 + ), + item( + 'audit_state', + 99, + + + , + fieldProps?.audit_state?.col || 3 + ), + item( + 'products_types', + 99, + + + , + fieldProps?.products_types?.col || 6 + ), + item( + 'dept', + 99, + + + , + fieldProps?.dept?.col || 6 + ), + item( + 'city', + 99, + + + , + fieldProps?.city?.col || 4 + ), + item( + 'unconfirmed', + 99, + + {t('group:unconfirmed')} + , + fieldProps?.unconfirmed?.col || 2 ), - ]; baseChildren = baseChildren .map((x) => { diff --git a/src/components/SearchInput.jsx b/src/components/SearchInput.jsx new file mode 100644 index 0000000..134c301 --- /dev/null +++ b/src/components/SearchInput.jsx @@ -0,0 +1,49 @@ +import React, { useMemo, useRef, useState } from 'react'; +import { Select, Spin } from 'antd'; +import { debounce, objectMapper } from '@/utils/commons'; + +function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) { + const [fetching, setFetching] = useState(false); + const [options, setOptions] = useState([]); + const fetchRef = useRef(0); + const debounceFetcher = useMemo(() => { + const loadOptions = (value) => { + fetchRef.current += 1; + const fetchId = fetchRef.current; + setOptions([]); + setFetching(true); + fetchOptions(value).then((newOptions) => { + const mapperOptions = newOptions.map(ele => objectMapper(ele, props.map)); + if (fetchId !== fetchRef.current) { + // for fetch callback order + return; + } + setOptions(mapperOptions); + setFetching(false); + }); + }; + return debounce(loadOptions, debounceTimeout); + }, [fetchOptions, debounceTimeout]); + return ( + + ); +} + +export default DebounceSelect; diff --git a/src/components/SecondHeaderWrapper.jsx b/src/components/SecondHeaderWrapper.jsx new file mode 100644 index 0000000..9a4f7bd --- /dev/null +++ b/src/components/SecondHeaderWrapper.jsx @@ -0,0 +1,31 @@ +import { Outlet, useNavigate } from 'react-router-dom'; +import { Layout, Flex, theme, Spin, Divider } from 'antd'; +import BackBtn from './BackBtn'; + +const { Content, Header } = Layout; +const HeaderWrapper = ({ children, header, loading, ...props }) => { + const navigate = useNavigate(); + const { + token: { colorBgContainer }, + } = theme.useToken(); + return ( + <> + + +
+ + {/* {header} */} +
{header}
+ +
+
+ + + {children || } + +
+
+ + ); +}; +export default HeaderWrapper; diff --git a/src/config.js b/src/config.js index fc69f82..36a5ea0 100644 --- a/src/config.js +++ b/src/config.js @@ -18,7 +18,7 @@ export const PERM_ACCOUNT_MANAGEMENT = '/account/management' export const PERM_ACCOUNT_NEW = '/account/new' export const PERM_ACCOUNT_DISABLE = '/account/disable' export const PERM_ACCOUNT_RESET_PASSWORD = '/account/reset-password' -export const PERM_ROLE_NEW = '/account/role/new' +export const PERM_ROLE_NEW = '/account/role-new' // 海外供应商 // category: oversea @@ -31,3 +31,10 @@ export const PERM_DOMESTIC = '/domestic/all' // 机票供应商 // category: air-ticket export const PERM_AIR_TICKET = '/air-ticket/all' + +// 价格管理 +export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理 +export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 +export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入 +export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核 +export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入 diff --git a/src/hooks/usePresets.js b/src/hooks/useDatePresets.js similarity index 61% rename from src/hooks/usePresets.js rename to src/hooks/useDatePresets.js index aa046e6..2eb4c7d 100644 --- a/src/hooks/usePresets.js +++ b/src/hooks/useDatePresets.js @@ -1,8 +1,9 @@ import { useEffect, useState } from 'react'; import dayjs from "dayjs"; import { useTranslation } from 'react-i18next'; +import i18n from '@/i18n'; -const usePresets = () => { +export const useDatePresets = () => { const [presets, setPresets] = useState([]); const { t, i18n } = useTranslation(); @@ -39,4 +40,21 @@ const usePresets = () => { return presets; } -export default usePresets; +export const useWeekdays = () => { + const [data, setData] = useState([]); + const { t, i18n } = useTranslation(); + useEffect(() => { + const newData = [ + { value: '1', label: t('weekdays.1') }, + { value: '2', label: t('weekdays.2') }, + { value: '3', label: t('weekdays.3') }, + { value: '4', label: t('weekdays.4') }, + { value: '5', label: t('weekdays.5') }, + { value: '6', label: t('weekdays.6') }, + { value: '7', label: t('weekdays.7') }, + ]; + setData(newData); + return () => {}; + }, [i18n.language]); + return data; +}; diff --git a/src/hooks/useHTLanguageSets.js b/src/hooks/useHTLanguageSets.js new file mode 100644 index 0000000..73b827e --- /dev/null +++ b/src/hooks/useHTLanguageSets.js @@ -0,0 +1,14 @@ +export const useHTLanguageSets = () => { + const newData = [ + { key: '1', value: '1', label: 'English' }, + { key: '2', value: '2', label: 'Chinese (中文)' }, + { key: '3', value: '3', label: 'Japanese (日本語)' }, + { key: '4', value: '4', label: 'German (Deutsch)' }, + { key: '5', value: '5', label: 'French (Français)' }, + { key: '6', value: '6', label: 'Spanish (Español)' }, + { key: '7', value: '7', label: 'Russian (Русский)' }, + { key: '8', value: '8', label: 'Italian (Italiano)' }, + ]; + + return newData; +}; diff --git a/src/hooks/useProductsSets.js b/src/hooks/useProductsSets.js new file mode 100644 index 0000000..6f7fbe5 --- /dev/null +++ b/src/hooks/useProductsSets.js @@ -0,0 +1,122 @@ +import { useEffect, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import useAuthStore from '@/stores/Auth'; +import { PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'; + +/** + * 产品管理 相关的预设数据 + * 项目类型 + * * 酒店预定 1 + * * 火车 2 + * * 飞机票务 3 + * * 游船 4 + * * 快巴 5 + * * 旅行社(综费) 6 + * * 景点 7 + * * 特殊项目 8 + * * 其他 9 + * * 酒店 A + * * 超公里 B + * * 餐费 C + * * 小包价 D // 包价线路 + * * 站 X + * * 购物 S + * * 餐 R (餐厅) + * * 娱乐 E + * * 精华线路 T + * * 客人testimonial F + * * 线路订单 O + * * 省 P + * * 信息 I + * * 国家 G + * * 城市 K + * * 图片 H + * * 地图 M + * * 包价线路 L (已废弃) + * * 节日节庆 V + * * 火车站 N + * * 手机租赁 Z + * * ---- webht 类型, 20240624 新增HT类型 ---- + * * 导游 Q + * * 车费 J + */ + +export const useProductsTypes = (showAll = false) => { + const [types, setTypes] = useState([]); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const allItem = [{ label: t('All'), value: '', key: '' }]; + const newData = [ + { label: t('products:type.Experience'), value: '6', key: '6' }, + { label: t('products:type.UltraService'), value: 'B', key: 'B' }, + { label: t('products:type.Car'), value: 'J', key: 'J' }, + { label: t('products:type.Guide'), value: 'Q', key: 'Q' }, + { label: t('products:type.Attractions'), value: '7', key: '7' }, + { label: t('products:type.Meals'), value: 'R', key: 'R' }, + { label: t('products:type.Extras'), value: '8', key: '8' }, + { label: t('products:type.Package'), value: 'D', key: 'D' }, + ]; + const res = showAll ? [...allItem, ...newData] : newData; + setTypes(res); + }, [i18n.language]); + + return types; +}; +export const useProductsTypesMapVal = (value) => { + const stateSets = useProductsTypes(); + const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {}); + return stateMapVal; +}; + +export const useProductsAuditStates = () => { + const [types, setTypes] = useState([]); + const { t, i18n } = useTranslation(); + + useEffect(() => { + const newData = [ + { key: '-1', value: '-1', label: t('products:auditState.New'), color: 'muted' }, + { key: '0', value: '0', label: t('products:auditState.Pending'), color: '' }, + { key: '2', value: '2', label: t('products:auditState.Approved'), color: 'primary' }, + { key: '3', value: '3', label: t('products:auditState.Rejected'), color: 'danger' }, + { key: '1', value: '1', label: t('products:auditState.Published'), color: 'primary' }, + // ELSE 未知 + ]; + setTypes(newData); + }, [i18n.language]); + + return types; +}; + +export const useProductsAuditStatesMapVal = (value) => { + const stateSets = useProductsAuditStates(); + const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {}); + return stateMapVal; +}; + +/** + * @ignore + */ +export const useProductsTypesFieldsets = (type) => { + const [isPermitted] = useAuthStore((state) => [state.isPermitted]); + const infoDefault = [['code'], ['title']]; + const infoAdmin = ['remarks', 'dept', 'display_to_c']; + const infoTypesMap = { + '6': [[],[]], + 'B': [['city_id', 'km'], []], + 'J': [['city_id', 'recommends_rate', 'duration', 'display_to_c'], ['description',]], + 'Q': [['city_id', 'duration', ], ['description',]], + 'D': [['city_id', 'recommends_rate','duration',], ['description',]], + '7': [['city_id', 'recommends_rate', 'duration', 'display_to_c', 'open_weekdays'], ['description',]], // todo: 怎么是2个图 + 'R': [['city_id',], ['description',]], + '8': [[],[]], // todo: ? + }; + const thisTypeFieldset = (_type) => { + const adminSet = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? infoAdmin : []; + return [ + [...infoDefault[0], ...infoTypesMap[_type][0], ...adminSet], + [...infoDefault[1], ...infoTypesMap[_type][1]] + ]; + }; + return thisTypeFieldset(type); +} diff --git a/src/i18n/LanguageSwitcher.jsx b/src/i18n/LanguageSwitcher.jsx index ac1f009..f09a07e 100644 --- a/src/i18n/LanguageSwitcher.jsx +++ b/src/i18n/LanguageSwitcher.jsx @@ -1,13 +1,30 @@ -import React, { useState } from 'react'; -import { Dropdown, Menu } from 'antd'; +import { useState, useEffect } from 'react'; +import { Dropdown } from 'antd'; import { useTranslation } from 'react-i18next'; +import { appendRequestParams } from '@/utils/request'; +const i18n_to_htcode = { + 'zh': 2, + 'en': 1, +}; + +export const useDefaultLgc = () => { + const { i18n } = useTranslation(); + return { language: i18n_to_htcode[i18n.language], }; +}; /** * 语言选择组件 */ const Language = () => { - const { t, i18n } = useTranslation(); +const { t, i18n } = useTranslation(); const [selectedKeys, setSelectedKeys] = useState([i18n.language]); + + useEffect(() => { + appendRequestParams('lgc', i18n_to_htcode[i18n.language]); + + return () => {}; + }, [i18n.language]); + // 切换语言事件 const handleChangeLanguage = ({ key }) => { setSelectedKeys([key]); diff --git a/src/i18n/index.js b/src/i18n/index.js index 9efd3bd..edd3f75 100644 --- a/src/i18n/index.js +++ b/src/i18n/index.js @@ -17,7 +17,7 @@ i18n backend: { loadPath: '/locales/{{lng}}/{{ns}}.json', }, - ns: ['common', 'group', 'vendor', 'account'], + ns: ['common', 'group', 'vendor', 'account', 'products'], defaultNS: 'common', detection: { // convertDetectedLanguage: 'Iso15897', diff --git a/src/main.jsx b/src/main.jsx index 9114460..ec6f14b 100644 --- a/src/main.jsx +++ b/src/main.jsx @@ -1,95 +1,110 @@ -import React from "react"; -import { configure } from "mobx"; -import ReactDOM from "react-dom/client"; +import React from 'react' +import ReactDOM from 'react-dom/client' import { createBrowserRouter, RouterProvider, -} from "react-router-dom"; -import "@/assets/global.css"; -import App from "@/views/App"; -import Standlone from "@/views/Standlone"; -import Login from "@/views/Login"; -import Logout from "@/views/Logout"; -import Index from "@/views/index"; -import ErrorPage from "@/components/ErrorPage"; +} from 'react-router-dom' +import '@/assets/global.css' +import App from '@/views/App' +import Standlone from '@/views/Standlone' +import Login from '@/views/Login' +import Logout from '@/views/Logout' +import ErrorPage from '@/components/ErrorPage' import RequireAuth from '@/components/RequireAuth' -import ReservationNewest from "@/views/reservation/Newest"; -import ReservationDetail from "@/views/reservation/Detail"; -import ChangePassword from "@/views/account/ChangePassword"; -import AccountProfile from "@/views/account/Profile"; -import AccountManagement from "@/views/account/Management"; -import RoleList from "@/views/account/RoleList"; -import FeedbackIndex from "@/views/feedback/Index"; -import FeedbackDetail from "@/views/feedback/Detail"; -import FeedbackCustomerDetail from "@/views/feedback/CustomerDetail"; -import ReportIndex from "@/views/report/Index"; -import NoticeIndex from "@/views/notice/Index"; -import NoticeDetail from "@/views/notice/Detail"; -import InvoiceIndex from "@/views/invoice/Index"; -import InvoiceDetail from "@/views/invoice/Detail"; -import InvoicePaid from "@/views/invoice/Paid"; -import InvoicePaidDetail from "@/views/invoice/PaidDetail"; -import Airticket from "@/views/airticket/Index"; -import AirticketPlan from "@/views/airticket/Plan"; +import ReservationNewest from '@/views/reservation/Newest' +import ReservationDetail from '@/views/reservation/Detail' +import ChangePassword from '@/views/account/ChangePassword' +import AccountProfile from '@/views/account/Profile' +import AccountManagement from '@/views/account/Management' +import RoleList from '@/views/account/RoleList' +import FeedbackIndex from '@/views/feedback/Index' +import FeedbackDetail from '@/views/feedback/Detail' +import FeedbackCustomerDetail from '@/views/feedback/CustomerDetail' +import ReportIndex from '@/views/report/Index' +import NoticeIndex from '@/views/notice/Index' +import NoticeDetail from '@/views/notice/Detail' +import InvoiceIndex from '@/views/invoice/Index' +import InvoiceDetail from '@/views/invoice/Detail' +import InvoicePaid from '@/views/invoice/Paid' +import InvoicePaidDetail from '@/views/invoice/PaidDetail' +import Airticket from '@/views/airticket/Index' +import AirticketPlan from '@/views/airticket/Plan' import { ThemeContext } from '@/stores/ThemeContext' +import { usingStorage } from '@/hooks/usingStorage' +import useAuthStore from './stores/Auth' +import { isNotEmpty } from '@/utils/commons' -import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET } from '@/config' +import ProductsManage from '@/views/products/Manage'; +import ProductsDetail from '@/views/products/Detail'; +import ProductsAudit from '@/views/products/Audit'; +import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config' -import './i18n'; +import './i18n' -configure({ - useProxies: "ifavailable", - enforceActions: "observed", - computedRequiresReaction: true, - observableRequiresReaction: false, - reactionRequiresObservable: true, - disableErrorBoundaries: process.env.NODE_ENV == "production" -}); +const { createRoot } = ReactDOM -const router = createBrowserRouter([ - { - path: "/", - element: , - errorElement: , - children: [ - { index: true, element: }, - { path: "account/change-password", element: }, - { path: "account/profile", element: }, - { path: "account/management", element: }, - { path: "account/role-list", element: }, - { path: "reservation/newest", element: }, - { path: "reservation/:reservationId", element: }, - { path: "feedback", element: }, - { path: "feedback/:GRI_SN/:CII_SN/:RefNo", element: }, - { path: "feedback/:GRI_SN/:RefNo", element: }, - { path: "report", element: }, - { path: "notice", element: }, - { path: "notice/:CCP_BLID", element: }, - { path: "invoice",element:}, - { path: "invoice/detail/:GMDSN/:GSN",element:}, - { path: "invoice/paid",element:}, - { path: "invoice/paid/detail/:flid", element: }, - { path: "airticket",element: }, - { path: "airticket/plan/:coli_sn",element:}, - ] - }, - { - element: , - children: [ - { path: "/login", element: }, - { path: "/logout", element: }, - ] +const initRouter = async () => { + return createBrowserRouter([ + { + path: '/', + element: , + errorElement: , + children: [ + { index: true, element: }, + { path: 'account/change-password', element: }, + { path: 'account/profile', element: }, + { path: 'account/management', element: }, + { path: 'account/role-list', element: }, + { path: 'reservation/newest', element: }, + { path: 'reservation/:reservationId', element: }, + { path: 'feedback', element: }, + { path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: }, + { path: 'feedback/:GRI_SN/:RefNo', element: }, + { path: 'report', element: }, + { path: 'notice', element: }, + { path: 'notice/:CCP_BLID', element: }, + { path: 'invoice',element:}, + { path: 'invoice/detail/:GMDSN/:GSN',element:}, + { path: 'invoice/paid',element:}, + { path: 'invoice/paid/detail/:flid', element: }, + { path: 'airticket',element: }, + { path: 'airticket/plan/:coli_sn',element:}, + { path: "products",element: }, + { path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:}, + { path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:}, + { path: "products/edit",element:}, + ] + }, + { + element: , + children: [ + { path: '/login', element: }, + { path: '/logout', element: }, + ] + } + ]) +} + +const initAppliction = async () => { + + const { loginToken, userId } = usingStorage() + + if (isNotEmpty(userId) && isNotEmpty(loginToken)) { + await useAuthStore.getState().initAuth() } -]); + const router = await initRouter() + + createRoot(document.getElementById('root')).render( + // + +
Loading...
} + /> +
+ //
+ ) +} -ReactDOM.createRoot(document.getElementById("root")).render( - // - -
Loading...
} - /> -
- //
-); +initAppliction() diff --git a/src/pageSpy/index.jsx b/src/pageSpy/index.jsx index 5cf6ac9..10c4c03 100644 --- a/src/pageSpy/index.jsx +++ b/src/pageSpy/index.jsx @@ -2,11 +2,15 @@ import { loadScript } from '@/utils/commons'; import { PROJECT_NAME } from '@/config'; export const loadPageSpy = (title) => { + + if (import.meta.env.DEV || window.$pageSpy) return + const PageSpySrc = [ 'https://page-spy.mycht.cn/page-spy/index.min.js', 'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js', 'https://page-spy.mycht.cn/plugin/rrweb/index.min.js', ]; + Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => { // 注册插件 PageSpy.registerPlugin(new DataHarborPlugin({ maximum: 2 * 1024 * 1024 })); diff --git a/src/stores/Account.js b/src/stores/Account.js index a2391be..f11f545 100644 --- a/src/stores/Account.js +++ b/src/stores/Account.js @@ -1,5 +1,6 @@ import { create } from 'zustand' import { fetchJSON, postForm } from '@/utils/request' +import { isEmpty, isNotEmpty } from '@/utils/commons' import { HT_HOST } from "@/config" import { usingStorage } from '@/hooks/usingStorage' @@ -10,6 +11,13 @@ export const postAccountStatus = async (formData) => { return errcode !== 0 ? {} : result } +export const postAccountPassword = async (formData) => { + + const { errcode, result } = await postForm( + `${HT_HOST}/service-CooperateSOA/reset_account_password`, formData) + return errcode !== 0 ? {} : result +} + export const fetchAccountList = async (params) => { const { errcode, result } = await fetchJSON( @@ -38,49 +46,90 @@ export const fetchRoleList = async () => { return errcode !== 0 ? {} : result } +export const fetchPermissionList = async () => { + + const { errcode, result } = await fetchJSON( + `${HT_HOST}/service-CooperateSOA/get_all_permission_list`) + return errcode !== 0 ? {} : result +} + +export const fetchPermissionListByRoleId = async (params) => { + + const { errcode, result } = await fetchJSON( + `${HT_HOST}/service-CooperateSOA/get_role_permission_list`, params) + return errcode !== 0 ? {} : result +} + +export const fetchTravelAgencyByName = async (name) => { + + const { errcode, result } = await fetchJSON( + `${HT_HOST}/Service_BaseInfoWeb/VendorList`, {q: name}) + return errcode !== 0 ? {} : result +} + const useAccountStore = create((set, get) => ({ accountList: [], - selectedAccount: null, + toggleAccountStatus: async (userId, status) => { - selectAccount: (account) => { - set(() => ({ - selectedAccount: account - })) + const statusValue = status ? 'enable' : 'disable' + + const formData = new FormData() + formData.append('lmi_sn', userId) + formData.append('account_status', statusValue) + + return postAccountStatus(formData) }, - disableAccount: async (accountId) => { + resetAccountPassword: async (userId, password) => { const formData = new FormData() - formData.append('wu_id', accountId) - formData.append('account_status', 'enable') + formData.append('lmi_sn', userId) + formData.append('newPassword', password) - const result = await postAccountStatus(formData) + return postAccountPassword(formData) + }, + + newEmptyRole: () => { + return { + role_id: null, + role_name: '', + role_ids: '' + } + }, - console.info(result) + newEmptyAccount: () => { + return { + accountId: null, + userId: null, + lmi2_sn: null, + username: '', + realname: '', + email: '', + travelAgencyId: null, + roleId: '' + } }, saveOrUpdateRole: async (formValues) => { const formData = new FormData() formData.append('role_id', formValues.role_id) formData.append('role_name', formValues.role_name) - formData.append('res_ids', '2,3') + formData.append('res_ids', formValues.res_array.join(',')) return postRoleForm(formData) }, saveOrUpdateAccount: async (formValues) => { - const { selectedAccount } = get() const { userId } = usingStorage() const formData = new FormData() - formData.append('wu_id', selectedAccount.userId) - formData.append('lmi_sn', selectedAccount.lmi_sn) - formData.append('lmi2_sn', selectedAccount.lmi2_sn) + formData.append('wu_id', formValues.accountId) + formData.append('lmi_sn', formValues.userId) + formData.append('lmi2_sn', formValues.lmi2_sn) formData.append('user_name', formValues.username) formData.append('real_name', formValues.realname) formData.append('email', formValues.email) - formData.append('travel_agency_id', formValues.travelAgencyId) formData.append('roles', formValues.roleId) @@ -90,10 +139,13 @@ const useAccountStore = create((set, get) => ({ }, searchAccountByCriteria: async (formValues) => { - + let travel_agency_ids = null + if (isNotEmpty(formValues.agency)) { + travel_agency_ids = formValues.agency.map((ele) => ele.key).join(',') + } const searchParams = { username: formValues.username, - realname: formValues.realname, + travel_agency_ids: travel_agency_ids, lgc: 2 } @@ -101,16 +153,18 @@ const useAccountStore = create((set, get) => ({ const mapAccoutList = resultArray.map((r) => { return { - userId: r.wu_id, - lmi_sn: r.lmi_sn, + accountId: r.wu_id, + userId: r.lmi_sn, lmi2_sn: r.lmi2_sn, username: r.user_name, realname: r.real_name, email: r.email, lastLogin: r.wu_lastlogindate, - travelAgency: r.travel_agency_name, + travelAgencyName: r.travel_agency_name, travelAgencyId: r.travel_agency_id, - roleId: r.roles, + disabled: r.wu_limitsign, + // 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。 + roleId: isEmpty(r.roles) ? 0 : parseInt(r.roles), role: r.roles_name, } }) diff --git a/src/stores/Auth.js b/src/stores/Auth.js index fe0900e..ef05fdc 100644 --- a/src/stores/Auth.js +++ b/src/stores/Auth.js @@ -7,7 +7,8 @@ import { usingStorage } from '@/hooks/usingStorage' const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN' const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID' const KEY_USER_ID = 'G-INT:USER_ID' -const KEY_USER_DETAIL = 'G-JSON:USER_DETAIL' + +const WILDCARD_TOKEN = '*' export const fetchLoginToken = async (username, password) => { @@ -28,86 +29,115 @@ export const fetchUserDetail = async (loginToken) => { return errcode !== 0 ? {} : Result } +export const fetchPermissionListByUserId = async (userId) => { + + const { errcode, result } = await fetchJSON( + `${HT_HOST}/service-CooperateSOA/get_account_permission_list`, { lmi_sn: userId}) + return errcode !== 0 ? {} : result +} + async function fetchLastRequet() { const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`) return errcode !== 0 ? {} : result } +const initialState = { + tokenInterval: null, + tokenTimeout: true, + loginStatus: 0, + defaltRoute: '', + currentUser: { + username: '', + realname: '', + rolesName: '', + emailAddress: '', + travelAgencyName: '', + }, + permissionList: [] +} + const useAuthStore = create((set, get) => ({ - tokenInterval: null, + ...initialState, - tokenTimeout: false, + initAuth: async () => { + const { startTokenInterval, loadUserPermission } = get() + const { setStorage, loginToken } = usingStorage() - loginStatus: 0, + const userJson = await fetchUserDetail(loginToken) - loginUser: { - token: '', - telephone: '', - emailAddress: '', - cityId: 0, - permissionList: [], - }, + appendRequestParams('token', loginToken) + appendRequestParams('lmi_sn', userJson.LMI_SN) - isPermitted: (perm) => { - return true - // 以上是 Hardcode 判断 - // 以下是权限列表从数据库读取后使用的方法 - // return this.permissionList.some((value, key, arry) => { - // if (value.indexOf(WILDCARD_TOKEN) > -1) { - // return true - // } - // if (value === perm) { - // return true - // } - // return false - // }) + setStorage(KEY_USER_ID, userJson.LMI_SN) + setStorage(KEY_TRAVEL_AGENCY_ID, userJson.LMI_VEI_SN) + await loadUserPermission(userJson.LMI_SN) + + set(() => ({ + currentUser: { + username: userJson.LoginName, + realname: userJson.real_name, + rolesName: userJson.roles_name, + emailAddress: userJson.LMI_listmail, + travelAgencyName: userJson.VName, + } + })) + + startTokenInterval() + loadPageSpy(userJson.real_name) }, - validateUserPassword: async (usr, pwd) => { - const { startTokenInterval } = get() + authenticate: async (usr, pwd) => { + const { initAuth } = get() const { setStorage } = usingStorage() const { token: loginToken } = await fetchLoginToken(usr, pwd) - const userDetail = await fetchUserDetail(loginToken) + setStorage(KEY_LOGIN_TOKEN, loginToken) + + await initAuth() set(() => ({ - loginUser: { - telephone: userDetail.LkPhone, - emailAddress: userDetail.LMI_listmail, - cityId: userDetail.citysn, - }, tokenTimeout: false, loginStatus: 302 })) + }, - setStorage(KEY_LOGIN_TOKEN, loginToken) - setStorage(KEY_USER_ID, userDetail.LMI_SN) - setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN) - setStorage(KEY_USER_DETAIL, {username: userDetail.LoginName, travelAgencyName: userDetail.VName}) - appendRequestParams('token', loginToken) - // loadPageSpy(`${json.Result.VName}-${json.Result.LoginName}`) - startTokenInterval() + loadUserPermission: async(userId) => { + let deaultPage = '/' + const permissionResult = await fetchPermissionListByUserId(userId) + const pageList = permissionResult.filter(p => { + return p.res_category === 'page' + }) + if (pageList.length > 0) { + const resPattern = pageList[0].res_pattern + const splitResult = resPattern.split('=') + if (splitResult.length > 1) + deaultPage = splitResult[1] + } + + set(() => ({ + defaultRoute: deaultPage, + permissionList: permissionResult.map(p => p.res_pattern) + })) }, logout: () => { - const { tokenInterval } = get() + const { tokenInterval, currentUser } = get() const { clearStorage } = usingStorage() clearStorage() clearInterval(tokenInterval) set(() => ({ - loginUser: { - }, - loginStatus: 0, - tokenInterval: null, - tokenTimeout: true + ...initialState, + currentUser: { + username: currentUser.username + } })) }, startTokenInterval: () => { - const { loginTimeout } = get() + const { logout } = get() async function checkTokenTimeout() { const { LastReqDate } = await fetchLastRequet() @@ -115,43 +145,55 @@ const useAuthStore = create((set, get) => ({ const now = new Date() const diffTime = now.getTime() - lastReqDate.getTime() const diffHours = diffTime/1000/60/60 - if (diffHours > 4) { - loginTimeout() + if (diffHours > 1) { + logout() } } - const interval = setInterval(() => checkTokenTimeout(), 1000*60*20) + const interval = setInterval(() => checkTokenTimeout(), 1000*60*10) set(() => ({ tokenInterval: interval })) }, - loginTimeout: () => { - const { tokenInterval } = get() - // TODO: 这里没有清理 token,刷新后可以正常使用系统 - clearInterval(tokenInterval) - set(() => ({ - tokenTimeout: true - })) - }, - + // TODO: 迁移到 Account.js changeUserPassword: (password, newPassword) => { const { userId } = usingStorage() - const formData = new FormData(); - formData.append('UserID', userId); - formData.append('Password', password); - formData.append('NewPassword', newPassword); - const postUrl = HT_HOST + '/service-CooperateSOA/SetPassword'; + const formData = new FormData() + formData.append('UserID', userId) + formData.append('Password', password) + formData.append('NewPassword', newPassword) + const postUrl = HT_HOST + '/service-CooperateSOA/SetPassword' return postForm(postUrl, formData) .then(json => { if (json.errcode == 0) { - return json; + return json } else { - throw new Error(json.errmsg + ': ' + json.errcode); + throw new Error(json.errmsg + ': ' + json.errcode) } - }); + }) + }, + + isPermitted: (perm) => { + const { permissionList } = get() + // 测试权限使用: + // if (perm === '/account/management') return false + // if (perm === '/account/role/new') return false + // return true + // 以上是 Hardcode 判断 + // 以下是权限列表从数据库读取后使用的方法 + return permissionList.some((value) => { + if (value.indexOf(WILDCARD_TOKEN) == 0) { + return true + } + if (value === perm) { + return true + } + return false + }) }, + })) -export default useAuthStore \ No newline at end of file +export default useAuthStore diff --git a/src/stores/Feedback.js b/src/stores/Feedback.js index dd3feb1..9b2356e 100644 --- a/src/stores/Feedback.js +++ b/src/stores/Feedback.js @@ -1,4 +1,3 @@ -import { makeAutoObservable, runInAction } from 'mobx'; import { fetchJSON, postForm } from '@/utils/request'; import { groupBy } from '@/utils/commons'; import * as config from '@/config'; diff --git a/src/stores/Invoice.js b/src/stores/Invoice.js index 814e69f..09ac0f2 100644 --- a/src/stores/Invoice.js +++ b/src/stores/Invoice.js @@ -1,10 +1,5 @@ -import { makeAutoObservable, runInAction } from "mobx"; import { fetchJSON, postForm } from "@/utils/request"; -import { prepareUrl, isNotEmpty, objectMapper } from "@/utils/commons"; import { HT_HOST } from "@/config"; -import { json } from "react-router-dom"; -import * as config from "@/config"; -import dayjs from "dayjs"; import { create } from 'zustand'; import { devtools } from 'zustand/middleware'; @@ -138,366 +133,3 @@ const useInvoiceStore = create( ); export default useInvoiceStore; - -export class Invoice { - constructor(root) { - makeAutoObservable(this, { rootStore: false }); - this.root = root; - } - - invoiceList = []; //账单列表 - invoicekImages = []; //图片列表 - invoiceGroupInfo = {}; //账单详细 - invoiceProductList = []; //账单细项 - invoiceZDDetail = []; //报账信息 - invoiceCurrencyList = []; //币种 - invoicePicList = []; //多账单图片列表数组 - invoiceFormData = { info_money: 0, info_Currency: "", info_date: "" }; //存储form数据 - - invoicePaid = [] ; //支付账单列表 - invoicePaidDetail = []; //每期账单详细 - - loading = false; - search_date_start = dayjs().subtract(2, "M").startOf("M"); - search_date_end = dayjs().endOf("M"); - - onDateRangeChange = dates => { - console.log(dates); - this.search_date_start = dates==null? null: dates[0]; - this.search_date_end = dates==null? null: dates[1]; - }; - - fetchInvoiceList(VEI_SN, GroupNo, DateStart, DateEnd,OrderType) { - this.loading = true; - const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTSearchGMBPageList") - .append("VEI_SN", VEI_SN) - .append("OrderType", 0) - .append("GroupNo", GroupNo.trim()) - .append("DateStart", DateStart) - .append("DateEnd", DateEnd) - .append("Orderbytype", 1) - .append("TimeType", 0) - .append("limitmarket", "") - .append("mddgroup", "") - .append("SecuryGroup", "") - .append("TotalNum", 0) - .append("PageSize", 2000) - .append("PageIndex", 1) - .append("PayState",OrderType) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - this.loading = false; - if (json.errcode == 0) { - if (isNotEmpty(json.Result)) { - this.invoiceList = json.Result.map((data, index) => { - return { - key: data.GMDSN, - gmd_gri_sn: data.GMD_GRI_SN, - gmd_vei_sn: data.GMD_VEI_SN, - GetGDate: data.GetGDate, - GMD_FillWorkers_SN: data.GMD_FillWorkers_SN, - GMD_FWks_LastEditTime: data.GMD_FWks_LastEditTime, - GMD_VerifyUser_SN: data.GMD_VerifyUser_SN, - GMD_Dealed: data.GMD_Dealed, - GMD_VRequestVerify: data.GMD_VRequestVerify, - LeftGDate: data.LeftGDate, - GMD_FillWorkers_Name: data.GMD_FillWorkers_Name, - GroupName: data.GroupName, - AllMoney: data.AllMoney, - PersonNum: data.PersonNum, - GMD_Currency: data.GMD_Currency, - VName: data.VName, - FKState: data.FKState, - }; - }); - } else { - this.invoiceList = []; - } - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - }); - } - - fetchInvoiceDetail(GMDSN, GSN) { - const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTGetZDDetail") - .append("VEI_SN", this.root.authStore.login.travelAgencyId) - .append("GRI_SN", GSN) - .append("GMD_SN", GMDSN) - .append("LGC", 1) - .append("Bill", 1) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - if (json.errcode == 0) { - this.invoiceGroupInfo = json.GroupInfo[0]; - this.invoiceProductList = json.ProductList; - this.invoiceCurrencyList = json.CurrencyList; - this.invoiceZDDetail = json.ZDDetail; - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - return json; - }); - } - - //获取供应商提交的图片 - getInvoicekImages(VEI_SN, GRI_SN) { - let url = `/service-fileServer/ListFile`; - url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}&FilePathName=invoice`; - url += `&token=${this.root.authStore.login.token}`; - fetch(config.HT_HOST + url) - .then(response => response.json()) - .then(json => { - console.log(json); - runInAction(() => { - this.invoicekImages = json.result.map((data, index) => { - return { - uid: -index, //用负数,防止添加删除的时候错误 - name: data.file_name, - status: "done", - url: data.file_url, - }; - }); - }); - }) - .catch(error => { - console.log("fetch data failed", error); - }); - } - - //从数据库获取图片列表 - getInvoicekImages_fromData(jsonData) { - let arrLen = jsonData.length; - let arrPicList = jsonData.map((data, index) => { - const GMD_Pic = data.GMD_Pic; - let picList = []; - if (isNotEmpty(GMD_Pic)) { - let js_Pic = JSON.parse(GMD_Pic); - picList = js_Pic.map((picData, pic_Index) => { - return { - uid: -pic_Index, //用负数,防止添加删除的时候错误 - name: "", - status: "done", - url: picData.url, - }; - }); - } - if (data.GMD_Dealed == false && arrLen == index + 1) { - this.invoicekImages = picList; - } - return picList; - }); - - runInAction(() => { - this.invoicePicList = arrPicList; - }); - } - - //获取数据库的表单默认数据回填。 - getFormData(jsonData) { - let arrLen = jsonData.length; - return jsonData.map((data, index) => { - if (data.GMD_Dealed == false && arrLen == index + 1) { - //只有最后一条账单未审核通过才显示 - runInAction(() => { - this.invoiceFormData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "" }; - }); - } - }); - } - - removeFeedbackImages(fileurl) { - let url = `/service-fileServer/FileDelete`; - url += `?fileurl=${fileurl}`; - url += `&token=${this.root.authStore.login.token}`; - return fetch(config.HT_HOST + url) - .then(response => response.json()) - .then(json => { - console.log(json); - return json.Result; - }) - .catch(error => { - console.log("fetch data failed", error); - }); - } - - postEditInvoiceDetail(GMD_SN, Currency, Cost, PayDate, Pic, Memo) { - let postUrl = HT_HOST + "/service-cusservice/EditSupplierFK"; - let formData = new FormData(); - formData.append("LMI_SN", this.root.authStore.login.userId); - formData.append("GMD_SN", GMD_SN); - formData.append("Currency", Currency); - formData.append("Cost", Cost); - formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : ""); - formData.append("Pic", Pic); - formData.append("Memo", Memo); - formData.append("token",this.root.authStore.login.token); - - return postForm(postUrl, formData).then(json => { - console.info(json); - return json; - }); - } - - postAddInvoice(GRI_SN, Currency, Cost, PayDate, Pic, Memo) { - let postUrl = HT_HOST + "/service-cusservice/AddSupplierFK"; - let formData = new FormData(); - formData.append("LMI_SN", this.root.authStore.login.userId); - formData.append("VEI_SN", this.root.authStore.login.travelAgencyId); - formData.append("GRI_SN", GRI_SN); - formData.append("Currency", Currency); - formData.append("Cost", Cost); - formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : ""); - formData.append("Pic", Pic); - formData.append("Memo", Memo); - formData.append("token",this.root.authStore.login.token); - return postForm(postUrl, formData).then(json => { - console.info(json); - return json; - }); - } - - //账单状态 - invoiceStatus(FKState) { - switch (FKState - 1) { - case 1: - return "Submitted"; - break; - case 2: - return "Travel Advisor"; - break; - case 3: - return "Finance Dept"; - break; - case 4: - return "Paid"; - break; - default: - return ""; - break; - } - } - - fetchInvoicePaid(VEI_SN, GroupNo, DateStart, DateEnd) { - this.loading = true; - const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaid") - .append("VEI_SN", VEI_SN) - .append("GroupNo", GroupNo) - .append("DateStart", DateStart) - .append("DateEnd", DateEnd) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - this.loading = false; - if (json.errcode == 0) { - if (isNotEmpty(json.Result)) { - this.invoicePaid = json.Result.map((data, index) => { - return { - key: data.fl_id, - fl_finaceNo: data.fl_finaceNo, - fl_vei_sn: data.fl_vei_sn, - fl_year: data.fl_year, - fl_month: data.fl_month, - fl_memo: data.fl_memo, - fl_adddate: data.fl_adddate, - fl_addUserSn: data.fl_addUserSn, - fl_updateUserSn: data.fl_updateUserSn, - fl_updatetime: data.fl_updatetime, - fl_state: data.fl_state, - fl_paid: data.fl_paid, - fl_pic: data.fl_pic, - fcount: data.fcount, - pSum: data.pSum, - }; - }); - } else { - this.invoicePaid = []; - } - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - }); - - } - - - fetchInvoicePaidDetail(VEI_SN,FLID){ - this.loading = true; - const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaidDetail") - .append("VEI_SN", VEI_SN) - .append("fl_id", FLID) - .append("token",this.root.authStore.login.token) - .build(); - - return fetchJSON(fetchUrl).then(json => { - runInAction(() => { - this.loading = false; - if (json.errcode == 0) { - if (isNotEmpty(json.Result)) { - this.invoicePaidDetail = json.Result.map((data, index) => { - return { - key: data.fl2_id, - fl2_fl_id: data.fl2_fl_id, - fl2_GroupName: data.fl2_GroupName, - fl2_gri_sn: data.fl2_gri_sn, - fl2_gmd_sn: data.fl2_gmd_sn, - fl2_wl: data.fl2_wl, - fl2_ArriveDate: data.fl2_ArriveDate, - fl2_price: data.fl2_price, - fl2_state: data.fl2_state, - fl2_updatetime: data.fl2_updatetime, - fl2_updateUserSn: data.fl2_updateUserSn, - fl2_memo: data.fl2_memo, - fl2_memo2: data.fl2_memo2, - fl2_paid: data.fl2_paid, - fl2_pic: data.fl2_pic, - }; - }); - } else { - this.invoicePaidDetail = []; - } - } else { - throw new Error(json.errmsg + ": " + json.errcode); - } - }); - }); - } - - /* 测试数据 */ - //账单列表范例数据 - testData = [ - { - GSMSN: 449865, - gmd_gri_sn: 334233, - gmd_vei_sn: 628, - GetDate: "2023-04-2 00:33:33", - GMD_FillWorkers_SN: 8617, - GMD_FWks_LastEditTime: "2023-04-26 12:33:33", - GMD_VerifyUser_SN: 8928, - GMD_Dealed: 1, - GMD_VRequestVerify: 1, - TotalCount: 22, - LeftGDate: "2023-03-30 00:00:00", - GMD_FillWorkers_Name: "", - GroupName: " 中华游230501-CA230402033", - AllMoney: 3539, - FKState: 1, - GMD_Currency: "", - PersonNum: "1大1小", - VName: "", - }, - ]; -} - -// export default Invoice; diff --git a/src/stores/Products/Index.js b/src/stores/Products/Index.js new file mode 100644 index 0000000..a06850e --- /dev/null +++ b/src/stores/Products/Index.js @@ -0,0 +1,145 @@ +import { create } from 'zustand'; +import { devtools } from 'zustand/middleware'; + +import { fetchJSON, postForm, postJSON } from '@/utils/request'; +import { HT_HOST } from '@/config'; +import { groupBy } from '@/utils/commons'; + +export const searchAgencyAction = async (param) => { + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param); + return errcode !== 0 ? [] : result; +}; + +/** + * 搜索所有产品, 返回产品列表 + * ! 只有审核通过, 已发布的 + * @param {object} params { keyword, use_year, product_types, travel_agency_id, city } + */ +export const searchPublishedProductsAction = async (param) => { + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/web_products_search`, param); + return errcode !== 0 ? [] : result; +}; + +export const copyAgencyDataAction = async (postbody) => { + const formData = new FormData(); + Object.keys(postbody).forEach((key) => { + formData.append(key, postbody[key]); + }); + const { errcode, result } = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_products_copy`, formData); + return errcode === 0 ? true : false; +}; + +export const getAgencyProductsAction = async (param) => { + const _param = { ...param, use_year: (param.use_year || '').replace('all', ''), audit_state: (param.audit_state || '').replace('all', '') }; + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/travel_agency_products`, _param); + return errcode !== 0 ? { agency: {}, products: [] } : result; +}; + +/** + * + */ +export const addProductExtraAction = async (body) => { + return true; // test: 先不更新到HT + const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body); + return errcode === 0 ? true : false; +}; + +/** + * + */ +export const delProductExtrasAction = async (body) => { + return true; // test: 先不更新到HT + const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_del`, body); + return errcode === 0 ? true : false; +}; + +/** + * 获取指定产品的附加项目 + * @param {object} param { id, travel_agency_id, use_year } + */ +export const getAgencyProductExtrasAction = async (param) => { + const _param = { ...param, use_year: (param.use_year || '').replace('all', '') }; + const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param); + return errcode !== 0 ? [] : result; +}; + +export const postProductsQuoteAuditAction = async (auditState, quoteRow) => { + const postbody = { + audit_state: auditState, + id: quoteRow.id, + travel_agency_id: quoteRow.travel_agency_id, + }; + const formData = new FormData(); + Object.keys(postbody).forEach((key) => { + formData.append(key, postbody[key]); + }); + const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/quotation_audit`, formData); + return json; + // return errcode !== 0 ? {} : result; +}; + +export const postProductsAuditAction = async (auditState, infoRow) => { + const postbody = { + audit_state: auditState, + id: infoRow.id, + travel_agency_id: infoRow.travel_agency_id, + }; + const formData = new FormData(); + Object.keys(postbody).forEach((key) => { + formData.append(key, postbody[key]); + }); + const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/travel-agency-products-audit`, formData); + return json; + // const { errcode, result } = json; + // return errcode !== 0 ? {} : result; +}; + +const initialState = { + loading: false, + searchValues: {}, + agencyList: [], + activeAgency: {}, + agencyProducts: {}, + editingProduct: {}, +}; +export const useProductsStore = create( + devtools((set, get) => ({ + // 初始化状态 + ...initialState, + + // state actions + setLoading: (loading) => set({ loading }), + setSearchValues: (searchValues) => set({ searchValues }), + setAgencyList: (agencyList) => set({ agencyList }), + setActiveAgency: (activeAgency) => set({ activeAgency }), + setAgencyProducts: (agencyProducts) => set({ agencyProducts }), + setEditingProduct: (editingProduct) => set({ editingProduct }), + + reset: () => set(initialState), + + // side effects + searchAgency: async (param) => { + const { setLoading, setAgencyList } = get(); + setLoading(true); + const res = await searchAgencyAction(param); + setAgencyList(res); + setLoading(false); + }, + + getAgencyProducts: async (param) => { + const { setLoading, setActiveAgency, setAgencyProducts } = get(); + setLoading(true); + const res = await getAgencyProductsAction(param); + const productsData = groupBy(res.products, (row) => row.info.product_type_id); + setAgencyProducts(productsData); + setActiveAgency(res.agency); + setLoading(false); + }, + + getAgencyProductExtras: async (param) => { + const res = await getAgencyProductExtrasAction(param); + // todo: + }, + })) +); +export default useProductsStore; diff --git a/src/stores/Reservation.js b/src/stores/Reservation.js index 8990ae7..2984fe2 100644 --- a/src/stores/Reservation.js +++ b/src/stores/Reservation.js @@ -8,7 +8,7 @@ export const fetchCityList = async (travelAgencyId, reservationId) => { const { errcode, Result } = await fetchJSON( `${HT_HOST}/service-cusservice/PTGetCityGuide`, { VEI_SN: travelAgencyId, GRI_SN: reservationId, LGC: 1 }) - return errcode !== 0 ? {} : Result; + return errcode !== 0 ? {} : Result } export const fetchPlanDetail = async (travelAgencyId, reservationId) => { @@ -31,7 +31,7 @@ export const fetchAttachList = async (reservationId) => { const { errcode, result } = await fetchJSON( `${HT_HOST}/service-fileServer/PlanChangeFileList`, { GRI_SN: reservationId }) - return errcode !== 0 ? {} : result; + return errcode !== 0 ? {} : result } const useReservationStore = create((set, get) => ({ @@ -91,26 +91,26 @@ const useReservationStore = create((set, get) => ({ })) }, - fetchReservationList: (formVal, current=1) => { + fetchReservationList: (formValues, current=1) => { const { travelAgencyId } = usingStorage() const { reservationPage } = get() // 设置为 0,后端会重新计算总数,当跳转第 X 页时可用原来的总数。 - const totalNum = current == 1 ? 0 : reservationPage.total; + const totalNum = current == 1 ? 0 : reservationPage.total const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/GetPlanSearchList') .append('VEI_SN', travelAgencyId) - .append('GroupNo', formVal.referenceNo) - .append('DateStart', formVal.startdate) - .append('DateEnd', formVal.enddate) - .append('NotConfirm', '')//status)// Todo: 待解决 + .append('GroupNo', formValues.referenceNo) + .append('DateStart', formValues.startdate) + .append('DateEnd', formValues.enddate) + .append('NotConfirm', formValues.unconfirmed) .append('TotalNum', totalNum) .append('PageSize', reservationPage.size) .append('PageIndex', current) - .build(); + .build() return fetchJSON(fetchUrl) .then(json => { if (json.errcode == 0) { - const mapReservationList = (json?.Result??[]).map((data, index) => { + const mapReservationList = (json?.Result??[]).map((data) => { return { key: data.vas_gri_sn, reservationId: data.vas_gri_sn, @@ -132,32 +132,32 @@ const useReservationStore = create((set, get) => ({ } })) } else { - throw new Error(json.errmsg + ': ' + json.errcode); + throw new Error(json.errmsg + ': ' + json.errcode) } - }); + }) }, fetchAllGuideList: () => { const { travelAgencyId } = usingStorage() const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetGuideList') .append('VEI_SN', travelAgencyId) - .build(); + .build() return fetchJSON(fetchUrl) .then(json => { if (json.errcode == 0) { - const guideList = (json?.Result??[]).map((data, index) => { + const guideList = (json?.Result??[]).map((data) => { return { guideId: data.TGI_SN, guideName: data.TGI2_Name, mobileNo: data.TGI_Mobile } - }); - return guideList; + }) + return guideList } else { - throw new Error(json.errmsg + ': ' + json.errcode); + throw new Error(json.errmsg + ': ' + json.errcode) } - }); + }) }, getReservationDetail: async (reservationId) => { @@ -167,8 +167,8 @@ const useReservationStore = create((set, get) => ({ const mapConfirmationList = planChangeList.map((data) => { const filterAttchList = attachListJson.filter(attch => { - return attch.PCI_SN === data.PCI_SN; - }); + return attch.PCI_SN === data.PCI_SN + }) return { key: data.PCI_SN, PCI_Changetext: data.PCI_Changetext, @@ -209,7 +209,7 @@ const useReservationStore = create((set, get) => ({ getReservationDetail(travelAgencyId, reservationDetail.reservationId) return json } - }); + }) }, setupCityGuide: (cityId, guideId) => { @@ -229,7 +229,7 @@ const useReservationStore = create((set, get) => ({ if (json.errcode != 0) { throw new Error(json.errmsg + ': ' + json.errcode) } - }); + }) }, @@ -241,7 +241,7 @@ const useReservationStore = create((set, get) => ({ .append('VEI_SN', travelAgencyId) .append('GRI_SN', selectedReservation.reservationId) .append('LGC', 1) - .build(); + .build() return fetchJSON(fetchUrl) .then(json => { @@ -252,10 +252,6 @@ const useReservationStore = create((set, get) => ({ return data.GuideName }).join(',') - runInAction(() => { - selectedReservation.guide = reservationGuide - }) - set((state) => ({ selectedReservation: { ...state.selectedReservation, @@ -271,4 +267,4 @@ const useReservationStore = create((set, get) => ({ } })) -export default useReservationStore \ No newline at end of file +export default useReservationStore diff --git a/src/utils/commons.js b/src/utils/commons.js index 2e83226..29fd1d6 100644 --- a/src/utils/commons.js +++ b/src/utils/commons.js @@ -255,23 +255,47 @@ export function omit(object, keysToOmit) { /** * 深拷贝 */ -export function cloneDeep(value) { - // return structuredClone(value); - if (typeof value !== "object" || value === null) { - return value; - } - - const result = Array.isArray(value) ? [] : {}; - - for (const key in value) { - if (Object.prototype.hasOwnProperty.call(value, key)) { - result[key] = cloneDeep(value[key]); - } - } - - return result; +export function cloneDeep(value, visited = new WeakMap()) { + // 处理循环引用 + if (visited.has(value)) { + return visited.get(value); + } + + // 特殊对象和基本类型处理 + if (value instanceof Date) { + return new Date(value); + } + if (value instanceof RegExp) { + return new RegExp(value.source, value.flags); + } + if (value === null || typeof value !== 'object') { + return value; + } + + // 创建一个新的WeakMap项以避免内存泄漏 + let result; + if (Array.isArray(value)) { + result = []; + visited.set(value, result); + } else { + result = {}; + visited.set(value, result); + } + + for (const key of Object.getOwnPropertySymbols(value)) { + // 处理Symbol属性 + result[key] = cloneDeep(value[key], visited); + } + + for (const key in value) { + if (Object.prototype.hasOwnProperty.call(value, key)) { + // 处理普通属性 + result[key] = cloneDeep(value[key], visited); + } + } + + return result; } - /** * 向零四舍五入, 固定精度设置 */ diff --git a/src/utils/lifecycle.js b/src/utils/lifecycle.js new file mode 100644 index 0000000..3b896db --- /dev/null +++ b/src/utils/lifecycle.js @@ -0,0 +1,45 @@ +const initListener = [] +const authListener = [] + +export const addInitLinstener = (fn) => { + initListener.push(fn) +} + +export const addAuthLinstener = (fn) => { + authListener.push(fn) +} + +export const notifyInit = async () => { + for (const listener of initListener) { + await listener() + } +} + +export const notifyAuth = async (obj) => { + for (const listener of authListener) { + await listener(obj) + } +} + +// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段。 +// 失败,无法同步调用异步方法! +export const lifecycleware = (fn) => (set, get, store) => { + + addInitLinstener(() => { + if (store.getState().hasOwnProperty('onInit')) { + store.getState().onInit() + } else { + console.info('store has no function: onInit.') + } + }) + + addAuthLinstener(() => { + if (store.getState().hasOwnProperty('onAuth')) { + store.getState().onAuth() + } else { + console.info('store has no function: onAuth.') + } + }) + + return fn(set, get, store) +} \ No newline at end of file diff --git a/src/utils/request.js b/src/utils/request.js index ac97803..b99451d 100644 --- a/src/utils/request.js +++ b/src/utils/request.js @@ -113,8 +113,14 @@ export function postForm(url, data) { } export function postJSON(url, obj) { + const initParams = getRequestInitParams(); + const params4get = Object.assign({}, initParams); + const params = new URLSearchParams(params4get).toString(); + const ifp = url.includes('?') ? '&' : '?'; + const fUrl = params !== '' ? `${url}${ifp}${params}` : url; + const headerObj = getRequestHeader() - return fetch(url, { + return fetch(fUrl, { method: 'POST', body: JSON.stringify(obj), headers: { diff --git a/src/views/App.jsx b/src/views/App.jsx index 779993c..2e00414 100644 --- a/src/views/App.jsx +++ b/src/views/App.jsx @@ -4,8 +4,7 @@ import { Layout, Menu, ConfigProvider, theme, Dropdown, Space, Row, Col, Badge, import { DownOutlined } from '@ant-design/icons'; import 'antd/dist/reset.css'; import AppLogo from '@/assets/logo-gh.png'; -import { isEmpty } from '@/utils/commons'; -import { appendRequestParams } from '@/utils/request' +import { isEmpty, isNotEmpty } from '@/utils/commons'; import Language from '../i18n/LanguageSwitcher'; import { useTranslation } from 'react-i18next'; import zhLocale from 'antd/locale/zh_CN'; @@ -15,72 +14,68 @@ import ErrorBoundary from '@/components/ErrorBoundary' import { BUILD_VERSION, } from '@/config'; import useNoticeStore from '@/stores/Notice'; import useAuthStore from '@/stores/Auth' +import { useThemeContext } from '@/stores/ThemeContext' import { usingStorage } from '@/hooks/usingStorage' +import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config' + const { Header, Content, Footer } = Layout; const { Title } = Typography; function App() { - const { t, i18n } = useTranslation(); + + const { t, i18n } = useTranslation() + + const { colorPrimary } = useThemeContext() const [password, setPassword] = useState('') - const [validateUserPassword, tokenTimeout] = useAuthStore( - (state) => [state.validateUserPassword, state.tokenTimeout]) + const [authenticate, tokenTimeout, isPermitted, currentUser] = useAuthStore( + (state) => [state.authenticate, state.tokenTimeout, state.isPermitted, state.currentUser]) - const { loginToken, userDetail } = usingStorage() + const { loginToken } = usingStorage() const noticeUnRead = useNoticeStore((state) => state.noticeUnRead) const href = useHref() const navigate = useNavigate() - const location = useLocation() // 除了路由 /p...以外都需要登陆系统 const needToLogin = href !== '/login' && isEmpty(loginToken) - if (!needToLogin) { - appendRequestParams('token', loginToken) - } - useEffect(() => { if (needToLogin) { navigate('/login') } }, [href]) - useEffect(() => { - window.gtag('event', 'page_view', { page_location: window.location.href }); - }, [location]); - const onSubmit = () => { - validateUserPassword(userDetail?.username, password) + authenticate(currentUser?.username, password) .catch(ex => { console.error(ex) alert(t('Validation.LoginFailed')) }) setPassword('') - }; + } - const splitPath = href.split('/'); - let defaultPath = 'reservation'; + const splitPath = href.split('/') + let defaultPath = 'notice' if (splitPath.length > 1) { - defaultPath = splitPath[1]; + defaultPath = splitPath[1] } - const { - token: { colorBgContainer }, - } = theme.useToken(); - const [antdLng, setAntdLng] = useState(enLocale); useEffect(() => { setAntdLng(i18n.language === 'en' ? enLocale : zhLocale); - }, [i18n.language]); + }, [i18n.language]) + return ( @@ -99,32 +94,30 @@ function App() { setPassword(e.target.value)} onPressEnter={onSubmit} - addonBefore={userDetail?.username} /> + addonBefore={currentUser?.username} /> - -
+ +
- + - App logo + App logo {t('menu.Reservation')} }, - { key: 'invoice', label: {t('menu.Invoice')} }, - { key: 'feedback', label: {t('menu.Feedback')} }, - { key: 'report', label: {t('menu.Report')} }, - { key: 'airticket', label: {t('menu.Airticket')} }, + isPermitted(PERM_OVERSEA) ? { key: 'reservation', label: {t('menu.Reservation')} } : null, + isPermitted(PERM_OVERSEA) ? { key: 'invoice', label: {t('menu.Invoice')} } : null, + isPermitted(PERM_OVERSEA) ? { key: 'feedback', label: {t('menu.Feedback')} } : null, + isPermitted(PERM_OVERSEA) ? { key: 'report', label: {t('menu.Report')} } : null, + isPermitted(PERM_AIR_TICKET) ? { key: 'airticket', label: {t('menu.Airticket')} } : null, + isPermitted(PERM_PRODUCTS_MANAGEMENT) ? { key: 'products', label: {t('menu.Products')} } : null, { key: 'notice', label: ( @@ -137,10 +130,10 @@ function App() { ]} /> - - - {userDetail?.travelAgencyName} - + +

+ {currentUser?.travelAgencyName} +

{t('ChangePassword')}, key: '0' }, { label: {t('Profile')}, key: '1' }, - { label: {t('account:management.tile')}, key: '3' }, - { label: {t('account:management.roleList')}, key: '4' }, + isPermitted(PERM_ACCOUNT_MANAGEMENT) ? { label: {t('account:accountList')}, key: '3' } : null, + isPermitted(PERM_ROLE_NEW) ? { label: {t('account:roleList')}, key: '4' } : null, { type: 'divider' }, { label: {t('Logout')}, key: '99' }, - ], - { type: 'divider' }, - { label: <>v{BUILD_VERSION}, key: 'BUILD_VERSION' }, + ] ], }} trigger={['click']} > e.preventDefault()}> - {userDetail?.username} +
{currentUser?.realname}
@@ -172,21 +163,15 @@ function App() {
- + {needToLogin ? <>login... : } -
+
China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}
- ); + ) } export default App diff --git a/src/views/Index.jsx b/src/views/Index.jsx deleted file mode 100644 index 9034490..0000000 --- a/src/views/Index.jsx +++ /dev/null @@ -1,13 +0,0 @@ -export default function Index() { - return ( -

- Global Highlights Hub -
- Check out{" "} - - the docs at chinahighlights.com - - . -

- ); -} \ No newline at end of file diff --git a/src/views/Login.jsx b/src/views/Login.jsx index b973b96..43119f5 100644 --- a/src/views/Login.jsx +++ b/src/views/Login.jsx @@ -6,9 +6,8 @@ import useAuthStore from '@/stores/Auth' import useNoticeStore from '@/stores/Notice' function Login() { - const [validateUserPassword, loginStatus] = - useAuthStore((state) => [state.validateUserPassword, state.loginStatus]) - const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount) + const [authenticate, loginStatus, defaultRoute] = + useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute]) const { t, i18n } = useTranslation() const { notification } = App.useApp() @@ -17,12 +16,12 @@ function Login() { useEffect (() => { if (loginStatus === 302) { - navigate('/reservation/newest') + navigate(defaultRoute) } }, [loginStatus]) const onFinish = (values) => { - validateUserPassword(values.username, values.password) + authenticate(values.username, values.password) .catch(ex => { console.error(ex) notification.error({ @@ -39,20 +38,19 @@ function Login() { } return ( - +
- diff --git a/src/views/Standlone.jsx b/src/views/Standlone.jsx index 24acad3..6ce6113 100644 --- a/src/views/Standlone.jsx +++ b/src/views/Standlone.jsx @@ -1,52 +1,42 @@ import { Outlet } from 'react-router-dom' -import { Layout, ConfigProvider, theme, Typography, Row, Col, App as AntApp } from 'antd' +import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from 'antd' import 'antd/dist/reset.css' import AppLogo from '@/assets/logo-gh.png' +import { useThemeContext } from '@/stores/ThemeContext' import Language from '../i18n/LanguageSwitcher' +import { BUILD_VERSION, } from '@/config'; -const { Title } = Typography const { Header, Content, Footer } = Layout function Standlone() { - const { - token: { colorBgContainer }, - } = theme.useToken() + const { colorPrimary } = useThemeContext() return ( - -
+ +
- - App logo + + App logo - Global Highlights Hub +

Global Highlights Hub

- + -
+
China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}
diff --git a/src/views/account/ChangePassword.jsx b/src/views/account/ChangePassword.jsx index 11e63e5..9978c07 100644 --- a/src/views/account/ChangePassword.jsx +++ b/src/views/account/ChangePassword.jsx @@ -41,15 +41,13 @@ function ChangePassword() { return ( <> - + (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss')) }, { title: t('account:action'), @@ -141,59 +53,84 @@ function Management() { ) } - function roleRender(text) { - return ( - - ) - } - - function actionRender(text, account) { + function actionRender(_, account) { return ( - + { + showDisableConfirm(account, checked) + }} /> ) } - const onPermissionChange = (newValue) => { - console.log('onChange ', newValue) - setPermissionValue(newValue) - } - - const [permissionValue, setPermissionValue] = useState(['0-0-0']) const [isAccountModalOpen, setAccountModalOpen] = useState(false) - const [isRoleModalOpen, setRoleModalOpen] = useState(false) const [dataLoading, setDataLoading] = useState(false) const [roleAllList, setRoleAllList] = useState([]) + const [travelAgencyList, setTravelAgencyList] = useState([]) + const [currentTravelAgency, setCurrentTravelAgency] = useState(null) const [accountForm] = Form.useForm() - const [searchAccountByCriteria, accountList, disableAccount, selectedAccount, saveOrUpdateAccount, selectAccount] = + const [searchAccountByCriteria, accountList, toggleAccountStatus, saveOrUpdateAccount, resetAccountPassword, newEmptyAccount] = useAccountStore((state) => - [state.searchAccountByCriteria, state.accountList, state.disableAccount, state.selectedAccount, state.saveOrUpdateAccount, state.selectAccount]) + [state.searchAccountByCriteria, state.accountList, state.toggleAccountStatus, state.saveOrUpdateAccount, state.resetAccountPassword, state.newEmptyAccount]) + const formValues = useFormStore(state => state.formValues) const { notification, modal } = App.useApp() + useEffect(() => { + fetchRoleList() + .then((roleList) => { + const roleListMap = roleList.map(r => { + return { + value: r.role_id, + label: r.role_name, + disabled: r.role_id === 1 + } + }) + roleListMap.unshift({ value: 0, label: '未设置', disabled: true }); + setRoleAllList(roleListMap) + }) + }, []) + + const handelAccountSearch = () => { + setDataLoading(true) + searchAccountByCriteria(formValues) + .catch(ex => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }) + }) + .finally(() => { + setDataLoading(false) + }) + } + const onAccountSeleted = async (account) => { + setTravelAgencyList([{ + label: account.travelAgencyName, + value: account.travelAgencyId + }]) accountForm.setFieldsValue(account) - selectAccount(account) - console.info(account) - const roleList = await fetchRoleList() - setRoleAllList(roleList.map(r => { - return { - value: r.role_id, - label: r.role_name, - disabled: r.role_id === 1 - } - })) + setCurrentTravelAgency(account.travelAgencyId) + setAccountModalOpen(true) + } + + const onNewAccount = () => { + const emptyAccount = newEmptyAccount() + accountForm.setFieldsValue(emptyAccount) setAccountModalOpen(true) } const onAccountFinish = (values) => { - console.log(values) saveOrUpdateAccount(values) + .then(() => { + handelAccountSearch() + }) .catch(ex => { - console.info(ex.message) notification.error({ message: 'Notification', description: ex.message, @@ -208,13 +145,47 @@ function Management() { // form.resetFields() } - const showDisableConfirm = (account) => { + const handleTravelAgencySearch = (newValue) => { + setDataLoading(true) + fetchTravelAgencyByName(newValue) + .then(result => { + setTravelAgencyList(result.map(r => { + return { + label: r.travel_agency_name, + value: r.travel_agency_id + } + })) + }) + .finally(() => { + setDataLoading(false) + }) + } + + const handleTravelAgencyChange = (newValue) => { + setCurrentTravelAgency(newValue) + } + + const showDisableConfirm = (account, status) => { + + const confirmTitle = status ? t('account:action.enable.title') : t('account:action.disable.title') + modal.confirm({ - title: 'Do you want to disable this account?', + title: confirmTitle, icon: , - content: `Username: ${account.username}, Realname: ${account.realname}`, + content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname, onOk() { - disableAccount(account.userId) + toggleAccountStatus(account.userId, status) + .then(() => { + handelAccountSearch() + }) + .catch(ex => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }) + }) }, onCancel() { }, @@ -222,12 +193,22 @@ function Management() { } const showResetPasswordConfirm = (account) => { + const confirmTitle = t('account:action.resetPassword.tile') + const randomPassword = account.username + '@' + (Math.floor(Math.random() * 900) + 100) modal.confirm({ - title: 'Do you want to reset password?', + title: confirmTitle, icon: , - content: `Username: ${account.username}, Realname: ${account.realname}`, + content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname, onOk() { - console.log('ResetPassword') + resetAccountPassword(account.userId, randomPassword) + .then(() => { + notification.info({ + message: `请复制新密码给 [${account.realname}]`, + description: '新密码:' + randomPassword, + placement: 'top', + duration: 60, + }) + }) }, onCancel() { }, @@ -242,29 +223,30 @@ function Management() { autoFocus: true, htmlType: 'submit', }} - title={t('account:management.newAccount')} + title={t('account:detail')} open={isAccountModalOpen} onOk={() => setAccountModalOpen(false)} onCancel={() => setAccountModalOpen(false)} - destroyOnClose={true} - clearOnDestroy={true} + destroyOnClose + forceRender modalRender={(dom) => ( + name='AccountForm' + form={accountForm} + layout='vertical' + size='large' + className='max-w-2xl' + onFinish={onAccountFinish} + onFinishFailed={onAccountFailed} + autoComplete='off' + > {dom} )} > + + + - + - - - {t('account:management.tile')} + + {t('account:accountList')} { - console.info(formValues) - setDataLoading(true) - searchAccountByCriteria(formValues) - .catch(ex => { - notification.error({ - message: 'Notification', - description: ex.message, - placement: 'top', - duration: 4, - }) - }) - .finally(() => { - setDataLoading(false) - }) + onSubmit={() => { + handelAccountSearch() }} /> - + diff --git a/src/views/account/Profile.jsx b/src/views/account/Profile.jsx index 1691081..d40ca83 100644 --- a/src/views/account/Profile.jsx +++ b/src/views/account/Profile.jsx @@ -1,36 +1,21 @@ import { useEffect, useState } from 'react' import { Descriptions, Col, Row } from 'antd'; import { useTranslation } from 'react-i18next' -import { fetchUserDetail } from '@/stores/Auth' -import { usingStorage } from '@/hooks/usingStorage' +import useAuthStore from '@/stores/Auth' function Profile() { const { t } = useTranslation() - const [userDetail, setUserDetail] = useState({}) - const { loginToken } = usingStorage() - - useEffect (() => { - fetchUserDetail(loginToken) - .then(json => { - setUserDetail({ - username: json.LoginName, - telephone: json.LkPhone, - emailAddress: json.LMI_listmail, - travelAgencyName: json.VName, - }) - }) - }, []) - + const currentUser = useAuthStore(state => state.currentUser) return ( - {userDetail?.username} - {userDetail?.telephone} - {userDetail?.emailAddress} - {userDetail?.travelAgencyName} + {currentUser?.username} + {currentUser?.realname}({currentUser?.rolesName}) + {currentUser?.emailAddress} + {currentUser?.travelAgencyName} diff --git a/src/views/account/RoleList.jsx b/src/views/account/RoleList.jsx index 923d43e..796aafe 100644 --- a/src/views/account/RoleList.jsx +++ b/src/views/account/RoleList.jsx @@ -1,115 +1,29 @@ -import { useState, useEffect } from 'react' -import { Row, Col, Space, Button, Table, Select, TreeSelect, Typography, Modal, App, Form, Input } from 'antd' -import { ExclamationCircleFilled } from '@ant-design/icons' -import { useTranslation } from 'react-i18next' -import useFormStore from '@/stores/Form' -import useAuthStore from '@/stores/Auth' -import useAccountStore from '@/stores/Account' -import { fetchRoleList } from '@/stores/Account' -import SearchForm from '@/components/SearchForm' import RequireAuth from '@/components/RequireAuth' import { PERM_ROLE_NEW } from '@/config' +import useAccountStore, { fetchPermissionList, fetchPermissionListByRoleId, fetchRoleList } from '@/stores/Account' +import { isEmpty } from '@/utils/commons' +import { + SyncOutlined, +} from '@ant-design/icons' +import { App, Button, Col, Form, Input, Modal, Row, Space, Table, Tag, TreeSelect, Typography } from 'antd' +import dayjs from 'dayjs' +import { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' const { Title } = Typography -const permissionData = [ - { - title: '海外供应商', - value: 'oversea-0', - key: 'oversea-0', - children: [ - { - title: '所有海外功能', - value: 'oversea-0-0', - key: 'oversea-0-0', - }, - ], - }, - { - title: '机票管理', - value: '0-0', - key: '0-0', - children: [ - { - title: '录入机票价格', - value: '0-0-0', - key: '0-0-0', - }, - ], - }, - { - title: '产品管理', - value: '0-1', - key: '0-1', - children: [ - { - title: '搜索供应商产品', - value: 'B-1-0', - key: 'B-1-0', - }, - { - title: '录入产品价格', - value: '0-1-0', - key: '0-1-0', - }, - { - title: '新增产品描述', - value: '0-1-1', - key: '0-1-1', - }, - { - title: '复制供应商产品信息', - value: '0-1-2', - key: '0-1-2', - }, - ], - }, - { - title: '账号管理', - value: '2-1', - key: '2-1', - children: [ - { - title: '搜索账号', - value: '2-1-01', - key: '2-1-01', - }, - { - title: '新增账号', - value: '2-1-11', - key: '2-1-11', - }, - { - title: '禁用账号', - value: '2-1-21', - key: '2-1-21', - }, - { - title: '重置账号密码', - value: '2-1-31', - key: '2-1-31', - }, - { - title: '新增角色', - value: '2-1-41', - key: '2-1-41', - }, - ], - }, -] - function RoleList() { const { t } = useTranslation() const roleListColumns = [ { - title: t('account:rolename'), + title: t('account:roleName'), dataIndex: 'role_name', - render: roleRender }, { title: t('account:createdOn'), dataIndex: 'created_on', + render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss')) }, { title: t('account:action'), @@ -118,57 +32,112 @@ function RoleList() { }, ] - function roleRender(text, role) { - return ( - - ) - } - - function actionRender(text, account) { - return ( - - - - - ) + function actionRender(_, role) { + if (role.role_id == 1) { + return (} color='warning'>不能修改) + } else { + return ( + + ) + } } const onPermissionChange = (newValue) => { - console.log('onChange ', newValue) setPermissionValue(newValue) } - useEffect (() => { + function groupByParam(array, param) { + return array.reduce((result, item) => { + (result[item[param]] = result[item[param]] || []).push(item) + return result + }, {}) + } + + useEffect(() => { + setDataLoading(true) fetchRoleList() .then(r => { setRoleAllList(r) }) + .finally(() => { + setDataLoading(false) + }) + + const categoryMap = new Map([ + ['system', '系统管理'], + ['oversea', '海外供应商'], + ['domestic', '国内供应商'], + ['air-ticket', '机票供应商'], + ['products', '产品价格'], + ['page', '默认页面'], + ]); + + const permissionTree = [] + fetchPermissionList() + .then(r => { + + const groupPermissionData = groupByParam(r, 'res_category') + const categoryKeys = Object.keys(groupPermissionData) + + categoryKeys.forEach((categoryName) => { + const permissisonList = groupPermissionData[categoryName] + const categoryGroup = { + title: categoryMap.get(categoryName), + value: categoryName, + key: categoryName, + children: permissisonList.map(p => { + return { + disableCheckbox: p.res_id == 1, + title: p.res_name, + value: p.res_id, + key: p.res_id, + } + }) + } + permissionTree.push(categoryGroup) + }) + + setPermissionTreeData(permissionTree) + }) }, []) - const [permissionValue, setPermissionValue] = useState(['0-0-0']) + const [permissionValue, setPermissionValue] = useState([]) + const [permissionTreeData, setPermissionTreeData] = useState([]) const [isRoleModalOpen, setRoleModalOpen] = useState(false) const [dataLoading, setDataLoading] = useState(false) const [roleAllList, setRoleAllList] = useState([]) const [roleForm] = Form.useForm() - const [saveOrUpdateRole] = + const [saveOrUpdateRole, newEmptyRole] = useAccountStore((state) => - [state.saveOrUpdateRole]) + [state.saveOrUpdateRole, state.newEmptyRole]) - const { notification, modal } = App.useApp() + const { notification } = App.useApp() - const onRoleSeleted = async (role) => { + const onRoleSeleted = (role) => { + fetchPermissionListByRoleId({ role_id: role.role_id }) + .then(result => { + role.res_array = result.map(r => r.res_id) + roleForm.setFieldsValue(role) + }) + setRoleModalOpen(true) + } + + const onNewRole = () => { + const role = newEmptyRole() roleForm.setFieldsValue(role) - // selectAccount(account) - // console.info(account) setRoleModalOpen(true) } const onRoleFinish = (values) => { - console.log(values) saveOrUpdateRole(values) + .then(() => { + fetchRoleList() + .then(r => { + setRoleAllList(r) + }) + }) .catch(ex => { - console.info(ex.message) notification.error({ message: 'Notification', description: ex.message, @@ -179,7 +148,6 @@ function RoleList() { } const onRoleFailed = (error) => { - console.log('Failed:', error) // form.resetFields() } @@ -191,72 +159,64 @@ function RoleList() { autoFocus: true, htmlType: 'submit', }} - title={t('account:management.newRole')} + title={t('account:detail')} open={isRoleModalOpen} onOk={() => setRoleModalOpen(false)} onCancel={() => setRoleModalOpen(false)} - destroyOnClose={true} - clearOnDestroy={true} + destroyOnClose + forceRender modalRender={(dom) => (
+ name='RoleForm' + form={roleForm} + layout='vertical' + size='large' + className='max-w-xl' + onFinish={onRoleFinish} + onFinishFailed={onRoleFailed} + autoComplete='off' + > {dom}
)} > - - - - - - - - - + + + + + + + - - {t('account:management.roleList')} + + {t('account:roleList')} - + @@ -268,6 +228,7 @@ function RoleList() { loading={dataLoading} rowKey='role_id' pagination={{ + pageSize: 20, showQuickJumper: true, showLessItems: true, showSizeChanger: true, diff --git a/src/views/invoice/Index.jsx b/src/views/invoice/Index.jsx index 02033ed..559a718 100644 --- a/src/views/invoice/Index.jsx +++ b/src/views/invoice/Index.jsx @@ -1,6 +1,4 @@ import { NavLink, useNavigate } from "react-router-dom"; -import { useState } from "react"; -import { toJS } from "mobx"; import { Row, Col, Space, Button, Table, App, Steps } from "antd"; import { formatDate, isNotEmpty } from "@/utils/commons"; import { AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons"; @@ -82,7 +80,7 @@ function Index() { fieldsConfig={{ shows: ['referenceNo', 'invoiceStatus', 'dates'], fieldProps: { - referenceNo: { col: 5 }, + referenceNo: { col: 7 }, invoiceStatus: { col: 4}, dates: { col: 10 }, }, @@ -103,7 +101,7 @@ function Index() { - +
diff --git a/src/views/products/Audit.jsx b/src/views/products/Audit.jsx new file mode 100644 index 0000000..2228b64 --- /dev/null +++ b/src/views/products/Audit.jsx @@ -0,0 +1,247 @@ +import { useEffect, useState } from 'react'; +import { useParams, Link } from 'react-router-dom'; +import { App, Button, Collapse, Table, Space, Divider } from 'antd'; +import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets'; +import SecondHeaderWrapper from '@/components/SecondHeaderWrapper'; +import { useTranslation } from 'react-i18next'; +import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index'; +import { cloneDeep, isEmpty } from '@/utils/commons'; +import useAuthStore from '@/stores/Auth'; +import RequireAuth from '@/components/RequireAuth'; +// import PrintContractPDF from './PrintContractPDF'; +import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config'; + +const Header = ({ title, agency, refresh, ...props }) => { + const { travel_agency_id, use_year, audit_state } = useParams(); + const { t } = useTranslation(); + const [activeAgency] = useProductsStore((state) => [state.activeAgency]); + const { message, notification } = App.useApp(); + const handleAuditItem = (state, row) => { + postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id }) + .then((json) => { + if (json.errcode === 0) { + message.success(json.errmsg); + if (typeof refresh === 'function') { + refresh(); + } + } + }) + .catch((ex) => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }); + }); + }; + return ( +
+
+

+ {title} + + {(use_year || '').replace('all', '')} +

+
+ {/* */} + {/* */} + + + {t('Edit')} + + + + + + {/* */} + + + + {/* todo: export, 审核完成之后才能导出 */} + + {/* */} +
+ ); +}; + +const PriceTable = ({ productType, dataSource, refresh }) => { + const { t } = useTranslation('products'); + const { travel_agency_id, use_year, audit_state } = useParams(); + const isPermitted = useAuthStore(state => state.isPermitted); + const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]); + const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]); + const { message, notification } = App.useApp(); + const stateMapVal = useProductsAuditStatesMapVal(); + + const [renderData, setRenderData] = useState(dataSource); + + // console.log(dataSource); + + const handleAuditPriceItem = (state, row, rowIndex) => { + postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id }) + .then((json) => { + if (json.errcode === 0) { + message.success(json.errmsg); + + if (typeof refresh === 'function') { + // refresh(); // debug: 不要刷新, 等太久 + // const newData = structuredClone(renderData); + const newData = cloneDeep(renderData); + newData.splice(rowIndex, 1, {...row, audit_state_id: state, }); + setRenderData(newData); + } + } + }) + .catch((ex) => { + notification.error({ + message: 'Notification', + description: ex.message, + placement: 'top', + duration: 4, + }); + }); + }; + + const rowStyle = (r, tri) => { + const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; + const [infoI, quoteI] = r.rowSpanI; + const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : ''; + return [trCls, bigTrCls].join(' '); + }; + + const columns = [ + { key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan, }), className: 'bg-white', render: (text, r) => { + const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || ''; + return isPermitted(PERM_PRODUCTS_OFFER_PUT) ? setEditingProduct(r.info)}>{title} : title; + } }, + // ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []), + { key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` }, + { key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` }, + // {key: 'unit', title: t('Unit'), }, + { + key: 'groupSize', + dataIndex: ['group_size_min'], + title: t('GroupSize'), + render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`, + }, + { + key: 'useDates', + dataIndex: ['use_dates_start'], + title: t('UseDates'), + render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''), + }, + { key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') }, + { + key: 'state', + title: t('State'), + render: (_, r) => { + const stateCls = ` text-${stateMapVal[`${r.audit_state_id}`]?.color} `; + return {stateMapVal[`${r.audit_state_id}`]?.label}; + }, + }, + { + title: '', + key: 'action', + render: (_, r, ri) => + (Number(r.audit_state_id)) === 0 ? ( + + + + + + + ) : null, + }, + ]; + return
r.id} />; +}; + +/** + * + */ +const TypesPanels = (props) => { + const { t } = useTranslation(); + const [loading, agencyProducts] = useProductsStore((state) => [state.loading, state.agencyProducts]); + // console.log(agencyProducts); + const productsTypes = useProductsTypes(); + const [activeKey, setActiveKey] = useState([]); + const [showTypes, setShowTypes] = useState([]); + useEffect(() => { + // 只显示有产品的类型; 展开产品的价格表, 合并名称列; 转化为价格主表, 携带产品属性信息 + const hasDataTypes = Object.keys(agencyProducts); + const _show = productsTypes + .filter((kk) => hasDataTypes.includes(kk.value)) + .map((ele) => ({ + ...ele, + extra: t('Table.Total', { total: agencyProducts[ele.value].length }), + children: ( + + r.concat( + c.quotation.map((q, i) => ({ + ...q, + weekdays: q.weekdays + .split(',') + .filter(Boolean) + .map((w) => t(`weekdaysShort.${w}`)) + .join(', '), + info: c.info, + lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...r, [clgc.lgc]: clgc}), {}), + rowSpan: i === 0 ? c.quotation.length : 0, + rowSpanI: [ri, i], + })) + ), + [] + )} + refresh={props.refresh} + /> + ), + })); + setShowTypes(_show); + + setActiveKey(isEmpty(_show) ? [] : [_show[0].key]); + return () => {}; + }, [productsTypes, agencyProducts]); + + const onCollapseChange = (_activeKey) => { + setActiveKey(_activeKey); + }; + return ; +}; + +const Audit = ({ ...props }) => { + const { travel_agency_id, use_year, audit_state } = useParams(); + const [loading, activeAgency, getAgencyProducts] = useProductsStore((state) => [state.loading, state.activeAgency, state.getAgencyProducts]); + + const handleGetAgencyProducts = () => { + getAgencyProducts({ travel_agency_id, use_year, audit_state }); + }; + + useEffect(() => { + handleGetAgencyProducts(); + + return () => {}; + }, [travel_agency_id]); + + return ( + <> + } loading={loading} > + {/* debug: 0 */} + {/* */} + + + + + ); +}; +export default Audit; diff --git a/src/views/products/Detail.jsx b/src/views/products/Detail.jsx new file mode 100644 index 0000000..f624d56 --- /dev/null +++ b/src/views/products/Detail.jsx @@ -0,0 +1,1027 @@ +import React, { useState, useEffect, useRef, useMemo } from 'react'; +import { Button, Card, Col, Row, Breadcrumb, Table, Popconfirm, Form, Input, InputNumber, Tag, Modal, Select, Tree, FloatButton, DatePicker } from 'antd'; +import { Link } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { useProductsTypes } from '@/hooks/useProductsSets'; +import Extras from './Detail/Extras'; +import { useParams } from 'react-router-dom'; +import useProductsStore from '@/stores/Products/Index'; +import { useHTLanguageSets } from '@/hooks/useHTLanguageSets'; +import { useDefaultLgc } from '@/i18n/LanguageSwitcher'; +import BatchImportPrice from './Detail/BatchImportPrice'; +import dayjs from 'dayjs'; +import { PlusCircleFilled } from '@ant-design/icons'; +import { DeptSelector } from '@/components/DeptSelector'; +import { useDatePresets } from '@/hooks/useDatePresets'; + +function Detail() { + const { t } = useTranslation(); + const [form] = Form.useForm(); + const { RangePicker } = DatePicker; + const [editingid, setEditingid] = useState(''); + const [tags, setTags] = useState([]); + const [isModalVisible, setIsModalVisible] = useState(false); + const [selectedTag, setSelectedTag] = useState(null); + const [saveData, setSaveData] = useState(null); + const [batchImportPriceVisible, setBatchImportPriceVisible] = useState(false); + const [quotationTableVisible, setQuotationTableVisible] = useState(false) + const [currentid, setCurrentid] = useState(null); + const [languageStatus, setLanguageStatus] = useState(null); + const [selectedNodeid, setSelectedNodeid] = useState(null); + const [remainderLanguage, setRemainderLanguage] = useState([]) + const [selectedDateData, setSelectedDateData] = useState({ dateRange: null, selectedDays: [] }); + const [treeData, setTreeData] = useState([]); + const productsTypes = useProductsTypes(); + const [productsData, setProductsData] = useState(null); + const [quotation, setQuotation] = useState(null); + const [lgc_details, setLgc_details] = useState(null); + const [languageLabel, setLanguageLabel] = useState(null); + const [infoDataForId, setInfoDataForId] = useState(null); + const { travel_agency_id } = useParams(); + const { language } = useDefaultLgc(); + const HTLanguageSets = useHTLanguageSets(); + const { Search } = Input; + const [addProductVisible, setAddProductVisible] = useState(false); + const [editingProduct, setEditingProduct] = useProductsStore((state) => [state.editingProduct, state.setEditingProduct]); + const [agencyProducts, setAgencyProducts] = useProductsStore((state) => [state.agencyProducts, state.setAgencyProducts]); + const { getAgencyProducts } = useProductsStore(); + const [expandedKeys, setExpandedKeys] = useState([]); + const [searchValue, setSearchValue] = useState(''); + const [autoExpandParent, setAutoExpandParent] = useState(true); + const [dataList, setDataList] = useState([]); + const [defaultData, setDefaultData] = useState([]); + const [batchImportData, setBatchImportData] = useState([]); + const [addProductType, setAddProductType] = useState(''); + const [addproductName, setAddProductName] = useState(''); + const [dataFetched, setDataFetched] = useState(false); // 添加一个标志位 + const [selectedNodeKey, setSelectedNodeKey] = useState(null); + const [selectedDays, setSelectedDays] = useState([]); + const [weekdays,setWeekdays] = useState([]); + const [currentQuotationRecord, setCurrentQuotationRecord] = useState({ + use_dates_start: null, + use_dates_end: null + }); + const formatDate = (date) => (date ? dayjs(date) : null); + + + const startDate = currentQuotationRecord.use_dates_start && dayjs(currentQuotationRecord.use_dates_start).isValid() + ? formatDate(currentQuotationRecord.use_dates_start) + : null; + const endDate = currentQuotationRecord.use_dates_end && dayjs(currentQuotationRecord.use_dates_end).isValid() + ? formatDate(currentQuotationRecord.use_dates_end) + : null; + + const [editIndex, setEditIndex] = useState(null); + const presets = useDatePresets(); + const handleBatchImportData = (data) => { + setBatchImportData(data); + }; + const days = [ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' + ]; + + const productProject = { + "6": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + ], + "B": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + { code: "km", name: t('products:KM'), nameKey: 'products:KM' }, + { code: "remarks", name: t('products:Remarks'), nameKey: 'products:Remarks' } + ], + "J": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + { code: "recommends_rate", name: t('products:recommendationRate'), nameKey: 'products:recommendationRate' }, + { code: "duration", name: t('products:Duration'), nameKey: 'products:Duration' }, + { code: "dept_name", name: t('products:Dept'), nameKey: 'products:Dept' }, + { code: "display_to_c", name: t('products:DisplayToC'), nameKey: 'products:DisplayToC' }, + { code: "remarks", name: t('products:Remarks'), nameKey: 'products:Remarks' }, + ], + "Q": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + { code: "recommends_rate", name: t('products:recommendationRate'), nameKey: 'products:recommendationRate' }, + { code: "duration", name: t('products:Duration'), nameKey: 'products:Duration' }, + { code: "dept_name", name: t('products:Dept'), nameKey: 'products:Dept' }, + { code: "display_to_c", name: t('products:DisplayToC'), nameKey: 'products:DisplayToC' }, + { code: "remarks", name: t('products:Remarks'), nameKey: 'products:Remarks' }, + ], + "D": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + { code: "recommends_rate", name: t('products:recommendationRate'), nameKey: 'products:recommendationRate' }, + { code: "duration", name: t('products:Duration'), nameKey: 'products:Duration' }, + { code: "dept_name", name: t('products:Dept'), nameKey: 'products:Dept' }, + { code: "display_to_c", name: t('products:DisplayToC'), nameKey: 'products:DisplayToC' }, + { code: "remarks", name: t('products:Remarks'), nameKey: 'products:Remarks' }, + ], + "7": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + { code: "recommends_rate", name: t('products:recommendationRate'), nameKey: 'products:recommendationRate' }, + { code: "duration", name: t('products:Duration'), nameKey: 'products:Duration' }, + { code: "open_weekdays", name: t('products:OpenWeekdays'), nameKey: 'products:OpenWeekdays' }, + { code: "remarks", name: t('products:Remarks'), nameKey: 'products:Remarks' }, + ], + "8": [ + { code: "code", name: t('products:Code') }, + { code: "city_name", name: t('products:City') }, + ], + "R": [ + { code: "code", name: t('products:Code'), nameKey: 'products:Code' }, + { code: "city_name", name: t('products:City'), nameKey: 'products:City' }, + ] + } + const [selectedCategory, setSelectedCategory] = useState(productProject.B); + + useEffect(() => { + setLanguageStatus(language); + const matchedLanguage = HTLanguageSets.find(HTLanguage => HTLanguage.key === language.toString()); + const languageLabel = matchedLanguage.label + setLanguageLabel(languageLabel) + setSelectedTag(languageLabel) + // setRemainderLanguage(HTLanguageSets.filter(item => item.key !== language.toString())) + + }, []); + + + + useEffect(() => { + const fetchData = async () => { + if (productsTypes && !dataFetched) { + const agency_id = { travel_agency_id }; + await getAgencyProducts(agency_id); + console.log("agencyProducts", agencyProducts) + console.log("productsTypes", productsTypes) + const generateTreeData = (productsTypes, productsData) => { + return productsTypes.map(type => ({ + title: type.label, + key: type.value, + selectable: false, + children: (productsData[type.value] || []).map(product => ({ + title: product.info.title, + key: `${type.value}-${product.info.id}`, + _raw: product, + })) + })); + }; + const tempExpandedKeys = productsTypes.map(item => item.key) + console.log("tempExpandedKeys", tempExpandedKeys) + const treeData = generateTreeData(productsTypes, agencyProducts); + console.log("treeData", treeData) + setDataFetched(true); // 设置标志位为 true,表示数据已获取 + setTreeData(treeData); + setExpandedKeys(tempExpandedKeys) + setProductsData(agencyProducts); + setDefaultData(treeData); + setDataList(flattenTreeData(treeData)); + } + + }; + + fetchData(); + }, [agencyProducts, dataFetched]); + + + const flattenTreeData = (tree) => { + let flatList = []; + const flatten = (nodes) => { + nodes.forEach((node) => { + flatList.push({ title: node.title, key: node.key }); + if (node.children) { + flatten(node.children); + } + }); + }; + flatten(tree); + return flatList; + }; + + const getParentKey = (key, tree) => { + let parentKey; + for (let i = 0; i < tree.length; i++) { + const node = tree[i]; + if (node.children) { + if (node.children.some((item) => item.key === key)) { + parentKey = node.key; + } else { + const pKey = getParentKey(key, node.children); + if (pKey) { + parentKey = pKey; + } + } + } + } + return parentKey; + }; + + const titleRender = (node) => { + const index = node.title.indexOf(searchValue); + const beforeStr = node.title.substr(0, index); + const afterStr = node.title.substr(index + searchValue.length); + const highlighted = ( + {searchValue} + ); + + return index > -1 ? ( + + {beforeStr} + {highlighted} + {afterStr} + + ) : ( + {node.title} + ); + }; + + + const onChange = (e) => { + const { value } = e.target; + const newExpandedKeys = dataList + .filter(item => item.title.includes(value)) + .map(item => getParentKey(item.key, defaultData)) + .filter((item, i, self) => item && self.indexOf(item) === i); + console.log("newExpandedKeys", newExpandedKeys) + setExpandedKeys(newExpandedKeys); + setSearchValue(value); + setAutoExpandParent(true); + }; + + const onExpand = (keys) => { + setExpandedKeys(keys); + setAutoExpandParent(false); + }; + + const isEditing = (record) => record.id === editingid; + + const edit = (record, index) => { + setQuotationTableVisible(true); + setEditIndex(index); + // record.use_dates_start = dayjs(record.use_dates_start); + // record.use_dates_end = dayjs(record.use_dates_end); + setCurrentQuotationRecord(record); + }; + + const cancel = () => { + setEditingid(''); + }; + + const handleDelete = (id) => { + const newData = [...quotation]; + const index = newData.findIndex((item) => id === item.id); + newData.splice(index, 1); + + const sortedData = [...newData].sort((a, b) => { + const aValidPeriod = dayjs(a.use_dates_end).diff(dayjs(a.use_dates_start)); + const bValidPeriod = dayjs(b.use_dates_end).diff(dayjs(b.use_dates_start)); + if (aValidPeriod !== bValidPeriod) { + return aValidPeriod - bValidPeriod; + } + const aGroupSize = a.group_size_max - a.group_size_min; + const bGroupSize = b.group_size_max - b.group_size_min; + + return aGroupSize - bGroupSize; + }); + + setQuotation(sortedData); + }; + + const handleAdd = () => { + const newData = { + value: '', + currency: '', + unit_name: '', + weekdays: '', + use_dates_start: '', + use_dates_end: '', + group_size_min: '', + group_size_max: '' + }; + setQuotation([...quotation, newData]); + }; + + const handleBatchImport = () => { + setBatchImportPriceVisible(true); + } + + const handleDateSelect = (id) => { + setCurrentid(id); + setDatePickerVisible(true); + }; + + const quotationTableVisibleOK = () => { + currentQuotationRecord.use_dates_start = dayjs(currentQuotationRecord.use_dates_start).format('YYYY-MM-DD') + currentQuotationRecord.use_dates_end = dayjs(currentQuotationRecord.use_dates_end).format('YYYY-MM-DD') + console.log("currentQuotationRecord", currentQuotationRecord); + console.log("qqqqq", quotation) + const tempQuotation = [...quotation]; + tempQuotation[editIndex] = { ...currentQuotationRecord,weekdays:weekdays }; + console.log("tempQuotation", tempQuotation) + const sortedData = [...tempQuotation].sort((a, b) => { + const aValidPeriod = dayjs(a.use_dates_end).diff(dayjs(a.use_dates_start)); + const bValidPeriod = dayjs(b.use_dates_end).diff(dayjs(b.use_dates_start)); + + if (aValidPeriod !== bValidPeriod) { + return aValidPeriod - bValidPeriod; + } + const aGroupSize = a.group_size_max - a.group_size_min; + const bGroupSize = b.group_size_max - b.group_size_min; + + return aGroupSize - bGroupSize; + }); + console.log("sortedData",sortedData) + + setQuotation(sortedData); + setQuotationTableVisible(false); + } + const quotationTableVisibleCancel = () => { + setQuotationTableVisible(false); + } + + + const handleBatchImportOK = () => { + console.log("quotation", quotation) + console.log('Batch Import Data:', batchImportData); + + + + const tempBatchImportData = batchImportData.map(item => { + const { tag, validPeriod, ...rest } = item; + return rest; + }); + const newData = [...quotation, ...tempBatchImportData]; + const sortedData = [...newData].sort((a, b) => { + if (a.group_size_min !== b.group_size_min) { + return a.group_size_min - b.group_size_min; + } + + return a.group_size_max - b.group_size_max; + }); + + + setQuotation(sortedData); + setBatchImportPriceVisible(false); + } + + + + const columns = [ + { title: t('products:adultPrice'), dataIndex: 'adult_cost', width: '10%', editable: true }, + { title: t('products:childrenPrice'), dataIndex: 'child_cost', width: '10%', editable: true }, + { title: t('products:currency'), dataIndex: 'currency', width: '10%', editable: true }, + { title: t('products:Types'), dataIndex: 'unit_name', width: '10%', editable: true }, + { + title: t('products:number'), + dataIndex: 'group_size', + width: '20%', + editable: true, + render: (_, record) => `${record.group_size_min}-${record.group_size_max}` + }, + + { + title: t('products:validityPeriod'), + dataIndex: 'validityPeriod', + width: '20%', + editable: true, + render: (_, record) => `${record.use_dates_start}-${record.use_dates_end}` + }, + + { title: t('products:Weekdays'), dataIndex: 'weekdays', width: '10%' }, + + { + title: t('products:operation'), + dataIndex: 'operation', + render: (_, record, index) => { + return ( + + edit(record, index)} style={{ marginRight: 8 }}>{t('Edit')} + handleDelete(record.id)}> + {t('Delete')} + + + ) + }, + }, + + ]; + + + // const mergedColumns = columns.map((col) => { + // if (!col.editable) { + // return col; + // } + // return { + // ...col, + // onCell: (record) => ({ + // record, + // inputType: col.dataIndex === 'age' ? 'number' : 'text', + // dataIndex: col.dataIndex, + // title: col.title, + // editing: isEditing(record), + // handleDateSelect: handleDateSelect, + // }), + // }; + // }); + + const handleTagClick = (tag) => { + setSelectedTag(tag); + const matchedLanguage = HTLanguageSets.find(language => language.label === tag); + const key = matchedLanguage ? matchedLanguage.key : null; + form.setFieldsValue({ + lgc_details: { + title: lgc_details[key] ? lgc_details[key].title : '', + descriptions: lgc_details[key] ? lgc_details[key].descriptions : '' + } + }); + setLanguageStatus(key) + + }; + + const showModal = () => setIsModalVisible(true); + + const handleOk = () => { + if (!selectedTag) return; + if (!remainderLanguage.some(item => item.label === selectedTag)) return; + if (remainderLanguage.includes(selectedTag)) return; + let tempRemainderLanguage = remainderLanguage.filter((item) => { + return item.label !== selectedTag; + }) + + + const matchedLanguage = HTLanguageSets.find(HTLanguage => HTLanguage.label === selectedTag); + const languageKey = parseInt(matchedLanguage.key) + if (!(languageKey in lgc_details)) { + const tempLgc_details = { + ...lgc_details, [languageKey]: { + title: "", + lgc: languageKey, + descriptions: "" + } + } + setLgc_details(tempLgc_details) + } + + setRemainderLanguage(tempRemainderLanguage) + setTags([...tags, selectedTag]) + + setSelectedTag(null); + setIsModalVisible(false); + } + + const handleCancel = () => setIsModalVisible(false); + + + const handleTagChange = (value) => { + console.log("handleTagChange", value) + setSelectedTag(value); + }; + + const handleChange = (field, value) => { + // 更新整个 lgc_details 对象 + const updatedLgcDetails = { + ...lgc_details, + [languageStatus]: { ...lgc_details[languageStatus], [field]: value, lgc: languageStatus } + }; + console.log("updatedLgcDetails", updatedLgcDetails) + setLgc_details(updatedLgcDetails) + + }; + + const handleDayClick = (dayIndex) => { + const dayOfWeek = (dayIndex % 7) + 1; + + setSelectedDays((prevSelectedDays) => { + const updatedDays = prevSelectedDays.includes(dayOfWeek) + ? prevSelectedDays.filter((d) => d !== dayOfWeek) + : [...prevSelectedDays, dayOfWeek]; + console.log("updatedDays",updatedDays); + const weekdaysString = updatedDays.sort().join(','); + console.log("weekdaysString",weekdaysString) + setWeekdays(weekdaysString) + return updatedDays; + }); + }; + + //树组件方法 + const handleNodeSelect = (_, { node }) => { + if (!node._raw.info.id) { + console.log("nodeNoID", node) + setQuotation([]) + const infoData = node._raw.info + const newLgcDetails = node._raw.lgc_details + const fatherKey = node.key.split('-')[0]; + setSelectedNodeid(node.key); + setSelectedNodeKey(fatherKey); + form.setFieldsValue({ + info: { + title: infoData.title, + code: infoData.code, + product_type_name: infoData.product_type_name, + city_name: infoData.city_name, + remarks: infoData.remarks, + open_weekdays: infoData.open_weekdays, + recommends_rate: infoData.recommends_rate, + duration: infoData.duration, + dept: infoData.dept, + km: infoData.km, + dept_name: infoData.dept_name, + }, + lgc_details: { + lgc: language, + title: newLgcDetails[language]?.title || '', + descriptions: newLgcDetails[language]?.descriptions || '' + } + }) + return + } + + + setTags([languageLabel]) + // 如果点击的是同一个节点,不做任何操作 + if (selectedNodeid === node.key) return; + + setSelectedNodeid(node.key); + const fatherKey = node.key.split('-')[0]; + setSelectedNodeKey(fatherKey); + console.log("node.key", node.key); + console.log("fatherKey", fatherKey) + setSelectedCategory(productProject[fatherKey]); + + setLanguageStatus(language); + const matchedLanguage = HTLanguageSets.find(HTLanguage => HTLanguage.key === language.toString()); + const languageLabelRefresh = matchedLanguage.label; + setLanguageLabel(languageLabelRefresh); + setSelectedTag(languageLabelRefresh); + setRemainderLanguage(HTLanguageSets.filter(item => item.key !== language.toString())); + + + setEditingProduct(node._raw); + + let initialQuotationData = null; + let infoData = null; + let lgcDetailsData = null; + console.log("productsData", productsData) + console.log("productsData[fatherKey]", productsData[fatherKey]) + console.log("node", node) + // console.log("node",node._raw) + productsData[fatherKey].forEach(element => { + if (element.info.id === node._raw.info.id) { + initialQuotationData = element.quotation; + infoData = element.info; + lgcDetailsData = element.lgc_details; + } + }); + + if (!node._raw.info.id) { + + } + + console.log("lgcDetailsData", lgcDetailsData) + // 累积 lgc_details 数据 + let newLgcDetails = {}; + if (lgcDetailsData) { + lgcDetailsData.forEach(element => { + newLgcDetails[element.lgc] = element; + }); + } + console.log("infoData", infoData) + console.log("laug", language) + + if (node._raw.info.id) { + setInfoDataForId(infoData.id) + } + setLgc_details(newLgcDetails); + + + const sortedData = [...initialQuotationData].sort((a, b) => { + // 计算有效期范围大小 + const aValidPeriod = dayjs(a.use_dates_end).diff(dayjs(a.use_dates_start)); + const bValidPeriod = dayjs(b.use_dates_end).diff(dayjs(b.use_dates_start)); + + // 按照有效期范围大小升序排序 + if (aValidPeriod !== bValidPeriod) { + return aValidPeriod - bValidPeriod; + } + + // 如果有效期范围相同,则按照人数范围大小升序排序 + const aGroupSize = a.group_size_max - a.group_size_min; + const bGroupSize = b.group_size_max - b.group_size_min; + + return aGroupSize - bGroupSize; + }); + setQuotation(sortedData); + + if (node._raw.info.id) { + form.setFieldsValue({ + info: { + title: infoData.title, + code: infoData.code, + product_type_name: infoData.product_type_name, + city_name: infoData.city_name, + remarks: infoData.remarks, + open_weekdays: infoData.open_weekdays, + recommends_rate: infoData.recommends_rate, + duration: infoData.duration, + dept: infoData.dept, + km: infoData.km, + dept_name: infoData.dept_name, + }, + lgc_details: { + lgc: language, + title: newLgcDetails[language]?.title || '', + descriptions: newLgcDetails[language]?.descriptions || '' + } + }); + } + + }; + + const handelAddProduct = () => { + // 找到对应的产品类型节点 + const productTypeNode = treeData.find(item => item.key === addProductType); + console.log("productTypeNode", productTypeNode); + + if (productTypeNode) { + // 在 children 数组中插入新的产品节点 + const newChildren = [ + ...productTypeNode.children, + { + title: addproductName, + key: `${addProductType}-${Date.now()}`, // 使用时间戳作为唯一的 key + _raw: { + info: { code: '' }, + lgc_details: [], + quotation: [] + } + } + ]; + // 创建新的 treeData 数组,确保 React 能够检测到更改 + const newTreeData = treeData.map(item => { + if (item.key === addProductType) { + return { + ...item, + children: newChildren, + }; + } + return item; + }); + // 更新 treeData + setEditingProduct(null) + setTreeData(newTreeData); + } + + console.log("productData", productsData) + console.log("addProductType", addProductType) + let tempProductDataList = productsData[addProductType]; + + //初始化产品数据 + const newProduct = { + info: { + code: 'addProduct' + }, + quotation: [], + lgc_details: [] + } + tempProductDataList.push(newProduct); + console.log("tempProductDataList", tempProductDataList) + const newProductsData = { + ...productsData, // 假设使用了展开运算符来复制现有数组 + [addProductType]: tempProductDataList + }; + setProductsData(newProductsData); + console.log("newProductsData", newProductsData) + setAddProductVisible(false); + }; + + const onSave = (values) => { + // 找到匹配的树节点 + let matchedTreeData = treeData.find(treeKey => treeKey.key === selectedNodeKey); + if (matchedTreeData) { + // 找到匹配的子节点 + const matchedTreeDataChildren = matchedTreeData.children; + // 检查是否已存在具有 selectedNodeid 的子节点 + console.log("matchedTreeDataChildren", matchedTreeDataChildren) + console.log("selectedNodeid", selectedNodeid) + // if (matchedTreeDataChildren.some(child => child.key === selectedNodeid)) { + // console.log("Child with this ID already exists."); + // return; + // } + let tempTreeDataChildrenData = matchedTreeDataChildren.find(element => element.key === selectedNodeid); + console.log("tempTreeDataChildrenData", tempTreeDataChildrenData); + // if (tempTreeDataChildrenData) { + tempTreeDataChildrenData._raw = values; + console.log("tempTreeDataChildrenData改", tempTreeDataChildrenData) + console.log("treeData111111", treeData) + console.log("lgc_details", lgc_details) + tempTreeDataChildrenData._raw.lgc_details = lgc_details + // console.log("matchedTreeData", matchedTreeData) + // if (!matchedTreeData.children.some(element => element.key === selectedNodeid)) { + // console.log("重复了"); + // matchedTreeData.children.push(tempTreeDataChildrenData) + // console.log("matchedTreeData改", matchedTreeData) + // return; + // } + + + // } else { + // console.log("No matching child node found."); + // } + } else { + console.log("No matching tree node found."); + } + + + + + // if (infoDataForId) { + // // 创建新的 tempData 对象 + // const tempData = { + // ...values, + // info: { ...values.info, id: infoDataForId }, + // quotation: quotation, + // lgc_details: Object.values(lgc_details) + // }; + + // setSaveData(tempData); + // console.log("保存的数据", tempData); + // } else { + // // 创建新的 tempData 对象 + // const tempData = { + // ...values, + // info: { ...values.info }, + // quotation: quotation, + // lgc_details: Object.values(lgc_details) + // }; + + // setSaveData(tempData); + // console.log("保存的数据", tempData); + // } + + }; + + return ( +
+ +
+ + + + + + + + + + + 供应商 }, + { title: 综费 }, + { title: editingProduct?.info?.title || t('New') } + ]} /> + } + > +

{t('products:EditComponents.info')}

+ + {selectedCategory.map((item, index) => { + // const key = `${item.code}-${index}`; + // console.log(key); + return ( +
+ + {item.code === "duration" ? ( + + ) : (item.code === "display_to_c") ? ( + + ) : (item.code === "dept_name") ? ( + + ) : ( + + )} + + + ); + })} + + + + {tags.map(tag => ( + handleTagClick(tag)} + color={tag === selectedTag ? 'blue' : undefined} + style={{ cursor: 'pointer' }} + > + {tag} + + ))} + + + + }> + + + + + + handleChange('title', e.target.value)} + /> + + + handleChange('descriptions', e.target.value)} + /> + + + {/* */} + + {/* */} +

{t('products:supplierQuotation')}

+ +
+ + + + + + + + + + + {/* */} + + {/* */} + } onClick={() => setAddProductVisible(true)} /> + + + + { + batchImportPriceVisible && ( + setBatchImportPriceVisible(false)} + width="80%" + > + + + ) + } + + { + addProductVisible && ( + setAddProductVisible(false)} + > +

选择产品类别

+ + + +

新增产品名称

+ setAddProductName(e.target.value)} + /> +
+ ) + } + + { + quotationTableVisible && ( + +

成人价

+ setCurrentQuotationRecord({ ...currentQuotationRecord, adult_cost: e })} /> +

儿童价

+ setCurrentQuotationRecord({ ...currentQuotationRecord, child_cost: e })} /> +

币种

+ +

类型

+ + +

人等

+
+

有效期

+ { + setCurrentQuotationRecord({ + ...currentQuotationRecord, + use_dates_start: dates[0], + use_dates_end: dates[1] + }); + }} + /> +

周末

+ {days.map((day, index) => ( + + ))} + + + + ) + } + + ); +} +export default Detail; + + diff --git a/src/views/products/Detail/BatchImportPrice.jsx b/src/views/products/Detail/BatchImportPrice.jsx new file mode 100644 index 0000000..0b35796 --- /dev/null +++ b/src/views/products/Detail/BatchImportPrice.jsx @@ -0,0 +1,371 @@ +import React, { useState } from 'react'; +import { Button, Card, Checkbox, Col, DatePicker, Form, Input, Row, Select, Space, Tag, Table, InputNumber } from 'antd'; +import { CloseOutlined } from '@ant-design/icons'; +import dayjs from 'dayjs'; +import { useDatePresets } from '@/hooks/useDatePresets'; +const { Option } = Select; +const { RangePicker } = DatePicker; + +const BatchImportPrice = ({ onBatchImportData }) => { + const [form] = Form.useForm(); + const [tags, setTags] = useState([]); + const [minPeople, setMinPeople] = useState(''); + const [maxPeople, setMaxPeople] = useState(''); + const [checkedDays, setCheckedDays] = useState([]); + const [tableData, setTableData] = useState([]); + const [sendData, setSendData] = useState(null); + const presets = useDatePresets(); + + const handleTagClose = (removedTag) => { + setTags(tags.filter(tag => tag !== removedTag)); + }; + + const handleInputConfirm = () => { + if (minPeople && maxPeople) { + const tag = `${minPeople}-${maxPeople}`; + if (tags.indexOf(tag) === -1) { + setTags([...tags, tag]); + } + } + setMinPeople(''); + setMaxPeople(''); + }; + + const handleCheckboxChange = (checkedValues) => { + setCheckedDays(checkedValues); + }; + + const generateTableData = () => { + const values = form.getFieldsValue(); + const weekdays = checkedDays.join(','); + let tempSendData = []; + console.log("values",values) + // 遍历 items + values.items.forEach((item, index) => { + // 遍历 validPeriods + let tempValidPeriods = [] + item.validPeriods?.forEach((period) => { + console.log("period",period) + const validPeriod = period.validPeriod.map(date => date.format('YYYY-MM-DD')).join('~'); + tempValidPeriods.push(validPeriod) + // 更新 tempSendData 中每一个 tag 的值 + }); + const priceType = `批量设置价格 ${index + 1} ${item.currency}/${item.type}` + let tempData = [] + const unit_name = item.type + const currency = item.currency + tags.forEach((tag) => { + tempValidPeriods.forEach(validPeriod => { + const group_size_min = tag.split('-')[0] + const group_size_max = tag.split('-')[1] + let unit_id = null + const use_dates_start = validPeriod.split('~')[0] + const use_dates_end = validPeriod.split('~')[1] + if (unit_name === "每人") { + unit_id = 0 + } else { + unit_id = 1 + } + tempData.push({ group_size_min, group_size_max, validPeriod, unit_id, unit_name, use_dates_start, use_dates_end, currency, weekdays, tag, priceType }) + }); + }) + console.log("tempData", tempData) + tempSendData.push(...tempData) + + }); + + // 设置最终的发送数据 + setSendData([...tempSendData]); // 使用展开操作符确保传递给 setSendData 的是一个新对象 + const data = []; + values.items.forEach((item, index) => { + item.validPeriods?.forEach((period, idx) => { + const row = { + key: `${index}-${idx}`, + priceType: `批量设置价格 ${index + 1} ${item.currency}/${item.type}`, + validPeriod: period.validPeriod.map(date => date.format('YYYY-MM-DD')).join('~'), + currency: item.currency, + type: item.type, + }; + tags.forEach((tag, tagIndex) => { + row[`adultPrice${tagIndex + 1}`] = 0; // Initialize with 0 + row[`childrenPrice${tagIndex + 1}`] = 0; // Initialize with 0 + }); + data.push(row); + }); + }); + // setSendData([...tempSendData,data]); + setTableData(data); + // onBatchImportData(data); // 将生成的初始表格数据传递回父组件 + }; + + + + const handleTableChange = (age_type, value, tag, priceType) => { + if (age_type === 'adult_cost') { + console.log("sendData", sendData) + const updatedSendData = sendData.map((item) => { + console.log("item.priceType === priceType", item.priceType === priceType) + console.log("item.priceType", item.priceType) + console.log("priceType", priceType) + if (item.priceType === priceType && item.tag === tag) { + return { + ...item, + adult_cost: value, // 更新对应的 adult_cost 属性 + }; + } + return item; // 对于不匹配的项,保持不变 + }); + // 更新 sendData + console.log("updatedSendData", updatedSendData) + onBatchImportData(updatedSendData); + setSendData(updatedSendData); + } else { + const updatedSendData = sendData.map((item) => { + if (item.priceType === priceType && item.tag === tag) { + return { + ...item, + child_cost: value, // 更新对应的 child_cost 属性 + }; + } + return item; // 对于不匹配的项,保持不变 + }); + // 更新 sendData + onBatchImportData(updatedSendData); + setSendData(updatedSendData); + } + }; + + + const generatePeopleColumns = () => { + const columns = []; + tags.forEach((tag, index) => { + columns.push({ + title: tag, + children: [ + { + title: '成人价', + dataIndex: `adultPrice${index + 1}`, + key: `adultPrice${index + 1}`, + render: (text, record, rowIndex) => { + const sameTagRecords = tableData.filter(item => item.priceType === record.priceType); + const firstTagIndex = tableData.findIndex(item => item.priceType === record.priceType && item.validPeriod === sameTagRecords[0].validPeriod); + + if (rowIndex === firstTagIndex) { + return { + children: ( + `${value}`} + parser={value => value.replace(/[^\d]/g, '')} + onChange={(value) => handleTableChange('adult_cost', value, tag, record.priceType)} + /> + ), + props: { + rowSpan: sameTagRecords.length, + }, + }; + } else { + return { + props: { + rowSpan: 0, + }, + }; + } + }, + }, + { + title: '儿童价', + dataIndex: `childrenPrice${index + 1}`, + key: `childrenPrice${index + 1}`, + render: (text, record, rowIndex) => { + const sameTagRecords = tableData.filter(item => item.priceType === record.priceType); + const firstTagIndex = tableData.findIndex(item => item.priceType === record.priceType && item.validPeriod === sameTagRecords[0].validPeriod); + + if (rowIndex === firstTagIndex) { + return { + children: ( + `${value}`} + parser={value => value.replace(/[^\d]/g, '')} + onChange={(value) => handleTableChange('child_cost', value, tag, record.priceType)} + /> + ), + props: { + rowSpan: sameTagRecords.length, + }, + }; + } else { + return { + props: { + rowSpan: 0, + }, + }; + } + }, + } + ] + }); + }); + return columns; + }; + + + const columns = [ + { + title: ' ', + dataIndex: 'priceType', + key: 'priceType', + width: "10%", + render: (text, record, index) => { + const obj = { + children: text, + props: {}, + }; + if (index > 0 && text === tableData[index - 1].priceType) { + obj.props.rowSpan = 0; + } else { + obj.props.rowSpan = tableData.filter(item => item.priceType === text).length; + } + return obj; + }, + }, + { + title: '有效期\\人等', + dataIndex: 'validPeriod', + key: 'validPeriod', + width: "15%" + }, + ...generatePeopleColumns(), + ]; + + return ( +
+ + +
+ + setMinPeople(e.target.value)} + /> + + setMaxPeople(e.target.value)} + /> + + + + + + + +
+ {tags.map((tag) => ( + handleTagClose(tag)}> + {tag} + + ))} +
+ + + + +
+ + {(fields, { add, remove }) => ( +
+ {fields.map((field, index) => ( + remove(field.name)} />} + > + + + + + + + + + + + {(periodFields, periodOpt) => ( +
+ {periodFields.map((periodField, idx) => ( + + + + + periodOpt.remove(periodField.name)} /> + + ))} + +
+ )} +
+
+
+ ))} + +
+ )} +
+ + + + + {tableData.length > 0 && ( +
+
+ setCurrentQuotationRecord({ ...currentQuotationRecord, group_size_min: e })} + style={{ width: '50%', marginRight: '10px' }} + /> + - + setCurrentQuotationRecord({ ...currentQuotationRecord, group_size_max: e })} + style={{ width: '50%', marginLeft: '10px' }} + /> +
+ + )} + + ); +}; + +export default BatchImportPrice; diff --git a/src/views/products/Detail/CopyProducts.jsx b/src/views/products/Detail/CopyProducts.jsx new file mode 100644 index 0000000..6b4d151 --- /dev/null +++ b/src/views/products/Detail/CopyProducts.jsx @@ -0,0 +1,160 @@ +import { useState, useEffect } from 'react'; +import { App, Form, Modal, DatePicker, Divider } from 'antd'; +import { isEmpty, objectMapper } from '@/utils/commons'; +import { useTranslation } from 'react-i18next'; + +import SearchInput from '@/components/SearchInput'; +import DeptSelector from '@/components/DeptSelector'; +import ProductsTypesSelector, { fetchVendorList } from '@/components/ProductsTypesSelector'; +import dayjs from 'dayjs'; +import arraySupport from 'dayjs/plugin/arraySupport'; +import { copyAgencyDataAction } from '@/stores/Products/Index'; + +import useAuthStore from '@/stores/Auth'; +import RequireAuth from '@/components/RequireAuth'; +import { PERM_PRODUCTS_MANAGEMENT } from '@/config'; + +dayjs.extend(arraySupport); + +export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => { + const { t } = useTranslation(); + const [form] = Form.useForm(); + + const isPermitted = useAuthStore((state) => state.isPermitted); + + useEffect(() => { + onFormInstanceReady(form); + }, []); + + const onValuesChange = (changeValues, allValues) => {}; + return ( + + {action === '#' && + + } + + + + {action === '#' && + + + + } + + + + + current <= dayjs([source.sourceYear, 12, 31])} /> + + + ); +}; +const formValuesMapper = (values) => { + const destinationObject = { + 'agency': { + key: 'target_agency', + transform: (value) => { + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : ''; + }, + }, + 'source_use_year': [{ key: 'source_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }], + 'target_use_year': [{ key: 'target_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }], + 'products_types': { + key: 'products_types', + transform: (value) => { + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : ''; + }, + }, + 'dept': { + key: 'dept', + transform: (value) => { + return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : ''; + }, + }, + }; + let dest = {}; + const { agency, year, ...omittedValue } = values; + dest = { ...omittedValue, ...objectMapper(values, destinationObject) }; + for (const key in dest) { + if (Object.prototype.hasOwnProperty.call(dest, key)) { + dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key]; + } + } + // omit empty + // Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]); + return dest; +}; +/** + * + */ +export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubmit, onCancel, initialValues, loading, copyModalVisible, setCopyModalVisible }) => { + const { t } = useTranslation(); + const { notification, message } = App.useApp(); + const [formInstance, setFormInstance] = useState(); + + const [copyLoading, setCopyLoading] = useState(false); + const handleCopyAgency = async (param) => { + param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency; + setCopyLoading(true); + console.log(param); + const toID = param.target_agency; + const success = await copyAgencyDataAction({...param, source_agency: source.sourceAgency.travel_agency_id}); + setCopyLoading(false); + success ? message.success(t('Success')) : message.error(t('Failed')); + + if (typeof onSubmit === 'function') { + onSubmit(param); + } + // setCopyModalVisible(false); + // navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`); + }; + return ( + { + onCancel(); + formInstance?.resetFields(); + }} + destroyOnClose + onOk={async () => { + try { + const values = await formInstance?.validateFields(); + // formInstance?.resetFields(); + const dest = formValuesMapper(values); + handleCopyAgency(dest); + } catch (error) { + console.log('Failed:', error); + } + }}> + +
+ {t('products:CopyFormMsg.Source')}: {source.sourceAgency.travel_agency_name} + + {source.sourceYear} +
+
+ { + setFormInstance(instance); + }} + /> +
+ ); +}; +export default CopyProductsFormModal; diff --git a/src/views/products/Detail/Extras.jsx b/src/views/products/Detail/Extras.jsx new file mode 100644 index 0000000..f9d82ec --- /dev/null +++ b/src/views/products/Detail/Extras.jsx @@ -0,0 +1,176 @@ +import { useEffect, useState, useSyncExternalStore } from 'react'; +import { useParams } from 'react-router-dom'; +import { useTranslation } from 'react-i18next'; +import { App, Table, Button, Modal, Popconfirm } from 'antd'; +import { getAgencyProductExtrasAction, searchPublishedProductsAction, addProductExtraAction, delProductExtrasAction } from '@/stores/Products/Index'; +import { cloneDeep, pick } from '@/utils/commons'; +import SearchForm from '@/components/SearchForm'; + +import RequireAuth from '@/components/RequireAuth'; +import { PERM_PRODUCTS_MANAGEMENT } from '@/config'; +import { useProductsTypesMapVal } from '@/hooks/useProductsSets'; + +const NewAddonModal = ({ onPick, ...props }) => { + const { travel_agency_id, use_year } = useParams(); + const { t } = useTranslation(); + const { notification, message } = App.useApp(); + + const productsTypesMapVal = useProductsTypesMapVal(); + + const [open, setOpen] = useState(false); + const [loading, setLoading] = useState(false); // bind loading + const [searchLoading, setSearchLoading] = useState(false); + const [searchResult, setSearchResult] = useState([]); + + const onSearchProducts = async (values) => { + const copyObject = cloneDeep(values); + const { starttime, endtime, year, ...param } = copyObject; + setSearchLoading(true); + setSearchResult([]); + const search_year = year || use_year; + const result = await searchPublishedProductsAction({ ...param, use_year: search_year, }); + setSearchResult(result); + setSearchLoading(false); + }; + const handleAddExtras = async (item) => { + if (typeof onPick === 'function') { + onPick(item); + } + }; + + const searchResultColumns = [ + { key: 'ptype', dataIndex: 'type', width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text]?.label || text }, + { key: 'code', dataIndex: 'code', width: '6rem', title: t('products:Code') }, + { key: 'title', dataIndex: 'title', width: '16rem', title: t('products:Title') }, + // { + // title: t('products:price'), + // dataIndex: ['quotation', '0', 'adult_cost'], + // width: '10rem', + // render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`, + // }, + { + key: 'action', + title: '', + width: 150, + render: (_, record) => ( + + ), + }, + ]; + const paginationProps = { + showTotal: (total) => t('Table.Total', { total }), + }; + return ( + <> + + + setOpen(false)} destroyOnClose> + { + onSearchProducts(formVal); + }} + /> +
+ + + ); +}; + +/** + * + */ +const Extras = ({ productId, onChange, ...props }) => { + const { t } = useTranslation(); + const { notification, message } = App.useApp(); + + const { travel_agency_id, use_year } = useParams(); + + const [extrasData, setExtrasData] = useState([]); + + const handleGetAgencyProductExtras = async () => { + const data = await getAgencyProductExtrasAction({ id: productId, travel_agency_id, use_year }); + setExtrasData(data); + }; + + const handleNewAddOn = async (item) => { + // setExtrasData(prev => [].concat(prev, [item])); + // todo: 提交后端; 重复绑定同一个 + const _item = pick(item, ['id', 'title', 'code']); + const newSuccess = await addProductExtraAction({ travel_agency_id, id: productId, extras: [_item] }); + newSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`); + await handleGetAgencyProductExtras(); + } + + const handleDelAddon = async (item) => { + const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, extras: [item.id] }); + delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`); + await handleGetAgencyProductExtras(); + }; + + useEffect(() => { + handleGetAgencyProductExtras(); + + return () => {}; + }, []); + + const columns = [ + { title: t('products:Title'), dataIndex: ['info', 'title'], width: '16rem', }, + // { + // title: t('products:Offer'), + // dataIndex: ['quotation', '0', 'value'], + // width: '10rem', + + // render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`, + // }, + // { title: t('products:Types'), dataIndex: 'age_type', width: '40%', }, + { + title: '', + dataIndex: 'operation', + width: '4rem', + render: (_, r) => ( + handleDelAddon(r)} okText={t('Yes')} > + + + ), + }, + ]; + + return ( + <> + +

{t('products:EditComponents.Extras')}

+
r.info.id} /> + + + + ); +}; +export default Extras; diff --git a/src/views/products/Detail/addValidityWithWeekend.jsx b/src/views/products/Detail/addValidityWithWeekend.jsx new file mode 100644 index 0000000..1dd0005 --- /dev/null +++ b/src/views/products/Detail/addValidityWithWeekend.jsx @@ -0,0 +1,55 @@ + +import React, { useState } from 'react'; +import { DatePicker, Button } from 'antd'; +import dayjs from 'dayjs'; +import { useDatePresets } from '@/hooks/useDatePresets'; +const addValidityWithWeekend = ({ onDateChange }) => { + const dateFormat = 'YYYY/MM/DD'; + const { RangePicker } = DatePicker; + const [dateRange, setDateRange] = useState(null); + const [selectedDays, setSelectedDays] = useState([]); + const presets = useDatePresets(); + const days = [ + 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday' + ]; + + + const handleChange = (date, dateString) => { + console.log("dateString",dateString) + onDateChange({ dateRange: dateString, selectedDays }); + }; + + + + const handleDayClick = (day) => { + setSelectedDays((prevSelectedDays) => { + const updatedDays = prevSelectedDays.includes(day) + ? prevSelectedDays.filter((d) => d !== day) + : [...prevSelectedDays, day]; + onDateChange({ dateRange, selectedDays: updatedDays }); + return updatedDays; + }); + }; + + return ( +
+

Data

+ {} +

Weekdays

+
+ {days.map((day, index) => ( + + ))} +
+
+ ); +}; + +export default addValidityWithWeekend; diff --git a/src/views/products/Manage.jsx b/src/views/products/Manage.jsx new file mode 100644 index 0000000..1aa105c --- /dev/null +++ b/src/views/products/Manage.jsx @@ -0,0 +1,118 @@ +import { useEffect, useState } from 'react'; +import { Link, useNavigate } from 'react-router-dom'; +import { App, Space, Table, Button, Modal, Divider } from 'antd'; +import SearchForm from '@/components/SearchForm'; +import dayjs from 'dayjs'; +import arraySupport from 'dayjs/plugin/arraySupport'; +import { useTranslation } from 'react-i18next'; +import useProductsStore, { copyAgencyDataAction } from '@/stores/Products/Index'; +import useFormStore from '@/stores/Form'; +import { objectMapper } from '@/utils/commons'; +import CopyProductsFormModal from './Detail/CopyProducts'; + +dayjs.extend(arraySupport); + +function Index() { + const { notification, message } = App.useApp(); + const navigate = useNavigate() + const { t } = useTranslation(); + const [loading, agencyList, searchAgency] = useProductsStore((state) => [state.loading, state.agencyList, state.searchAgency]); + const [searchValues, setSearchValues] = useProductsStore((state) => [state.searchValues, state.setSearchValues]); + const formValuesToSub = useFormStore(state => state.formValuesToSub); + + const useYear = formValuesToSub.year; + + const handleSearchAgency = (formVal = undefined) => { + const { starttime, endtime, ...param } = formVal || formValuesToSub; + const searchParam = objectMapper(param, { agency: 'travel_agency_ids', startdate: 'edit_date1', enddate: 'edit_date2', year: 'use_year' }); + setSearchValues(searchParam); + searchAgency(searchParam); + } + + const [copyModalVisible, setCopyModalVisible] = useState(false); + const [sourceAgency, setSourceAgency] = useState({}); + const [copyAction, setCopyAction] = useState(); + const [copyLoading, setCopyLoading] = useState(false); + const handleCopyAgency = async (param) => { + // setCopyLoading(true); + const toID = param.target_agency; + // const success = await copyAgencyDataAction({...param, source_agency: sourceAgency.travel_agency_id}); + // setCopyLoading(false); + // success ? message.success('复制成功') : message.error('复制失败'); + + setCopyModalVisible(false); + navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`); + }; + + const openCopyModal = (from, action) => { + setSourceAgency(from); + setCopyAction(action); + setCopyModalVisible(true); + }; + + useEffect(() => { + // handleSearchAgency(); + }, []); + + const showTotal = (total) => t('Table.Total', { total }); + + const columns = [ + { title: t('products:Vendor'), key: 'vendor', dataIndex: 'travel_agency_name' }, + { title: t('products:CreatedBy'), key: 'poster_by', dataIndex: 'poster_name' }, + { title: t('products:CreateDate'), key: 'poster_date', dataIndex: 'poster_date' }, + { title: t('products:AuState'), key: 'audit_state', dataIndex: 'audit_state' }, + { title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by_name' }, + { title: t('products:AuditDate'), key: 'audit_date', dataIndex: 'audit_date' }, + { + title: '', + key: 'action', + render: (_, r) => ( + + {t('Edit')} + {t('Audit')} + + + + ), + }, + ]; + return ( + + { + handleSearchAgency(formVal); + }} + /> +
+ + {/* 复制弹窗 */} + setCopyModalVisible(false)} + onSubmit={(formVal) => { + handleCopyAgency(formVal); + }} + {...{copyModalVisible, setCopyModalVisible}} + /> + + ); +} + +export default Index; diff --git a/src/views/reservation/Detail.jsx b/src/views/reservation/Detail.jsx index 2e6f8f8..e2cd7c2 100644 --- a/src/views/reservation/Detail.jsx +++ b/src/views/reservation/Detail.jsx @@ -1,19 +1,19 @@ -import { useParams } from "react-router-dom" +import { useParams } from 'react-router-dom' import { useEffect, useState } from 'react' import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App } from 'antd' import { FileOutlined -} from '@ant-design/icons'; +} from '@ant-design/icons' import { usingStorage } from '@/hooks/usingStorage' import useReservationStore from '@/stores/Reservation' import { useTranslation } from 'react-i18next' import BackBtn from '@/components/BackBtn' -const { Title, Paragraph } = Typography; -const { TextArea } = Input; +const { Title, Paragraph } = Typography +const { TextArea } = Input function Detail() { - const { t } = useTranslation(); + const { t } = useTranslation() const confirmationListColumns = [ { @@ -43,9 +43,9 @@ function Detail() { ]; function detailTextRender(text, confirm) { - const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, "\n——————————————————————\n"); + const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, '\n——————————————————————\n'); return ( -
+
{formattedText}
); @@ -56,7 +56,7 @@ function Detail() { <> {confirm.attachmentList.map(attch => { return ( - }>{attch.file_name} + }>{attch.file_name} )} )} @@ -65,7 +65,7 @@ function Detail() { function confirmRender(text, confirm) { return ( - + ); } @@ -96,7 +96,7 @@ function Detail() { const showConfirmModal = (confirm) => { setIsModalOpen(true); - const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, "\n——————————————————————\n"); + const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, '\n——————————————————————\n'); setConfirmText(formattedText); selectConfirmation(confirm); }; @@ -140,7 +140,7 @@ function Detail() { {t('group:ConfirmationDetails')}
-
+
{confirmText}
@@ -155,7 +155,7 @@ function Detail() { }} /> - +
{t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate}; @@ -165,17 +165,19 @@ function Detail() { - - + + - - + + - +
{text} ) @@ -30,7 +30,7 @@ function Newest() { { title: t('group:ArrivalDate'), dataIndex: 'arrivalDate', - render: (text, record) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')), + render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')), }, { title: t('group:Pax'), @@ -43,7 +43,7 @@ function Newest() { { title: t('group:ResSendingDate'), dataIndex: 'reservationDate', - render: (text, record) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')), + render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')), }, { title: t('group:Guide'), @@ -52,7 +52,7 @@ function Newest() { }, ]; - function guideRender(text, reservation) { + function guideRender(_, reservation) { if (reservation.guide === '') { return ( @@ -69,13 +69,11 @@ function Newest() { } } - function cityGuideRender(text, city) { + function cityGuideRender(_, city) { return (
- +