Compare commits

..

4 Commits

1
.gitignore vendored

@ -24,4 +24,3 @@ dist-ssr
*.sw?
/package-lock.json
pnpm-lock.yaml

@ -7,21 +7,6 @@ Global Highlights Hub 海外供应商平台
2. 运行开发环境npm run dev 或者 start.bat
3. 打包代码npm run build 或者 build.bat
## 版本设置
npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]
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
npm version prerelease
2.0.0-alpha-0 -> 2.0.0-alpha-1 -> 2.0.0-alpha-2 ..n -> 2.0.0-alpha-n
npm version patch --no-git-tag-version
2.0.0-n -> 2.0.0
"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
@ -33,19 +18,19 @@ antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
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
生成Docx文档 https://docx.js.org/#/?id=welcome
## 阿里云OSS
Bucket 名称global-highlights-hub
Endpointoss-cn-hongkong.aliyuncs.com
global-highlights-hub.oss-cn-hongkong.aliyuncs.com
反馈表测试链接
http://202.103.68.111:5173/feedback/330948
---
```mermaid
---
title: GHHub 开发管理 - 机票和价格管理
@ -53,7 +38,6 @@ title: GHHub 开发管理 - 机票和价格管理
gitGraph TB:
commit
commit
branch release
branch feature/i18n
checkout main
commit id: "release" tag: "1.0.5.31"
@ -68,19 +52,16 @@ gitGraph TB:
checkout main
commit
commit
commit id: "账户体系"
checkout feature/price_manager
commit
commit
commit
commit id: "完成价格"
checkout main
merge feature/price_manager id: "合并价格" type:HIGHLIGHT
commit
commit
commit tag: "2.0.0" id: "完成机票"
commit
commit
commit id: "pre-release" tag: "pre-release" type:HIGHLIGHT
commit tag: "1.1.0" id: "完成机票"
branch pre-release
merge feature/price_manager id: "合并机票+价格" type:HIGHLIGHT
checkout main
merge pre-release id: "合并发布" tag: "1.2.0"
commit
```

@ -1 +0,0 @@
npm version patch

@ -1 +0,0 @@
npm version prerelease

@ -1,5 +1,3 @@
use Tourmanager
CREATE TABLE auth_role
(
[role_id] [int] IDENTITY(1,1) NOT NULL,
@ -63,15 +61,11 @@ 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 ('所有火车票功能', '/train-ticket/all', 'train-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/new', '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')
@ -88,9 +82,15 @@ 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_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('采购年份', 'route=/products/pick-year', 'page')
VALUES ('产品管理(供应商)', 'route=/products?from', '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)

Binary file not shown.

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Highlights Hub</title>
<title>Global Highlights Hub</title>
<style>
.loading{width:150px;height:8px;border-radius:4px;margin:0 auto;margin-top:200px;position:relative;background:#777;overflow:hidden}
.loading span{display:block;width:100%;height:100%;border-radius:3px;background:#00b96b;animation:changePosition 4s linear infinite}

@ -1,36 +1,31 @@
{
"name": "global-highlights-hub",
"name": "global.highlights.hub",
"private": true,
"version": "2.2.5",
"version": "2.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"4test": "vite build --mode test",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@react-pdf/renderer": "^3.4.0",
"antd": "^5.27.0",
"dayjs": "^1.11.13",
"docx": "^8.5.0",
"file-saver": "^2.0.5",
"antd": "^5.17.2",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^14.1.2",
"react-router-dom": "^6.30.1",
"react-router-dom": "^6.10.0",
"react-to-pdf": "^1.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^4.5.7"
"zustand": "^4.5.2"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-legacy": "^4.0.2",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -7,36 +7,6 @@
"Fail": "Failed to change password. Please try again.",
"CurrentPassword": "Please input your password.",
"NewPassword": "Please input your new password.",
"ReenterPassword": "Please reenter your password.",
"username": "请重复输入用户名。",
"realname": "请重复输入真实姓名。",
"email": "请重复输入邮箱。",
"travelAgency": "请重复输供应商。",
"role": "请重复输入角色。",
"roleName": "请重复输入角色名称。"
},
"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"
"ReenterPassword": "Please reenter your password."
}
}

@ -19,50 +19,50 @@
"Back": "Back",
"Download": "Download",
"Upload": "Upload",
"Preview": "Preview",
"preview": "Preview",
"Total": "Total",
"Action": "Action",
"Import": "Import",
"Export": "Export",
"Copy": "Copy",
"sureCancel": "Are you sure to cancel?",
"sureDelete": "Are you sure to delete?",
"sureSubmit": "Are you sure to submit?",
"sureDelete":"Are you sure to delete?",
"Yes": "Yes",
"No": "No",
"Success": "Success",
"Failed": "Failed",
"All": "All",
"Table": {
"Total": "Total {{total}} items"
},
"operator": "Operator",
"time": "Time",
"Login": "Login",
"Username": "Username",
"Realname": "Realname",
"Password": "Password",
"ChangePassword": "Change password",
"Profile": "Profile",
"Logout": "Logout",
"LoginTimeout": "Login timeout",
"LoginTimeoutTip": "Please input your password",
"userProfile": "User Profile",
"Telephone": "Telephone",
"Email": "Email address",
"Address": "Address",
"Company": "Company",
"Department": "Department",
"datetime": {
"thisWeek": "This Week",
"lastWeek": "Last Week",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"nextMonth": "Next Month",
"lastThreeMonth": "Last Three Month",
"nextThreeMonth": "Next Three Month",
"firstHalfYear": "First Half Year",
"latterHalfYear": "Latter Half Year",
"thisYear": "This Year"
},
"weekdays": {
@ -90,7 +90,6 @@
"Notice": "Notice",
"Report": "Report",
"Airticket": "AirTicket",
"Trainticket": "TrainTicket",
"Products": "Products"
},
"Validation": {
@ -98,13 +97,5 @@
"LoginFailed": "Incorrect password, Login failed.",
"UsernameIsEmpty": "Please input your username",
"PasswordIsEmpty": "Please input your password"
},
"invoiceStatus": {
"Status": "Status",
"Not_submitted": "Not submitted",
"Submitted": "Submitted",
"Travel_advisor_approved": "Travel advisor approved",
"Finance_Dept_arrproved": "Finance Dept arrproved",
"Paid": "Paid"
}
}

@ -1,7 +1,6 @@
{
"ArrivalDate": "Arrival Date",
"RefNo": "Reference number",
"unconfirmed": "Unconfirmed",
"Pax": "Pax",
"Status": "Status",
"City": "City",

@ -1,155 +1,111 @@
{
"ProductType": "Product Type",
"ProductName": "Product Name",
"ContractRemarks": "合同备注",
"versionHistory": "Version History",
"versionPublished": "Published",
"versionSnapshot": "Snapshot",
"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": "per Person",
"1": "per Group",
"title": "Price Unit"
},
"State": "State",
"Title": "Title",
"Vendor": "Vendor",
"AuState": "Audit State",
"CreatedBy": "Created By",
"CreateDate": "Create Date",
"AuditedBy": "Audited By",
"AuditDate": "Audit Date",
"AuditRes": "Audit Result",
"OpenHours": "Open Hours",
"Duration": "Duration",
"KM": "KM",
"RecommendsRate": "Recommends Rate",
"OpenWeekdays": "Open Weekdays",
"DisplayToC": "Display To C",
"SortOrder": "Sort order",
"subTypeD": "Package Type",
"Dept": "Dept",
"Code": "Code",
"City": "City",
"Remarks": "Memo",
"Description": "Description",
"addQuotation": "Add quotation",
"adultPrice": "Adult price",
"childrenPrice": "Child price",
"currency": "Currency",
"unit_name": "Unit",
"group_size": "Group Size",
"use_dates": "Use Dates",
"operation": "operation",
"price": "Price",
"Offer": "Offer",
"Weekdays": "Weekdays",
"OnWeekdays": "On Weekdays: ",
"Unlimited": "Unlimited",
"UseYear": "Use Year",
"AgeType": {
"Type": "Age Type",
"Adult": "Adult",
"Child": "Child"
},
"CopyFormMsg": {
"Source": "Source ",
"target": "Target ",
"withQuote": "Whether to copy the quotation",
"requiredVendor": "Please pick a target vendor",
"requiredTypes": "Please select product types",
"requiredDept": "Please pick a owner department",
"copyTo": "Copy to"
},
"Validation": {
"adultPrice": "请输入成人价",
"childrenPrice": "请输入儿童价",
"currency": "请输入币种",
"unit_name": "请输入类型",
"group_size_min": "请输入人等",
"group_size_max": "请输入人等",
"use_dates": "请输入有效期"
},
"sureSubmitAudit": "确认提交产品审核? 提交后,产品将进入待审核流程. ",
"FormTooltip": {
"Type": "Product Type",
"Title": "Title",
"Code": "Code",
"City": "起始城市,举例:北京",
"Dept": "Department",
"Duration": "",
"RecommendsRate": "",
"OpenHours": "Open Hours",
"OpenWeekdays": "Open Weekdays",
"DisplayToC": "",
"KM": "",
"Description": "",
"Remarks": "",
"PriceUnit": "报价单位:每人/每团",
"UseDates": "数值越短价格越优先。举例英文导游1.1-12.31价300元 8.1-8.31价400元如走团时间为8月自动取400元。",
"NewTitle": {
"6": "",
"7": "官方景点名称,举例:陕西历史博物馆",
"8": "举例:故宫导游门票、上海外滩深度游导游补助",
"B": "A点-B点有疑议的需加往返/单程),举例:北京大兴机场-市区(单程)",
"J": "A点-B点时长车费有疑议的需加往返/单程),举例:张家界高铁站-市区(车费,单程)",
"Q": "城市语种导游工资单位,举例:北京英语导游工资(元/天/团)",
"R": "普通餐标、豪华餐标、特色餐标,举例:普通餐标",
"D": "城市A点-B点时间包含内容举例北京市区一日游车导"
},
"----Todo: 下面一组待定": "#",
"B": {
"Title": "A点-B点举例桂林-龙胜",
"#": ""
},
"J": {
"Title": "A点-B点时长车费举例张家界5晚6天车费",
"#": ""
},
"Q": {
"Title": "城市语种导游工资单位,举例:北京英文导游工资(元/天/团)",
"#": ""
},
"R": {
"Title": "普通、豪华、特色餐标,举例:普通餐标",
"#": ""
},
"D": {
"Title": "城市A点-B点时间包含内容举例北京市区一日游车导",
"#": ""
}
},
"LgcModal": {
"title": "Add language",
"placeholder": "Pick a language"
},
"#": "Product"
}
{
"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": "DisplayToC",
"Dept": "Dept",
"productProject": "Product project",
"Code": "Code",
"City": "City",
"Remarks": "Remarks",
"tourTime": "Tour time",
"recommendationRate": "Recommends rate",
"Name": "Name",
"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"
},
"save":"save",
"edit":"edit",
"delete":"delete",
"cancel":"cancel",
"sureCancel":"Sure you want to cancel?",
"sureDelete":"Sure you want to delete?",
"CopyFormMsg": {
"requiredVendor": "Please pick a target vendor",
"requiredTypes": "Please select product types",
"requiredDept": "Please pick a owner department"
},
"#": "#"
}

@ -7,13 +7,7 @@
"Fail": "密码更新失败",
"CurrentPassword": "请输入密码。",
"NewPassword": "请输入新密码。",
"ReenterPassword": "请重复输入密码。",
"username": "请重复输入用户名。",
"realname": "请重复输入真实姓名。",
"email": "请重复输入邮箱。",
"travelAgency": "请重复输供应商。",
"role": "请重复输入角色。",
"roleName": "请重复输入角色名称。"
"ReenterPassword": "请重复输入密码。"
},
"createdOn": "创建时间",
"action": "操作",
@ -23,14 +17,12 @@
"action.enable.title": "确定启用该账号吗?",
"action.disable.title": "确定禁用该账号吗?",
"action.resetPassword": "重置密码",
"action.resetPassword.tile": "确定重置账号密码吗?",
"accountList": "管理账号",
"newAccount": "新增账号",
"detail": "详细信息",
"username": "用户名",
"realname": "姓名",
"travelAgency": "供应商",
"travelAgencyName": "供应商名称",
"email": "邮箱地址",
"lastLogin": "最后登陆时间",

@ -19,50 +19,50 @@
"Back": "返回",
"Download": "下载",
"Upload": "上传",
"Preview": "预览",
"preview": "预览",
"Total": "总数",
"Action": "操作",
"Import": "导入",
"Export": "导出",
"Copy": "复制",
"sureCancel": "确定取消?",
"sureDelete": "确定删除?",
"sureSubmit": "确定提交?",
"sureDelete":"确定删除?",
"Yes": "是",
"No": "否",
"Success": "成功",
"Failed": "失败",
"All": "所有",
"Table": {
"Total": "共 {{total}} 条"
},
"operator": "操作",
"time": "时间",
"Login": "登录",
"Username": "账号",
"Realname": "姓名",
"Password": "密码",
"ChangePassword": "修改密码",
"Profile": "账户中心",
"Logout": "退出",
"LoginTimeout": "登录超时",
"LoginTimeoutTip": "请输入密码",
"userProfile": "账号信息",
"Telephone": "联系电话",
"Email": "电子邮箱",
"Address": "公司地址",
"Company": "公司名称",
"Department": "部门",
"datetime": {
"thisWeek": "本周",
"lastWeek": "上周",
"thisMonth": "本月",
"lastMonth": "上月",
"nextMonth": "下月",
"lastThreeMonth": "前三个月",
"nextThreeMonth": "后三个月",
"firstHalfYear": "上半年",
"latterHalfYear": "下半年",
"thisYear": "今年"
},
"weekdays": {
@ -90,7 +90,6 @@
"Notice": "通知",
"Report": "质量评分",
"Airticket": "机票订票",
"Trainticket": "火车订票",
"Products": "产品管理"
},
"Validation": {
@ -98,13 +97,5 @@
"LoginFailed": "密码错误,登陆失败。",
"UsernameIsEmpty": "请输入账号",
"PasswordIsEmpty": "请输入密码"
},
"invoiceStatus": {
"Status": "审核状态",
"Not_submitted": "待提交",
"Submitted": "待审核",
"Travel_advisor_approved": "顾问已审核",
"Finance_Dept_arrproved": "财务已审核",
"Paid": "已打款"
}
}

@ -1,7 +1,6 @@
{
"ArrivalDate": "抵达日期",
"RefNo": "团号",
"unconfirmed": "未确认",
"Pax": "人数",
"Status": "状态",
"City": "城市",
@ -12,5 +11,6 @@
"ConfirmationDate": "确认日期",
"ConfirmationDetails": "确认信息",
"PNR": "旅客订座记录",
"#": "#"
}

@ -1,134 +1,112 @@
{
"ProductType": "项目类型",
"ProductName": "产品名称",
"ContractRemarks": "合同备注",
"versionHistory": "查看历史",
"versionPublished": "已发布的",
"versionSnapshot": "快照",
"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": "报价单位"
},
"State": "状态",
"Title": "名称",
"Vendor": "供应商",
"AuState": "审核状态",
"CreatedBy": "提交人员",
"CreateDate": "提交时间",
"AuditedBy": "审核人员",
"AuditDate": "审核时间",
"AuditRes": "审核结果",
"OpenHours": "游览时间",
"Duration": "游览时长",
"KM": "公里数",
"RecommendsRate": "推荐指数",
"OpenWeekdays": "开放时间",
"DisplayToC": "报价信显示",
"SortOrder": "排序",
"subTypeD": "包价类型",
"Dept": "小组",
"Code": "简码",
"City": "城市",
"Remarks": "备注",
"Description": "描述",
"addQuotation": "添加报价",
"adultPrice": "成人价",
"childrenPrice": "儿童价",
"currency": "币种",
"unit_name": "类型",
"group_size": "人等",
"use_dates": "有效期",
"operation": "操作",
"price": "价格",
"Offer": "报价",
"Weekdays": "周末",
"OnWeekdays": "周: ",
"Unlimited": "不限",
"UseYear": "年份",
"AgeType": {
"Type": "人群",
"Adult": "成人",
"Child": "儿童"
},
"CopyFormMsg": {
"Source": "源",
"target": "目标",
"withQuote": "是否复制报价",
"requiredVendor": "请选择目标供应商",
"requiredTypes": "请选择产品类型",
"requiredDept": "请选择所属小组",
"copyTo": "复制到"
},
"Validation": {
"adultPrice": "请输入成人价",
"childrenPrice": "请输入儿童价",
"currency": "请输入币种",
"unit_name": "请输入类型",
"group_size_min": "请输入人等",
"group_size_max": "请输入人等",
"use_dates": "请输入有效期"
},
"sureSubmitAudit": "确认提交产品审核? 提交后,产品将进入待审核流程. ",
"FormTooltip": {
"Type": "Product Type",
"Title": "Title",
"Code": "Code",
"City": "起始城市,举例:北京",
"Dept": "Department",
"Duration": "",
"RecommendsRate": "",
"OpenHours": "Open Hours",
"OpenWeekdays": "Open Weekdays",
"DisplayToC": "",
"KM": "",
"Description": "",
"Remarks": "",
"PriceUnit": "报价单位:每人/每团",
"UseDates": "数值越短价格越优先。举例英文导游1.1-12.31价300元 8.1-8.31价400元如走团时间为8月自动取400元。",
"NewTitle": {
"6": "",
"7": "官方景点名称,举例:陕西历史博物馆",
"8": "举例:故宫导游门票、上海外滩深度游导游补助",
"B": "A点-B点有疑议的需加往返/单程),举例:北京大兴机场-市区(单程)",
"J": "A点-B点时长车费有疑议的需加往返/单程),举例:张家界高铁站-市区(车费,单程)",
"Q": "城市语种导游工资单位,举例:北京英语导游工资(元/天/团)",
"R": "普通餐标、豪华餐标、特色餐标,举例:普通餐标",
"D": "城市A点-B点时间包含内容举例北京市区一日游车导"
}
},
"LgcModal": {
"title": "添加语种",
"placeholder": "选择语种"
},
"#": "产品"
}
{
"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": "儿童"
},
"save":"保存",
"edit":"编辑",
"delete":"删除",
"cancel":"取消",
"sureCancel": "确定取消?",
"sureDelete":"确定删除?",
"CopyFormMsg": {
"requiredVendor": "请选择目标供应商",
"requiredTypes": "请选择产品类型",
"requiredDept": "请选择所属小组"
},
"#": "#"
}

@ -1,6 +1,26 @@
@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;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -9,7 +9,7 @@ const BackBtn = ({to, ...props}) => {
const navigate = useNavigate();
return (
<>
{isNotEmpty(to) ? <Link to={to} className='px-4'>{t('Back')}</Link> : <Button type='link' onClick={() => navigate(-1)}>{t('Back')}</Button>}
{isNotEmpty(to) ? <Link to={to}>{t('Back')}</Link> : <Button type='link' onClick={() => navigate(-1)}>{t('Back')}</Button>}
</>
);
};

@ -0,0 +1,243 @@
import React, { useState } from 'react';
import { Table, Input, Button, DatePicker, Row, Col, Tag, Select } from 'antd';
const { RangePicker } = DatePicker;
const { Option } = Select;
const BatchImportPrice = () => {
const [startDate, setStartDate] = useState(null);
const [endDate, setEndDate] = useState(null);
const [startPeople, setStartPeople] = useState(1);
const [endPeople, setEndPeople] = useState(5);
const [dateRanges, setDateRanges] = useState([]);
const [peopleRanges, setPeopleRanges] = useState([]);
const [tableData, setTableData] = useState([]);
const [currency, setCurrency] = useState('RMB'); // RMB
const [type, setType] = useState('每人'); //
const handleGenerateTable = () => {
if (dateRanges.length === 0 || peopleRanges.length === 0) return;
const newData = dateRanges.flatMap(dateRange => {
const { startDate, endDate } = dateRange;
const dates = generateDateRange(startDate, endDate);
const row = { dateRange: `${startDate.format('YYYY-MM-DD')} ~ ${endDate.format('YYYY-MM-DD')}` };
peopleRanges.forEach(peopleRangeString => {
const [start, end] = peopleRangeString.split('-').map(Number);
generatePeopleRange(start, end).forEach(person => {
row[`${person}_adultPrice`] = '';
row[`${person}_childPrice`] = '';
});
});
dates.forEach(date => {
row[date] = '';
});
return row;
});
setTableData(newData);
};
const generateDateRange = (start, end) => {
const dates = [];
let currentDate = start.clone();
while (currentDate <= end) {
dates.push(currentDate.format('YYYY-MM-DD'));
currentDate = currentDate.add(1, 'day');
}
return dates;
};
const generatePeopleRange = (start, end) => {
const range = [];
for (let i = start; i <= end; i++) {
range.push(`人等${i}`);
}
return range;
};
const handleCellChange = (value, dateRange, peopleRange, type) => {
const newData = [...tableData];
const rowIndex = newData.findIndex(row => row.dateRange === dateRange);
newData[rowIndex][`${peopleRange}_${type}`] = value;
setTableData(newData);
};
const handleAddDateRange = () => {
if (startDate && endDate) {
const newDateRange = { startDate, endDate };
//
const isDateRangeExist = dateRanges.some(range => (
range.startDate.isSame(startDate, 'day') && range.endDate.isSame(endDate, 'day')
));
if (!isDateRangeExist) {
setDateRanges([...dateRanges, newDateRange]);
}
}
};
const handleAddPeopleRange = () => {
if (startPeople <= endPeople) {
const newPeopleRange = `${startPeople}-${endPeople}`;
//
const isPeopleRangeExist = peopleRanges.includes(newPeopleRange);
if (!isPeopleRangeExist) {
setPeopleRanges([...peopleRanges, newPeopleRange]);
}
}
};
const handleRemoveTag = (index, type) => {
if (type === 'date') {
setDateRanges(dateRanges.filter((_, i) => i !== index));
} else {
const removedPeopleRange = peopleRanges[index];
setPeopleRanges(peopleRanges.filter(range => range !== removedPeopleRange));
}
setTableData([]);
};
const [adultPrice, setAdultPrice] = useState('');
const [childPrice, setChildPrice] = useState('');
const handleAdultPriceChange = (value, dateRange) => {
const newData = [...tableData];
const rowIndex = newData.findIndex(row => row.dateRange === dateRange);
newData[rowIndex]['成人价'] = value;
setTableData(newData);
};
const handleChildPriceChange = (value, dateRange) => {
const newData = [...tableData];
const rowIndex = newData.findIndex(row => row.dateRange === dateRange);
newData[rowIndex]['儿童价'] = value;
setTableData(newData);
};
const columns = [
{
title: '日期\\人等',
dataIndex: 'dateRange',
key: 'dateRange',
},
...peopleRanges.flatMap(peopleRange => ([
{
title: peopleRange,
dataIndex: `${peopleRange}_price`,
key: `${peopleRange}_price`,
render: (text, record) => (
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<Input
value={record[`${peopleRange}_adultPrice`]}
onChange={(e) => handleCellChange(e.target.value, record.dateRange, peopleRange, 'adultPrice')}
placeholder="成人价"
style={{ width: '45%' }}
suffix={`${currency}/${type}`}
/>
<Input
value={record[`${peopleRange}_childPrice`]}
onChange={(e) => handleCellChange(e.target.value, record.dateRange, peopleRange, 'childPrice')}
placeholder="儿童价"
style={{ width: '45%' }}
suffix={`${currency}/${type}`}
/>
</div>
),
}
])),
];
return (
<>
<Row>
<Col span={4}>
<Select value={currency} onChange={value => setCurrency(value)} style={{ width: '100%', marginTop: 10 }}>
<Option value="RMB">RMB</Option>
<Option value="MY">MY</Option>
</Select>
</Col>
<Col span={4}>
<Select value={type} onChange={value => setType(value)} style={{ width: '100%', marginTop: 10 }}>
<Option value="每人">每人</Option>
<Option value="美团">美团</Option>
</Select>
</Col>
</Row>
<Row gutter={16}>
<Col span={8}>
<RangePicker
onChange={(dates) => {
if (dates && dates.length === 2) {
setStartDate(dates[0]);
setEndDate(dates[1]);
} else {
setStartDate(null);
setEndDate(null);
}
}}
/>
</Col>
<Button onClick={handleAddDateRange} type="primary">记录有效期</Button>
</Row>
<Row gutter={16}>
<Col span={8}>
<Input.Group compact style={{ marginTop: 10 }}>
<Input
style={{ width: 100, textAlign: 'center' }}
placeholder="Start"
value={startPeople}
onChange={(e) => setStartPeople(parseInt(e.target.value, 10))}
/>
<Input
style={{ width: 30, borderLeft: 0, pointerEvents: 'none', backgroundColor: '#fff' }}
placeholder="~"
disabled
/>
<Input
style={{ width: 100, textAlign: 'center', borderLeft: 0 }}
placeholder="End"
value={endPeople}
onChange={(e) => setEndPeople(parseInt(e.target.value, 10))}
/>
</Input.Group>
</Col>
<Button onClick={handleAddPeopleRange} type="primary" style={{ marginTop: 10 }}>记录人等</Button>
</Row>
<Button onClick={handleGenerateTable} type="primary" style={{ marginTop: 10 }}>生成表格</Button>
<Row gutter={16} style={{ marginTop: '16px' }}>
<Col span={24}>
{dateRanges.map((dateRange, index) => (
<Tag key={index} closable onClose={() => handleRemoveTag(index, 'date')}>
{`${dateRange.startDate.format('YYYY-MM-DD')} ~ ${dateRange.endDate.format('YYYY-MM-DD')}`}
</Tag>
))}
{peopleRanges.map((peopleRange, index) => (
<Tag key={index} closable onClose={() => handleRemoveTag(index, 'people')}>
{peopleRange}
</Tag>
))}
</Col>
</Row>
<Table
columns={columns}
dataSource={tableData}
bordered
pagination={false}
style={{ marginTop: '16px' }}
/>
</>
);
};
export default BatchImportPrice;

@ -1,29 +0,0 @@
import { createContext, useEffect, useState } from 'react';
import {} from 'antd';
import SearchInput from './SearchInput';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { useTranslation } from 'react-i18next';
//
export const fetchCityList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/search_cities`, { q });
return errcode !== 0 ? [] : result;
};
const CitySelector = ({ ...props }) => {
const { t } = useTranslation();
return (
<>
<SearchInput
placeholder={t('products:City')}
mode={null}
maxTagCount={0}
{...props}
fetchOptions={fetchCityList}
map={{ city_name: 'label', city_id: 'value' }}
/>
</>
);
};
export default CitySelector;

@ -1,94 +0,0 @@
import React, { useState, useEffect } from "react";
import { Tag, Button, message } from 'antd';
import { CaretUpOutlined, CaretDownOutlined, DownloadOutlined } from '@ant-design/icons';
import { utils, writeFile } from "xlsx";
import { isEmpty, getNestedValue } from "../utils/commons";
/**
* @property diffPercent
* @property diffData
* @property data1
* @property data2
*/
export const VSTag = (props) => {
const { diffPercent, diffData, data1, data2 } = props;
const CaretIcon = parseInt(diffPercent) < 0 ? CaretDownOutlined : CaretUpOutlined;
const tagColor = parseInt(diffPercent) < 0 ? 'gold' : 'lime';
return parseInt(diffPercent) === 0 ? (
'-'
) : (
<span>
{/* <div>
{data1} vs {data2}
</div> */}
<Tag icon={<CaretIcon />} color={tagColor}>
{diffPercent}<span>%</span>{' '}<span>{diffData}</span>
</Tag>
</span>
);
};
/**
* 导出表格数据存为xlsx
*/
export const TableExportBtn = (props) => {
const output_name = `${props.label}`;
const [columnsMap, setColumnsMap] = useState([]);
const [summaryRow, setSummaryRow] = useState({});
useEffect(() => {
const r1 = props.columns.reduce((r, v) => ({
...r,
...(v.children ? v.children.reduce((rc, vc, ci) => ({
...rc,
...(vc?.titleX ? {[`${v?.titleX || v.title},${vc.titleX}`]: vc.titleX } : {[(v?.titleX || v.title) + (ci || '')]: `${vc?.titleX || vc?.title || ''}`}),
}), {}) : {})
}), {});
const flatCols = props.columns.flatMap((v, k) =>
v.children ? v.children.map((vc, ci) => ({ ...vc, title: `${v?.titleX || v.title}` + (vc?.titleX ? `,${vc.titleX}` : (ci || '')) })) : {...v, title: `${v?.titleX || v.title}`}
);
// .filter((c) => c.dataIndex)
// !['string', 'number'].includes(typeof vc.title) ? `${v?.titleX || v.title}` : `${v?.titleX || v.title}-${vc.title || ''}`
;
setColumnsMap(flatCols);
// console.log('flatCols', flatCols);
setSummaryRow(r1);
// console.log('summaryRow', r1);
return () => {};
}, [props.columns]);
const onExport = () => {
if (isEmpty(props.dataSource)) {
message.warning('无结果.');
return false;
}
const data = props.dataSource.map((item) => {
const itemMapped = columnsMap.reduce((sv, kset) => {
const render_val = typeof kset?.render === 'function' ? kset.render('', item) : null;
const data_val = kset?.dataIndex ? (Array.isArray(kset.dataIndex) ? getNestedValue(item, kset.dataIndex) : item[kset.dataIndex]) : undefined;
const x_val = item[`${kset.dataIndex}_X`];
// const _title = kset.title.replace('-[object Object]', '');
const v = { [kset.title]: x_val || data_val || render_val };
return { ...sv, ...v };
}, {});
return itemMapped;
});
const ws = utils.json_to_sheet([].concat(isEmpty(summaryRow) ? [] : [summaryRow], data), { header: columnsMap.filter((r) => r.dataIndex).map((r) => r.title) });
const wb = utils.book_new();
utils.book_append_sheet(wb, ws, 'sheet');
writeFile(wb, `${output_name}.xlsx`);
};
return (
<Button
type="link"
icon={<DownloadOutlined />}
size="small"
disabled={false}
onClick={onExport}
>
{props.btnTxt || '导出excel'}
</Button>
);
};

@ -39,6 +39,7 @@ export const DeptSelector = ({show_all, isLeaf,...props}) => {
<div>
<Select
mode={props.mode}
style={{ width: '100%' }}
placeholder="选择小组"
labelInValue
maxTagCount={1}

@ -1,277 +0,0 @@
import { useEffect, useState } from "react";
import { Upload, List, Button, Tooltip, Popconfirm, Col, Row } from "antd";
import { UploadOutlined, FileTextOutlined, DeleteOutlined, StopOutlined } from "@ant-design/icons";
import { Image } from "antd";
import { fetchJSON } from "@/utils/request";
import { HT3_HOST } from "@/config";
//
export const simple_encrypt = text => {
const key = "TPDa*UU8h5%!zS";
let encrypted = [];
let keyIndex = 0;
for (let i = 0; i < text.length; i++) {
const charCode = text.charCodeAt(i);
const keyCharCode = key.charCodeAt(keyIndex);
const encryptedChar = charCode ^ keyCharCode;
encrypted.push(encryptedChar);
keyIndex = (keyIndex + 1) % key.length;
}
return encrypted.map(byte => byte.toString(16).padStart(2, "0")).join("");
};
//
const getImageList = async (key, overlist = false, ignore_case = true) => {
try {
let url;
if (overlist) {
url = `${HT3_HOST}/oss/list_over_unique_key?key=${key}&ignore_case=${ignore_case}`;
} else {
url = `${HT3_HOST}/oss/list_unique_key?key=${key}&ignore_case=${ignore_case}`;
}
const { errcode, result } = await fetchJSON(url);
if (errcode === 0) {
return result
.map(file => ({
key: file.key,
encrypt_key: file.encrypt_key,
size: file.size,
status: "done",
url: file.url,
last_modified: file.last_modified,
}))
.sort((a, b) => {
const dateA = new Date(a.last_modified);
const dateB = new Date(b.last_modified);
return dateA - dateB;
});
}
} catch (error) {
console.error("获取图片列表失败", error);
}
return [];
};
//
const deleteImage = async (key, ignore_case =true) => {
try {
const { errcode } = await fetchJSON(`${HT3_HOST}/oss/delete_unique_key?key=${key}&ignore_case=${ignore_case}`, {
method: "GET",
});
return errcode === 0;
} catch (error) {
console.error("删除图片失败", error);
return false;
}
};
//
const getSignature = async (file, key, onSuccess, onError, ignore_case = true) => {
try {
const { errcode, result } = await fetchJSON(`${HT3_HOST}/oss/signature_unique_key?key=${key}&filename=${file.name}&ignore_case=${ignore_case}`);
if (errcode === 0) {
const { method, host, signed_headers } = result;
const response = await fetch(host, {
method,
headers: signed_headers,
body: file,
});
if (response.ok) {
onSuccess(response, file);
} else {
onError(new Error("图片上传失败"));
}
}
} catch (error) {
console.error("获取签名失败:", error);
onError(error);
}
};
export const ImageUploader = props => {
const [fileList, setFileList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState("");
const key = simple_encrypt(props.osskey);
const ignore_case = props.ignore_case;
//
useEffect(() => {
const loadImages = async () => {
setIsLoading(true);
const images = await getImageList(key, false, ignore_case);
setFileList(images);
if (props.onChange) {
//
props.onChange(images);
}
setIsLoading(false);
};
if (key) {
loadImages();
}
}, [key]);
//
const handleDelete = async file => {
const success = await deleteImage(file.encrypt_key, ignore_case);
if (success) {
const newImages = fileList.filter(item => item.encrypt_key !== file.encrypt_key);
if (props.onChange) {
props.onChange(newImages);
}
setFileList(newImages);
//console.log("");
} else {
//console.error("");
}
};
//
const handleChange = ({ fileList: newFileList }) => {
setFileList(newFileList);
};
const handleUploadFile = ({ file, onProgress, onSuccess, onError }) => {
getSignature(
file,
key,
(response, file) => {
getImageList(key, false, ignore_case).then(newImages => {
if (props.onChange) {
props.onChange(newImages);
}
setFileList(prevList => {
//
const index = prevList.findIndex(item => item.status === "uploading");
if (index !== -1) {
const newPrevList = [...prevList];
newPrevList.splice(index, 1);
//
const newItems = newImages.filter(newItem => !newPrevList.some(prevItem => prevItem.key === newItem.key));
//
return [...newPrevList, ...newItems];
}
return prevList;
});
});
},
onError,
ignore_case
);
};
const handlePreview = file => {
if (!file.url) {
return;
}
setPreviewImage(file.url);
setPreviewOpen(true);
};
const handleRemove = () => {
return false;
};
return (
<>
<Upload
customRequest={handleUploadFile}
multiple={true}
onRemove={handleRemove}
listType="picture-card"
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
showUploadList={{
showDownloadIcon: true,
showRemoveIcon: props.deletable,
removeIcon: file => {
return (
<Popconfirm
title="Delete"
description="Are you sure you want to delete the file?"
onConfirm={() => {
handleDelete(file);
}}
onCancel={() => setFileList([...fileList])}
okText="Yes"
cancelText="No">
<DeleteOutlined />
</Popconfirm>
)
},
}}>
<div>
<UploadOutlined />
<div style={{ marginTop: 8 }}>Select File</div>
</div>
</Upload>
<List loading={isLoading} dataSource={fileList} />
{previewImage && (
<Image
wrapperStyle={{ display: "none" }}
preview={{
visible: previewOpen,
onVisibleChange: visible => setPreviewOpen(visible),
afterOpenChange: visible => !visible && setPreviewImage(""),
}}
src={previewImage}
/>
)}
</>
);
};
export const ImageViewer = props => {
const [fileList, setFileList] = useState([]);
const key = props.osskey;
const overlist = props.overlist;
const ignore_case = props.ignore_case;
//
useEffect(() => {
const loadImages = async () => {
const images = await getImageList(key, overlist,ignore_case);
setFileList(images);
if (props.onChange) {
//
props.onChange(images);
}
};
if (key) {
loadImages();
}
}, [key]);
return (
<>
<Image.PreviewGroup>
<Row gutter={[20, 20]}>
{fileList &&
fileList.map(item => {
return (
<Col key={item.encrypt_key}>
{item.key.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i) ? (
<Image width={200} src={item.url} />
) : (
<a href={item.url} download>
<Button type="primary" icon={<FileTextOutlined />} size="large" title={item.key.replace(/^.*[\\\/]/, "")}>
...{item.key.slice(-10)}
</Button>
</a>
)}
</Col>
);
})}
</Row>
</Image.PreviewGroup>
</>
);
};
export default ImageUploader;

@ -1,76 +0,0 @@
import { useState } from "react";
import { Popover, message, FloatButton, Button, Form, Input } from "antd";
import { BugOutlined } from "@ant-design/icons";
import useAuthStore from "@/stores/Auth";
import { uploadPageSpyLog, sendNotify } from "@/pageSpy";
function LogUploader() {
const [open, setOpen] = useState(false);
const hide = () => {
setOpen(false);
};
const handleOpenChange = (newOpen) => {
setOpen(newOpen);
};
const [currentUser] = useAuthStore((s) => [s.currentUser]);
const [messageApi, contextHolder] = message.useMessage();
const [formBug] = Form.useForm();
const popoverContent = (
<Form
layout={"vertical"}
form={formBug}
initialValues={{ problem: '' }}
scrollToFirstError
onFinish={async (values) => {
const success = await uploadPageSpyLog();
messageApi.success("Thanks for the feedback😊");
if (success) {
sendNotify(currentUser?.realname + "说:" + values.problem);
} else {
sendNotify(currentUser?.realname + "上传日志失败");
}
hide();
formBug.setFieldsValue({problem: ''});
}}
>
<Form.Item
name="problem"
label="Need help?"
rules={[{ required: true, message: "Specify issue needing support." }]}
>
<Input.TextArea rows={3} />
</Form.Item>
<Button
type="primary"
htmlType="submit"
color="cyan"
variant="solid"
block
>
Submit
</Button>
</Form>
);
return (
<>
{contextHolder}
<Popover
content={popoverContent}
trigger={["click"]}
placement="topRight"
open={open}
onOpenChange={handleOpenChange}
fresh
destroyOnHidden
>
<FloatButton icon={<BugOutlined />} />
</Popover>
</>
);
}
export default LogUploader;

@ -1,77 +0,0 @@
import { useEffect, useState } from 'react';
import { Spin, Cascader } from 'antd';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { useTranslation } from 'react-i18next';
import { groupBy } from '@/utils/commons';
//
export const fetchAgencyProductsList = async (params) => {
const map = { title: 'label', id: 'value' };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/travel_agency_products`, params);
const byTypes = errcode !== 0 ? {} : (groupBy(result.products, (row) => row.info.product_type_name));
// console.log(byTypes)
return Object.keys(byTypes).map((type_name) => ({
label: type_name,
title: type_name,
key: type_name,
value: type_name,
// disableCheckbox: true,
level: 1,
options: byTypes[type_name].map((row) => ({ ...row, label: `${row.info.code} : ${row.info.title}`, value: row.info.id })),
children: byTypes[type_name].map((row) => ({ ...row, label: `${row.info.code} : ${row.info.title}`, value: row.info.id, key: row.info.id, level:2 })),
}));
};
const ProductsSelector = ({ params, value, ...props }) => {
const { t } = useTranslation();
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState([]);
const fetchAction = async () => {
setOptions([]);
setFetching(true);
const data = await fetchAgencyProductsList(params);
// console.log(data)
setOptions(data);
setFetching(false);
return data;
};
useEffect(() => {
fetchAction();
return () => {};
}, []);
const filter = (inputValue, path) => path.some((option) => option.label.toLowerCase().indexOf(inputValue.toLowerCase()) > -1);
const onCascaderChange = (value, selectedOptions) => {
// console.log(value, selectedOptions)
const selectedP = selectedOptions.map(([parent, item]) => item);
// console.log(selectedP);
if (typeof props.onChange === 'function') {
props.onChange(selectedP);
}
}
return (
<>
<Cascader
placeholder={t('products:ProductName')}
allowClear
expandTrigger="hover"
multiple
showCheckedStrategy={Cascader.SHOW_CHILD}
maxTagCount={0}
classNames={{ popup: { root: 'h-96 overflow-y-auto [&_.ant-cascader-menu]:h-full [&_.ant-cascader-checkbox-disabled]:hidden'}}}
{...props}
notFoundContent={fetching ? <Spin size='small' /> : null}
options={options}
onChange={onCascaderChange}
showSearch={{ filter }}
/>
</>
);
};
export default ProductsSelector;

@ -1,14 +0,0 @@
import { Select } from 'antd';
import { useProductsTypes } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
const ProductsTypesSelector = ({...props}) => {
const productsTypes = useProductsTypes();
const { t } = useTranslation();
return (
<>
<Select labelInValue allowClear placeholder={t('products:ProductType')} options={productsTypes} {...props}/>
</>
);
};
export default ProductsTypesSelector;

@ -4,7 +4,7 @@ import useAuthStore from '@/stores/Auth'
export default function RequireAuth({ children, ...props }) {
const [isPermitted, currentUser] = useAuthStore(state => [state.isPermitted, state.currentUser])
const isPermitted = useAuthStore((state) => state.isPermitted)
const { userId } = usingStorage()
if (isPermitted(props.subject)) {
@ -15,7 +15,7 @@ export default function RequireAuth({ children, ...props }) {
<Result
status='403'
title='403'
subTitle={`抱歉,你(${currentUser.username})没有权限使用该功能(${props.subject})`}
subTitle={`抱歉,你(${userId})没有权限使用该功能`}
/>
)
}

@ -1,399 +1,353 @@
import { useEffect } from "react";
import { Form, Input, Row, Col, Select, DatePicker, Space, Button, Checkbox } from "antd";
import { objectMapper, at } from "@/utils/commons";
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from "@/config";
import useFormStore from "@/stores/Form";
import { useDatePresets } from "@/hooks/useDatePresets";
import { useTranslation } from "react-i18next";
import { useEffect } from 'react';
import { Form, Input, Row, Col, Select, DatePicker, Space, Button } from 'antd';
import { objectMapper, at } from '@/utils/commons';
import { DATE_FORMAT, SMALL_DATETIME_FORMAT } from '@/config';
import useFormStore from '@/stores/Form';
import { useDatePresets } from '@/hooks/useDatePresets';
import { useProductsTypes } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
import SearchInput from "./SearchInput";
import AuditStateSelector from "./AuditStateSelector";
import DeptSelector from "./DeptSelector";
import ProductsTypesSelector from "./ProductsTypesSelector";
import CitySelector from "@/components/CitySelector";
import VendorSelector from "@/components/VendorSelector";
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import SearchInput from './SearchInput';
import AuditStateSelector from './AuditStateSelector';
import DeptSelector from './DeptSelector';
//
export const fetchVendorList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q })
return errcode !== 0 ? [] : result
}
const { RangePicker } = DatePicker;
const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, formName, formLayout, loading, ...props }) => {
const { t } = useTranslation();
const presets = useDatePresets();
const [formValues, setFormValues] = useFormStore(state => [state.formValues, state.setFormValues]);
const [formValuesToSub, setFormValuesToSub] = useFormStore(state => [state.formValuesToSub, state.setFormValuesToSub]);
const [form] = Form.useForm();
const { sort, hides, shows, fieldProps, fieldComProps } = {
sort: "",
// initialValue: '',
fieldProps: "",
fieldComProps: "",
hides: [],
shows: [],
...props.fieldsConfig,
};
const readValues = { ...initialValue, ...formValues };
export const SelectProductsTypes = ({...props}) => {
const productsTypes = useProductsTypes();
const { t } = useTranslation();
return (
<>
<Select labelInValue allowClear placeholder={t('products:ProductType')} options={productsTypes} {...props}/>
</>
);
};
const formValuesMapper = values => {
const destinationObject = {
keyword: { key: "keyword", transform: value => value || "" },
username: { key: "username", transform: value => value || "" },
referenceNo: { key: "referenceNo", transform: value => value || "" },
dates: [
{ key: "startdate", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "enddate", transform: arrVal => (arrVal ? arrVal[1].endOf('month').format(SMALL_DATETIME_FORMAT) : "") },
{ key: "starttime", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "endtime", transform: arrVal => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : "") },
],
invoiceStatus: { key: "invoiceStatus", transform: value => value?.value || value?.key || "", default: "" },
invoiceCheckStatus: { key: "invoiceCheckStatus", transform: value => value?.value || value?.key || "", default: "" },
audit_state: { key: "audit_state", transform: value => value?.value || value?.key || "", default: "" },
agency: {
key: "agency",
transform: value => {
return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : "";
},
},
year: [{ key: "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 => {
//console.log(value);
return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : "";
},
},
city: {
key: "city",
transform: value => {
return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : "";
},
},
plan_state: { key: "plan_state", transform: value => value?.value || value?.key || "", default: "" },
airticket_state: { key: "airticket_state", transform: value => value?.value || value?.key || "", default: "" },
unconfirmed: { key: "unconfirmed", transform: value => (value ? 1 : 0) },
};
let dest = {};
const { dates, ...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;
};
const SearchForm = ({ initialValue, onSubmit, onReset, confirmText, formName, formLayout, loading, ...props }) => {
const { t } = useTranslation();
const presets = useDatePresets();
const [formValues, setFormValues] = useFormStore((state) => [state.formValues, state.setFormValues]);
const [formValuesToSub, setFormValuesToSub] = useFormStore((state) => [state.formValuesToSub, state.setFormValuesToSub]);
const [form] = Form.useForm();
const { sort, hides, shows, fieldProps, fieldComProps } = {
sort: '',
// initialValue: '',
fieldProps: '',
fieldComProps: '',
hides: [],
shows: [],
...props.fieldsConfig,
};
const readValues = { ...initialValue, ...formValues };
useEffect(() => {
setFormValues(readValues);
const dest = formValuesMapper(readValues);
setFormValuesToSub(dest);
const formValuesMapper = (values) => {
const destinationObject = {
'keyword': { key: 'keyword', transform: (value) => value || '' },
'referenceNo': { key: 'referenceNo', transform: (value) => value || '' },
'dates': [
{ key: 'startdate', transform: (arrVal) => (arrVal ? arrVal[0].format(DATE_FORMAT) : '') },
{ key: 'enddate', transform: (arrVal) => (arrVal ? arrVal[1].format(DATE_FORMAT) : '') },
{ key: 'starttime', transform: (arrVal) => (arrVal ? arrVal[0].format(DATE_FORMAT) : '') },
{ key: 'endtime', transform: (arrVal) => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : '') },
],
'invoiceStatus': { key: 'invoiceStatus', transform: (value) => value?.value || value?.key || '', default: '' },
'audit_state': { key: 'audit_state', transform: (value) => value?.value || value?.key || '', default: '' },
'agency': {
key: 'agency',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'year': [
{ key: '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) => {
console.log(value);
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
};
let dest = {};
const { dates, ...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;
};
if (typeof onMounted === "function") {
onMounted(dest);
}
useEffect(() => {
const dest = formValuesMapper(formValues);
setFormValuesToSub(dest);
return () => {};
}, []);
return () => {};
}, []);
const onFinish = (values) => {
console.log('Received values of form, origin form value: \n', values);
const dest = formValuesMapper(values);
console.log('form value send to onSubmit:\n', dest);
const str = new URLSearchParams(dest).toString();
setFormValues(values);
setFormValuesToSub(dest);
if (typeof onSubmit === 'function') {
onSubmit(null, dest, values, str);
}
};
const onFinish = values => {
//console.log("Received values of form, origin form value: \n", values);
const dest = formValuesMapper(values);
//console.log("form value send to onSubmit:\n", dest);
const str = new URLSearchParams(dest).toString();
setFormValues(values);
setFormValuesToSub(dest);
if (typeof onSubmit === "function") {
onSubmit(null, dest, values, str);
}
};
const handleReset = () => {
form.setFieldsValue({
// 'DateType': undefined,
});
if (typeof onReset === 'function') {
onReset();
}
};
const onValuesChange = (changedValues, allValues) => {
const handleReset = () => {
form.setFieldsValue({
// 'DateType': undefined,
});
if (typeof onReset === "function") {
onReset();
}
};
const onValuesChange = (changedValues, allValues) => {
const dest = formValuesMapper(allValues);
setFormValues(allValues);
setFormValuesToSub(dest);
// console.log('form onValuesChange', Object.keys(changedValues), args);
};
const dest = formValuesMapper(allValues);
setFormValues(allValues);
setFormValuesToSub(dest);
// console.log('form onValuesChange', Object.keys(changedValues), args);
};
const onFinishFailed = ({ values, errorFields }) => {
console.log("form validate failed", "\nform values:", values, "\nerrorFields", errorFields);
};
const onFinishFailed = ({ values, errorFields }) => {
console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields);
};
return (
<>
<Form form={form} layout={"horizontal"} name={formName || "advanced_search"} className="orders-search-form" onFinish={onFinish} onValuesChange={onValuesChange} onFinishFailed={onFinishFailed}>
{/* <EditableContext.Provider value={form}> */}
<Row gutter={16}>
{getFields({ sort, initialValue: readValues, hides, shows, fieldProps, fieldComProps, form, presets, t })}
{/* 'textAlign': 'right' */}
<Col flex="1 0 90px" className="flex justify-normal items-start">
<Space align="center">
<Button size={"middle"} type="primary" htmlType="submit" loading={loading}>
{confirmText || t("Search")}
</Button>
{/* <Button size="small" onClick={onReset}>
return (
<>
<Form form={form} layout={'horizontal'} name={formName || 'advanced_search'} className='orders-search-form' onFinish={onFinish} onValuesChange={onValuesChange} onFinishFailed={onFinishFailed} >
{/* <EditableContext.Provider value={form}> */}
<Row gutter={16}>
{getFields({ sort, initialValue: readValues, hides, shows, fieldProps, fieldComProps, form, presets, t })}
{/* 'textAlign': 'right' */}
<Col flex='1 0 90px' className='flex justify-normal items-start' >
<Space align='center'>
<Button size={'middle'} type='primary' htmlType='submit' loading={loading}>
{confirmText || t('Search')}
</Button>
{/* <Button size="small" onClick={onReset}>
重置
</Button> */}
</Space>
</Col>
</Row>
{/* </EditableContext.Provider> */}
</Form>
</>
);
</Space>
</Col>
</Row>
{/* </EditableContext.Provider> */}
</Form>
</>
);
};
function getFields(props) {
const { fieldProps, fieldComProps, form, presets, t } = props;
const bigCol = 4 * 2;
const midCol = 6;
const layoutProps = {
// gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 4 },
md: { span: 8 },
sm: { span: 12 },
xs: { span: 24 },
};
const item = (name, sort = 0, render, col) => {
const customCol = col || 4;
const mdCol = customCol * 2;
return {
key: "",
sort,
name,
render,
hide: false,
col: { lg: { span: customCol }, md: { span: mdCol < 8 ? 10 : mdCol }, flex: mdCol < 8 ? "1 0" : "" },
};
};
let baseChildren = [];
baseChildren = [
item(
"keyword", //
99,
<Form.Item name="keyword" {...fieldProps.keyword}>
<Input allowClear {...fieldComProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
"referenceNo", //
99,
<Form.Item name="referenceNo" label={t("group:RefNo")} {...fieldProps.referenceNo}>
<Input placeholder={t("group:RefNo")} allowClear />
</Form.Item>,
fieldProps?.referenceNo?.col || 6
),
item(
"PNR", //PNR
99,
<Form.Item name="PNR" label="PNR">
<Input placeholder={t("group:PNR")} allowClear />
</Form.Item>,
fieldProps?.PNR?.col || 4
),
item(
"invoiceStatus", //
99,
<Form.Item name={`invoiceStatus`} initialValue={at(props, "initialValue.invoiceStatus")[0] || { value: "0", label: t("invoiceStatus.Status") }}>
<Select
labelInValue
options={[
{ value: "0", label: t("invoiceStatus.Status") },
{ value: "1", label: t("invoiceStatus.Not_submitted") },
{ value: "2", label: t("invoiceStatus.Submitted") },
{ value: "3", label: t("invoiceStatus.Travel_advisor_approved") },
{ value: "4", label: t("invoiceStatus.Finance_Dept_arrproved") },
{ value: "5", label: t("invoiceStatus.Paid") },
]}
/>
</Form.Item>,
fieldProps?.invoiceStatus?.col || 3
),
item(
"invoiceCheckStatus", //
99,
<Form.Item name={`invoiceCheckStatus`} initialValue={at(props, "initialValue.invoiceCheckStatus")[0] || { value: "0", label: "全部"}}>
<Select
labelInValue
options={[
{ value: "0", label: "全部" },
{ value: "1", label: "待提交" },
{ value: "2", label: "已提交待审核" },
{ value: "3", label: "已完成" },
]}
/>
</Form.Item>,
fieldProps?.invoiceCheckStatus?.col || 3
),
item(
"dates", //
99,
<Form.Item name={"dates"} label={t("group:ArrivalDate")} {...fieldProps.dates} initialValue={at(props, "initialValue.dates")[0]}>
{/* <DatePickerCharts isform={true} {...fieldProps.dates} form={form} /> */}
<RangePicker allowClear={true} inputReadOnly={true} presets={presets} placeholder={["From", "Thru"]} {...fieldComProps.dates} />
</Form.Item>,
fieldProps?.dates?.col || midCol
),
item(
"username", //
99,
<Form.Item name="username" label={t("account:username")} {...fieldProps.username}>
<Input placeholder={t("account:username")} allowClear />
</Form.Item>,
fieldProps?.username?.col || 4
),
/**
*
*/
item(
"year", //
99,
<Form.Item name={"year"} label={t("products:UseYear")} {...fieldProps.year} initialValue={at(props, "initialValue.year")[0]}>
<DatePicker picker="year" allowClear {...fieldComProps.year} />
</Form.Item>,
fieldProps?.year?.col || 3
),
item(
"agency", //
99,
<Form.Item name="agency" label={t("products:Vendor")} {...fieldProps.agency} initialValue={at(props, "initialValue.agency")[0]}>
<VendorSelector maxTagCount={1} {...fieldComProps.agency} />
</Form.Item>,
fieldProps?.agency?.col || 6
),
item(
"audit_state", // : /
99,
<Form.Item name={`audit_state`} initialValue={at(props, "initialValue.audit_state")[0] || { value: "", label: "Status" }}>
<AuditStateSelector {...fieldComProps.audit_state} />
</Form.Item>,
fieldProps?.audit_state?.col || 3
),
item(
"airticket_state", //-1 0 1
99,
<Form.Item name="airticket_state" label="出票状态" initialValue={at(props, "initialValue.airticket_state")[0] || { value: "-1", label: "所有" }}>
<Select
labelInValue
options={[
{ value: "-1", label: "所有" },
{ value: "0", label: "未出票" },
{ value: "1", label: "已出票" },
]}
/>
</Form.Item>,
fieldProps?.airticket_state?.col || 4
),
item(
"plan_state", //-1 0123
99,
<Form.Item name="plan_state" label="计划状态" initialValue={at(props, "initialValue.plan_state")[0] || { value: "-1", label: "所有" }}>
<Select
labelInValue
options={[
{ value: "-1", label: "所有" },
{ value: "0", label: "新计划" },
{ value: "1", label: "已确认" },
{ value: "2", label: "有变更" },
{ value: "3", label: "已取消" },
]}
/>
</Form.Item>,
fieldProps?.plan_state?.col || 4
),
item(
"products_types", //
99,
<Form.Item name={`products_types`} label={t("products:ProductType")} {...fieldProps.products_types} initialValue={at(props, "initialValue.products_types")[0] || undefined}>
<ProductsTypesSelector maxTagCount={1} {...fieldComProps.products_types} />
</Form.Item>,
fieldProps?.products_types?.col || 6
),
item(
"dept", //
99,
<Form.Item name={`dept`} label={t("products:Dept")} {...fieldProps.dept} initialValue={at(props, "initialValue.dept")[0] || undefined}>
<DeptSelector {...fieldComProps.dept} />
</Form.Item>,
fieldProps?.dept?.col || 6
),
item(
"city", //
99,
<Form.Item name={`city`} label={t("products:City")} {...fieldProps.city} initialValue={at(props, "initialValue.city")[0] || undefined}>
<CitySelector {...fieldComProps.city} />
</Form.Item>,
fieldProps?.city?.col || 4
),
item(
"unconfirmed", //
99,
<Form.Item name={`unconfirmed`} valuePropName="checked" initialValue={at(props, "initialValue.unconfirmed") || false}>
<Checkbox>{t("group:unconfirmed")}</Checkbox>
</Form.Item>,
fieldProps?.unconfirmed?.col || 2
),
];
baseChildren = baseChildren
.map(x => {
x.hide = false;
if (props.sort === undefined) {
return x;
}
const tmpSort = props.sort;
for (const key in tmpSort) {
if (Object.prototype.hasOwnProperty.call(tmpSort, key)) {
if (x.name === key) {
x.sort = tmpSort[key];
}
}
}
return x;
})
.map(x => {
if (props.hides.length === 0 && props.shows.length === 0) {
return x;
}
if (props.hides.length === 0) {
x.hide = !props.shows.includes(x.name);
} else if (props.shows.length === 0) {
x.hide = props.hides.includes(x.name);
}
return x;
})
.filter(x => !x.hide)
.sort((a, b) => {
return a.sort < b.sort ? -1 : 1;
});
const children = [];
const leftStyle = {}; // { borderRight: '1px solid #dedede' };
for (let i = 0; i < baseChildren.length; i++) {
let style = {}; // { padding: '0px 2px' };
style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style;
style = !baseChildren[i].hide ? { ...style, display: "block" } : { ...style, display: "none" };
const Item = (
<Col key={String(i)} style={style} {...layoutProps} {...baseChildren[i].col}>
{baseChildren[i].render}
</Col>
);
children.push(Item);
}
return children;
const { fieldProps, fieldComProps, form, presets, t } = props;
const bigCol = 4 * 2;
const midCol = 6;
const layoutProps = {
// gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 4 },
md: { span: 8 },
sm: { span: 12 },
xs: { span: 24 },
};
const item = (name, sort = 0, render, col) => {
const customCol = col || 4;
const mdCol = customCol * 2;
return {
'key': '',
sort,
name,
render,
'hide': false,
'col': { lg: { span: customCol }, md: { span: mdCol < 8 ? 10 : mdCol }, flex: mdCol < 8 ? '1 0' : '' },
};
};
let baseChildren = [];
baseChildren = [
item(
'keyword',
99,
<Form.Item name='keyword' {...fieldProps.keyword}>
<Input allowClear {...fieldComProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
'referenceNo',
99,
<Form.Item name='referenceNo' label={t('group:RefNo')} {...fieldProps.referenceNo}>
<Input placeholder={t('group:RefNo')} allowClear />
</Form.Item>,
fieldProps?.referenceNo?.col || 6
),
item(
'PNR',
99,
<Form.Item name='PNR' label='PNR'>
<Input placeholder={t('group:PNR')} allowClear />
</Form.Item>,
fieldProps?.PNR?.col || 4
),
item(
'invoiceStatus',
99,
<Form.Item name={`invoiceStatus`} initialValue={at(props, 'initialValue.invoiceStatus')[0] || { value: '0', label: 'Status' }}>
<Select
style={{ width: '100%' }}
labelInValue
options={[
{ value: '0', label: 'Status' },
{ value: '1', label: 'Not submitted' },
{ value: '2', label: 'Submitted' },
{ value: '3', label: 'Travel advisor approved' },
{ value: '4', label: 'Finance Dept arrproved' },
{ value: '5', label: 'Paid' },
]}
/>
</Form.Item>,
fieldProps?.invoiceStatus?.col || 3
),
item(
'dates',
99,
<Form.Item name={'dates'} label={t('group:ArrivalDate')} {...fieldProps.dates} initialValue={at(props, 'initialValue.dates')[0]}>
{/* <DatePickerCharts isform={true} {...fieldProps.dates} form={form} /> */}
<RangePicker allowClear={true} inputReadOnly={true} presets={presets} placeholder={['From', 'Thru']} {...fieldComProps.dates} />
</Form.Item>,
fieldProps?.dates?.col || midCol
),
item(
'username',
99,
<Form.Item name='username' label={t('account:username')} {...fieldProps.username}>
<Input placeholder={t('account:username')} allowClear />
</Form.Item>,
fieldProps?.username?.col || 4
),
item(
'realname',
99,
<Form.Item name='realname' label={t('account:realname')} {...fieldProps.realname}>
<Input placeholder={t('account:realname')} allowClear />
</Form.Item>,
fieldProps?.realname?.col || 4
),
/**
*
*/
item(
'year',
99,
<Form.Item name={'year'} label={t('products:UseYear')} {...fieldProps.year} initialValue={at(props, 'initialValue.year')[0]}>
<DatePicker picker='year' allowClear {...fieldComProps.year} />
</Form.Item>,
fieldProps?.year?.col || 3
),
item(
'agency',
99,
<Form.Item name='agency' label={t('products:Vendor')} {...fieldProps.agency} initialValue={at(props, 'initialValue.agency')[0]}>
<SearchInput
placeholder={t('products:Vendor')}
mode={'multiple'}
maxTagCount={0}
{...fieldComProps.agency}
fetchOptions={fetchVendorList}
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
/>
</Form.Item>,
fieldProps?.agency?.col || 6
),
item(
'audit_state',
99,
<Form.Item name={`audit_state`} initialValue={at(props, 'initialValue.audit_state')[0] || { value: '', label: 'Status' }}>
<AuditStateSelector {...fieldComProps.audit_state} />
</Form.Item>,
fieldProps?.audit_state?.col || 3
),
item(
'products_types',
99,
<Form.Item name={`products_types`} label={t('products:ProductType')} {...fieldProps.products_types} initialValue={at(props, 'initialValue.products_types')[0] || undefined}>
<SelectProductsTypes maxTagCount={1} {...fieldComProps.products_types} />
</Form.Item>,
fieldProps?.products_types?.col || 6
),
item(
'dept',
99,
<Form.Item name={`dept`} label={t('products:Dept')} {...fieldProps.dept} initialValue={at(props, 'initialValue.dept')[0] || undefined}>
<DeptSelector {...fieldComProps.dept} />
</Form.Item>,
fieldProps?.dept?.col || 6
),
];
baseChildren = baseChildren
.map((x) => {
x.hide = false;
if (props.sort === undefined) {
return x;
}
const tmpSort = props.sort;
for (const key in tmpSort) {
if (Object.prototype.hasOwnProperty.call(tmpSort, key)) {
if (x.name === key) {
x.sort = tmpSort[key];
}
}
}
return x;
})
.map((x) => {
if (props.hides.length === 0 && props.shows.length === 0) {
return x;
}
if (props.hides.length === 0) {
x.hide = !props.shows.includes(x.name);
} else if (props.shows.length === 0) {
x.hide = props.hides.includes(x.name);
}
return x;
})
.filter((x) => !x.hide)
.sort((a, b) => {
return a.sort < b.sort ? -1 : 1;
});
const children = [];
const leftStyle = {}; // { borderRight: '1px solid #dedede' };
for (let i = 0; i < baseChildren.length; i++) {
let style = {}; // { padding: '0px 2px' };
style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style;
style = !baseChildren[i].hide ? { ...style, display: 'block' } : { ...style, display: 'none' };
const Item = (
<Col key={String(i)} style={style} {...layoutProps} {...baseChildren[i].col}>
{baseChildren[i].render}
</Col>
);
children.push(Item);
}
return children;
}
export default SearchForm;

@ -3,24 +3,24 @@ import { Layout, Flex, theme, Spin, Divider } from 'antd';
import BackBtn from './BackBtn';
const { Content, Header } = Layout;
const HeaderWrapper = ({ children, header, loading, backTo, ...props }) => {
const HeaderWrapper = ({ children, header, loading, ...props }) => {
const navigate = useNavigate();
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<>
<Spin spinning={loading || false} wrapperClassName='h-full [&_.ant-spin-container]:h-full' >
<Layout className=' bg-white h-full'>
<Spin spinning={loading || false}>
<Layout className=' bg-white'>
<Header className='header px-6 h-10 ' style={{ background: 'white' }}>
<Flex justify={'space-between'} align={'center'} className='h-full'>
{/* {header} */}
<div className='grow h-full'>{header}</div>
{backTo!==false && <BackBtn to={backTo} />}
<BackBtn />
</Flex>
</Header>
<Divider className='my-2' />
<Content className='overflow-auto' style={{ backgroundColor: colorBgContainer }}>
<Content className='' style={{ backgroundColor: colorBgContainer }}>
{children || <Outlet />}
</Content>
</Layout>

@ -1,29 +0,0 @@
import { createContext, useEffect, useState } from 'react';
import {} from 'antd';
import SearchInput from './SearchInput';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { useTranslation } from 'react-i18next';
//
export const fetchVendorList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q });
return errcode !== 0 ? [] : result;
};
const VendorSelector = ({ ...props }) => {
const { t } = useTranslation();
return (
<>
<SearchInput
placeholder={t('products:Vendor')}
mode={'multiple'}
maxTagCount={0}
{...props}
fetchOptions={fetchVendorList}
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
/>
</>
);
};
export default VendorSelector;

@ -0,0 +1,51 @@
import React, { useState } from 'react';
import { DatePicker, Button } from 'antd';
const Date = ({ onDateChange }) => {
const dateFormat = 'YYYY/MM/DD';
const { RangePicker } = DatePicker;
const [dateRange, setDateRange] = useState(null);
const [selectedDays, setSelectedDays] = useState([]);
const days = [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
];
const handleChange = (date, dateString) => {
const range = dateString[0] + "-" + dateString[1];
setDateRange(range);
onDateChange({ dateRange: range, 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 (
<div>
<h4>Data</h4>
<RangePicker format={dateFormat} onChange={handleChange} />
<h4>Weekdays</h4>
<div>
{days.map((day, index) => (
<Button
key={index}
type={selectedDays.includes(day) ? 'primary' : 'default'}
onClick={() => handleDayClick(day)}
style={{ margin: '5px' }}
>
{day}
</Button>
))}
</div>
</div>
);
};
export default Date;

@ -1,22 +1,15 @@
export const PROJECT_NAME = "GHHub";
// mode: test内部测试使用
export const HT_HOST = import.meta.env.MODE === 'test' ? 'http://120.79.9.217:10024' : import.meta.env.PROD ? 'https://p9axztuwd7x8a7.mycht.cn' : 'http://202.103.68.144:890'
export const HT3_HOST = 'https://hub.globalhighlights.com/ht3.0'
import dayjs from "dayjs";
export const OVERSEA_HOST = 'https://ht20-p9axztuwd7x8a7.mycht.cn'
export const PROJECT_NAME = "GHHub";
export const HT_HOST = import.meta.env.PROD ? "https://p9axztuwd7x8a7.mycht.cn" :"http://202.103.68.144:890"; //"http://202.103.68.231:889";
export const DATE_FORMAT = "YYYY-MM-DD";
export const DATE_FORMAT_MONTH = "YYYY-MM";
export const SMALL_DATETIME_FORMAT = "YYYY-MM-DD 23:59";
export const OFFICEWEBVIEWERURL = "https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src=";
const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '')
const __BUILD_DATE__ = `__BUILD_DATE__`;
const __GIT_HEAD__ = `__GIT_HEAD__`
export const BUILD_VERSION = import.meta.env.PROD ? __BUILD_VERSION__ : import.meta.env.MODE;
export const BUILD_DATE = import.meta.env.PROD ? __BUILD_DATE__ : new Date().toLocaleString();
export const GIT_HEAD = import.meta.env.PROD ? __GIT_HEAD__ : 'current';
// 权限常量定义
// 账号、权限管理
@ -39,14 +32,9 @@ export const PERM_DOMESTIC = '/domestic/all'
// category: air-ticket
export const PERM_AIR_TICKET = '/air-ticket/all'
// 火车票供应商
// category: train-ticket
export const PERM_TRAIN_TICKET = '/train-ticket/all'
// 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_NEW = '/products/new'; // 新增产品
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 @deprecated
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'; // 价格.录入

@ -25,26 +25,10 @@ export const useDatePresets = () => {
label: t("datetime.lastMonth"),
value: [dayjs().subtract(1, "M").startOf("M"), dayjs().subtract(1, "M").endOf("M")],
},
{
label: t("datetime.nextMonth"),
value: [dayjs().add(1, "M").startOf("M"), dayjs().add(1, "M").endOf("M")],
},
{
label: t("datetime.lastThreeMonth"),
value: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
},
{
label: t("datetime.nextThreeMonth"),
value: [dayjs().startOf("M"), dayjs().add(3,"M").endOf("M")],
},
{
label: t("datetime.firstHalfYear"),
value: [dayjs().startOf("y"), dayjs().endOf("y").subtract(6, "M")],
},
{
label: t("datetime.latterHalfYear"),
value: [dayjs().startOf("y").add(6,"M"), dayjs().endOf("y")],
},
{
label: t("datetime.thisYear"),
value: [dayjs().startOf("y"), dayjs().endOf("y")],

@ -12,9 +12,3 @@ export const useHTLanguageSets = () => {
return newData;
};
export const useHTLanguageSetsMapVal = () => {
const stateSets = useHTLanguageSets();
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
return stateMapVal;
};

@ -1,139 +0,0 @@
import { flush, groupBy, isEmpty, isNotEmpty, unique, uniqWith } from '@/utils/commons';
import dayjs from 'dayjs';
// Shoulder Season 平季; peak season 旺季
const isFullYearOrLonger = (year, startDate, endDate) => {
// Parse the dates
const start = dayjs(startDate, 'YYYY-MM-DD');
const end = dayjs(endDate, 'YYYY-MM-DD');
// Create the start and end dates for the year
const yearStart = dayjs(`${year}-01-01`, 'YYYY-MM-DD');
const yearEnd = dayjs(`${year}-12-31`, 'YYYY-MM-DD');
// Check if start is '01-01' and end is '12-31' and the year matches
const isFullYear = start.isSame(yearStart, 'day') && end.isSame(yearEnd, 'day');
// Check if the range is longer than a year
const isLongerThanYear = end.diff(start, 'year') >= 1;
return isFullYear || isLongerThanYear;
};
const uniqueBySub = (arr) =>
arr.filter((subArr1, _, self) => {
return !self.some((subArr2) => {
if (subArr1 === subArr2) return false; // don't compare a subarray with itself
const set1 = new Set(subArr1);
const set2 = new Set(subArr2);
// check if subArr1 is a subset of subArr2
return [...set1].every((value) => set2.has(value));
});
});
export const chunkBy = (use_year, dataList = [], by = []) => {
const dataRollSS = dataList.map((rowp, ii) => {
const quotation = rowp.quotation.map((quoteItem) => {
return {
...quoteItem,
quote_season: isFullYearOrLonger(use_year, quoteItem.use_dates_start, quoteItem.use_dates_end) ? 'SS' : 'PS',
};
});
return { ...rowp, quotation };
});
// 人等分组只取平季, 因为产品只一行
const allQuotesSS = dataRollSS.reduce((acc, rowp) => acc.concat(rowp.quotation.filter((q) => q.quote_season === 'SS')), []);
const allQuotesPS = dataRollSS.reduce((acc, rowp) => acc.concat(rowp.quotation.filter((q) => q.quote_season === 'PS')), []);
const allQuotesSSS = isEmpty(allQuotesSS) ? allQuotesPS : allQuotesSS;
const PGroupSizeSS = allQuotesSSS.reduce((aq, cq) => {
aq[cq.WPI_SN] = aq[cq.WPI_SN] || [];
aq[cq.WPI_SN].push(cq.group_size_min);
aq[cq.WPI_SN] = unique(aq[cq.WPI_SN]);
aq[cq.WPI_SN] = aq[cq.WPI_SN].slice().sort((a, b) => a - b);
return aq;
}, {});
const maxGroupSize = Math.max(...allQuotesSSS.map((q) => q.group_size_max));
const maxSet = maxGroupSize === 1000 ? Infinity : maxGroupSize;
const _SSMinSet = uniqWith(Object.values(PGroupSizeSS), (a, b) => a.join(',') === b.join(','));
// const uSSsizeSetArr = (_SSMinSet)
const uSSsizeSetArr = uniqueBySub(_SSMinSet);
// * 若不重叠分组, 则上面不要 uniqueBySub
for (const key in PGroupSizeSS) {
if (Object.prototype.hasOwnProperty.call(PGroupSizeSS, key)) {
const element = PGroupSizeSS[key];
const findSet = uSSsizeSetArr.find((minCut) => element.every((v) => minCut.includes(v)));
PGroupSizeSS[key] = findSet;
}
}
const [SSsizeSets, PSsizeSets] = [uSSsizeSetArr, []].map((arr) => {
const _arr = structuredClone(arr);
const arrSets = _arr.map((keyMins) =>
keyMins.reduce((acc, curr, idx, minsArr) => {
const _max = idx === minsArr.length - 1 ? maxSet : Number(minsArr[idx + 1]) - 1;
acc.push([Number(curr), _max]);
return acc;
}, [])
);
return arrSets;
});
const compactSizeSets = {
SSsizeSetKey: uSSsizeSetArr.map((s) => s.join(',')).filter(isNotEmpty),
sizeSets: SSsizeSets,
};
const chunkSS = structuredClone(dataRollSS).map((rowp) => {
const pkey = (PGroupSizeSS[rowp.info.id] || []).join(',') || compactSizeSets.SSsizeSetKey[0]; // todo:
const thisRange = (PGroupSizeSS[rowp.info.id] || []).reduce((acc, curr, idx, minsArr) => {
const _max = idx === minsArr.length - 1 ? maxSet : Number(minsArr[idx + 1]) - 1;
acc.push([Number(curr), _max]);
return acc;
}, []);
const _quotation = rowp.quotation.map((quoteItem) => {
const ssSets = isEmpty(thisRange) ? SSsizeSets[0] : structuredClone(thisRange).reverse();
const matchRange = ssSets.find((ss) => quoteItem.group_size_min >= ss[0] && quoteItem.group_size_max <= ss[1]);
const findEnd = matchRange || ssSets.find((ss) => quoteItem.group_size_max > ss[0] && quoteItem.group_size_max <= ss[1] && ss[1] !== Infinity);
const findStart = findEnd || ssSets.find((ss) => quoteItem.group_size_min >= ss[0]);
const finalRange = findStart || ssSets[0];
quoteItem.quote_size = finalRange.join('-');
return quoteItem;
});
const quote_chunk_flat = groupBy(_quotation, (quoteItem2) => by.map((key) => quoteItem2[key]).join('@'));
const quote_chunk = Object.keys(quote_chunk_flat).reduce((qc, ckey) => {
const ckeyArr = ckey.split('@');
if (isEmpty(qc[ckeyArr[0]])) {
qc[ckeyArr[0]] = ckeyArr[1] ? { [ckeyArr[1]]: quote_chunk_flat[ckey] } : quote_chunk_flat[ckey];
} else {
qc[ckeyArr[0]][ckeyArr[1]] = (qc[ckeyArr[0]][ckeyArr[1]] || []).concat(quote_chunk_flat[ckey]);
}
return qc;
}, {});
return {
...rowp,
sizeSetsSS: pkey,
quotation: _quotation,
quote_chunk,
};
});
const allquotation = chunkSS.reduce((a, c) => a.concat(c.quotation), []);
// 取出两季相应的时效区间
const SSRange = unique((allquotation || []).filter((q) => q.quote_season === 'SS').map((qr) => `${qr.use_dates_start}~${qr.use_dates_end}`));
const PSRange = unique((allquotation || []).filter((q) => q.quote_season === 'PS').map((qr) => `${qr.use_dates_start}~${qr.use_dates_end}`));
return {
chunk: chunkSS,
dataSource: chunkSS,
SSRange,
PSRange,
...compactSizeSets,
};
};

@ -1,395 +0,0 @@
import { flush, groupBy, isEmpty, isNotEmpty, pick, unique, uniqWith } from '@/utils/commons';
import dayjs from 'dayjs';
import { formatGroupSize } from './useProductsSets';
// Shoulder Season 平季; peak season 旺季
export const isFullYearOrLonger = (year, startDate, endDate) => {
// Parse the dates
const start = dayjs(startDate, 'YYYY-MM-DD');
const end = dayjs(endDate, 'YYYY-MM-DD');
// Create the start and end dates for the year
const yearStart = dayjs(`${year}-01-01`, 'YYYY-MM-DD');
const yearEnd = dayjs(`${year}-12-31`, 'YYYY-MM-DD');
// Check if start is '01-01' and end is '12-31' and the year matches
const isFullYear = start.isSame(yearStart, 'day') && end.isSame(yearEnd, 'day');
// Check if the range is longer than a year
const isLongerThanYear = end.diff(startDate, 'year') >= 1;
const isLongerThan12M = end.diff(startDate, 'month') >= 11;
return isFullYear || isLongerThanYear || isLongerThan12M;
};
const uniqueBySub = (arr) => {
const sortedArr = arr.sort((a, b) => b.length - a.length);
const uniqueArr = [];
sortedArr.forEach((currentSubArr) => {
const isSubsetOfUnique = uniqueArr.some((uniqueSubArr) => {
return currentSubArr.every((item) => uniqueSubArr.includes(item));
});
if (!isSubsetOfUnique) {
uniqueArr.push(currentSubArr);
}
});
return uniqueArr;
}
export const chunkBy = (use_year, dataList = [], by = []) => {
const dataRollSS = dataList.map((rowp, ii) => {
const quotation = rowp.quotation.map((quoteItem) => {
return {
...quoteItem,
quote_season: isFullYearOrLonger(use_year, quoteItem.use_dates_start, quoteItem.use_dates_end) ? 'SS' : 'PS',
};
});
return { ...rowp, quotation };
});
// 人等分组只取平季, 因为产品只一行
const allQuotesSS = dataRollSS.reduce((acc, rowp) => acc.concat(rowp.quotation.filter((q) => q.quote_season === 'SS')), []);
const allQuotesPS = dataRollSS.reduce((acc, rowp) => acc.concat(rowp.quotation.filter((q) => q.quote_season === 'PS')), []);
const allQuotesSSS = isEmpty(allQuotesSS) ? allQuotesPS : allQuotesSS;
const allQuotesSSS2 = [].concat(allQuotesSS, allQuotesPS);
const PGroupSizeSS = allQuotesSSS.reduce((aq, cq) => {
aq[cq.WPI_SN] = aq[cq.WPI_SN] || [];
aq[cq.WPI_SN].push(`${cq.group_size_min}-${cq.group_size_max}`);
// aq[cq.WPI_SN].push([cq.group_size_min, cq.group_size_max]);
// aq[cq.WPI_SN].push(cq.group_size_min);
aq[cq.WPI_SN] = unique(aq[cq.WPI_SN]);
aq[cq.WPI_SN] = aq[cq.WPI_SN].slice().sort((a, b) => a.split('-')[0] - b.split('-')[0]);
return aq;
}, {});
// debug:
// PGroupSizeSS['5098'] = ['1-1000'];
// PGroupSizeSS['5099'] = ['1-2', '3-4'];
const PGroupSizePS = allQuotesPS.reduce((aq, cq) => {
aq[cq.WPI_SN] = aq[cq.WPI_SN] || [];
aq[cq.WPI_SN].push(`${cq.group_size_min}-${cq.group_size_max}`);
// aq[cq.WPI_SN].push([cq.group_size_min, cq.group_size_max]);
// aq[cq.WPI_SN].push(cq.group_size_min);
aq[cq.WPI_SN] = unique(aq[cq.WPI_SN]);
aq[cq.WPI_SN] = aq[cq.WPI_SN].slice().sort((a, b) => a.split('-')[0] - b.split('-')[0]);
return aq;
}, {});
// 补全产品旺季的人等分组 (当旺季和平季的人等不完全一致时)
const allWPI = unique(allQuotesSSS2.map((ele) => ele.WPI_SN));
for (const WPI of allWPI) {
// for (const WPI in PGroupSizeSS) {
// if (Object.prototype.hasOwnProperty.call(PGroupSizeSS, WPI)) {
const element = PGroupSizeSS[WPI] || [];
const elementP = PGroupSizePS[WPI] || [];
const diff = (elementP || []).filter((ele, index) => !element.includes(ele));
PGroupSizeSS[WPI] = element.concat(diff);
// }
}
// console.log('PGroupSizeSS', PGroupSizeSS, '\nPGroupSizePS', PGroupSizePS, '\nallQuotesSSS', allQuotesSSS2)
// const maxGroupSize = Math.max(...allQuotesSSS.map((q) => q.group_size_max));
// const maxSet = maxGroupSize === 1000 ? Infinity : maxGroupSize;
const _SSMinSet = uniqWith(Object.values(PGroupSizeSS), (a, b) => a.join(',') === b.join(','));
// const uSSsizeSetArr = (_SSMinSet)
const uSSsizeSetArr = uniqueBySub(_SSMinSet);
// console.log('_SSMinSet', _SSMinSet, '\n uSSsizeSetArr', uSSsizeSetArr)
// * 若不重叠分组, 则上面不要 uniqueBySub
for (const key in PGroupSizeSS) {
if (Object.prototype.hasOwnProperty.call(PGroupSizeSS, key)) {
const element = PGroupSizeSS[key];
const findSet = uSSsizeSetArr.find((minCut) => element.every((v) => minCut.includes(v)));
PGroupSizeSS[key] = findSet;
}
}
// console.log('PGroupSizeSS -- ', PGroupSizeSS)
const [SSsizeSets, PSsizeSets] = [uSSsizeSetArr, []].map((arr) => {
const _arr = structuredClone(arr);
const arrSets = _arr.map((keyMinMaxStrs) =>
keyMinMaxStrs.reduce((acc, curr, idx, minMaxArr) => {
const curArr = curr.split('-').map(val => parseInt(val, 10));
acc.push(curArr);
// const _max = idx === minsArr.length - 1 ? maxSet : Number(minsArr[idx + 1]) - 1;
// acc.push([Number(curr), _max]);
return acc;
}, [])
);
return arrSets;
});
// console.log('uSSsizeSetArr', uSSsizeSetArr);
const [SSsizeSetsMap, PSsizeSetsMap] = [uSSsizeSetArr, []].map((arr) => {
const _arr = structuredClone(arr);
const SetsMap = _arr.reduce((acc, keyMinMaxStrs, ii, strArr) => {
const _key = keyMinMaxStrs.join(',');
// console.log(_key);
const _value = keyMinMaxStrs.reduce((acc, curr, idx, minMaxArr) => {
const curArr = curr.split('-').map((val) => parseInt(val, 10));
acc.push(curArr);
return acc;
}, []);
return { ...acc, [_key]: _value };
}, {});
return SetsMap;
});
// console.log('SSsizeSetsMap', SSsizeSetsMap);
const compactSizeSets = {
SSsizeSetKey: uSSsizeSetArr.map((s) => s.join(',')).filter(isNotEmpty),
sizeSets: SSsizeSets,
SSsizeSetsMap,
};
// console.log('sizeSets -- ', SSsizeSets, '\nSSsizeSetKey', compactSizeSets.SSsizeSetKey, '\nSSsizeSetsMap', SSsizeSetsMap)
const chunkSS = structuredClone(dataRollSS).map((rowp) => {
const pkey = (PGroupSizeSS[rowp.info.id] || []).join(',') || compactSizeSets.SSsizeSetKey[0]; // todo:
let unitCnt = { '0': 0, '1': 0 }; // ? todo: 以平季的为准
const _quotation = rowp.quotation.map((quoteItem) => {
unitCnt[quoteItem.unit_id]++;
quoteItem.quote_size = pkey;
quoteItem.quote_col_key = formatGroupSize(quoteItem.group_size_min, quoteItem.group_size_max);
quoteItem.use_dates_start = quoteItem.use_dates_start.replace(/-/g, '.');
quoteItem.use_dates_end = quoteItem.use_dates_end.replace(/-/g, '.');
return quoteItem;
});
const quote_chunk_flat = groupBy(_quotation, (quoteItem2) => by.map((key) => quoteItem2[key]).join('@') || '#');
const quote_chunk = Object.keys(quote_chunk_flat).reduce((qc, ckey) => {
const ckeyArr = ckey.split('@');
if (isEmpty(qc[ckeyArr[0]])) {
qc[ckeyArr[0]] = ckeyArr[1] ? { [ckeyArr[1]]: quote_chunk_flat[ckey] } : quote_chunk_flat[ckey];
} else {
qc[ckeyArr[0]][ckeyArr[1]] = (qc[ckeyArr[0]][ckeyArr[1]] || []).concat(quote_chunk_flat[ckey]);
}
return qc;
}, {});
const _quotationTransposeBySize = Object.keys(quote_chunk).reduce((accBy, byKey) => {
const byValues = quote_chunk[byKey];
const groupTablesBySize = groupBy(byValues, 'quote_size');
const transposeTables = Object.keys(groupTablesBySize).reduce((accBy, sizeKeys) => {
const _sizeRows = groupTablesBySize[sizeKeys];
const rowsByDate = groupBy(_sizeRows, qi => `${qi.use_dates_start}~${qi.use_dates_end}`);
const _rowsFromDate = Object.keys(rowsByDate).reduce((accDate, dateKeys) => {
const _dateRows = rowsByDate[dateKeys];
const rowKey = _dateRows.map(e => e.id).join(',');
const keepCol = pick(_dateRows[0], ['WPI_SN', 'WPP_VEI_SN', 'currency', 'unit_id', 'unit_name', 'use_dates_start', 'use_dates_end', 'weekdays', 'quote_season']);
const _colFromDateRow = _dateRows.reduce((accCols, rowp) => {
// const _colRow = pick(rowp, ['currency', 'unit_id', 'unit_name', 'use_dates_start', 'use_dates_end', 'weekdays', 'child_cost', 'adult_cost']);
return { ...accCols, [rowp.quote_col_key]: rowp };
}, {...keepCol, originRows: _dateRows, rowKey });
accDate.push(_colFromDateRow);
return accDate;
}, []);
return { ...accBy, [sizeKeys]: _rowsFromDate };
}, {});
return { ...accBy, [byKey]: transposeTables };
}, {});
// console.log(_quotationTransposeBySize);
return {
...rowp,
unitCnt,
unitSet: Object.keys(unitCnt).reduce((a, b) => unitCnt[a] > unitCnt[b] ? a : b),
sizeSetsSS: pkey,
_quotationTransposeBySize,
quotation: _quotation,
quote_chunk,
};
});
const allquotation = chunkSS.reduce((a, c) => a.concat(c.quotation), []);
// 取出两季相应的时效区间
const SSRange = unique((allquotation || []).filter((q) => q.quote_season === 'SS').map((qr) => `${qr.use_dates_start}~${qr.use_dates_end}`));
const PSRange = unique((allquotation || []).filter((q) => q.quote_season === 'PS').map((qr) => `${qr.use_dates_start}~${qr.use_dates_end}`));
// const transposeDataSS = chunkSS
return {
chunk: chunkSS,
// dataSource: chunkSS,
SSRange,
PSRange,
...compactSizeSets, // { SSsizeSetKey, sizeSets }
};
};
/**
* [单位, 人等]拆分表格
* @use D J B R 8
*/
export const splitTable_SizeSets = (chunkData) => {
const { SSRange, PSRange, SSsizeSetKey, SSsizeSetsMap, chunk } = chunkData;
// console.log('---- chunk', chunk);
const bySizeUnitSetKey = groupBy(chunk, pitem => ['unitSet', 'sizeSetsSS', ].map((key) => pitem[key]).join('@'));
// agencyProducts.J.
// console.log('bySizeSetKey', bySizeUnitSetKey);
const tables = Object.keys(bySizeUnitSetKey).map((sizeSetsUnitStr) => {
const [unitSet, sizeSetsStr] = sizeSetsUnitStr.split('@');
const _thisSSsetProducts = bySizeUnitSetKey[sizeSetsUnitStr];
const _subTable = _thisSSsetProducts.map(({ info, sizeSetsSS, _quotationTransposeBySize, unitSet, ...pitem }) => {
const transpose = _quotationTransposeBySize['#'][sizeSetsSS];
const _pRow = transpose.map((quote, qi) => ({ ...quote, rowSpan: qi === 0 ? transpose.length : 0 }));
return { info, sizeSetsSS, unitSet, rows: _pRow, transpose };
});
return { cols: SSsizeSetsMap[sizeSetsStr], colsKey: sizeSetsStr, unitSet, sizeSetsUnitStr, data: _subTable };
});
// console.log('---- tables', tables);
const tablesQuote = tables.map(({ cols, colsKey, unitSet, sizeSetsUnitStr, data }, ti) => {
const _table = data.reduce((acc, prow) => {
const prows = prow.rows.map((_q) => ({ ..._q, info: prow.info, dateText: `${_q.use_dates_start}~${_q.use_dates_end}` }));
return acc.concat(prows);
}, []);
return { cols, colsKey: sizeSetsUnitStr, data: _table }; // `${unitSet}@${colsKey}`
});
// console.log('---- tablesQuote', tablesQuote);
return tablesQuote;
};
/**
* 按季度分列 [平季, 旺季]
* @use Q 7 6
*/
export const splitTable_Season = (chunkData) => {
const { SSRange, PSRange, SSsizeSetKey, SSsizeSetsMap, chunk } = chunkData;
// console.log(chunkData);
const tablesQuote = chunk.map((pitem) => {
const { quote_chunk } = pitem;
// const bySeason = groupBy(pitem.quotation, (ele) => ele.quote_season);
const rowSeason = Object.keys(quote_chunk).reduce((accp, _s) => {
const bySeasonValue = groupBy(quote_chunk[_s], (ele) => ['adult_cost', 'child_cost', 'group_size_min', 'group_size_max', 'unit_id'].map((k) => ele[k]).join('@'));
// console.log('---- bySeasonValue', _s, bySeasonValue);
const byDate = groupBy(quote_chunk[_s], (ele) => `${ele.use_dates_start}~${ele.use_dates_end}`);
// console.log('---- byDate', _s, byDate);
const subHeader = Object.keys(bySeasonValue).length >= Object.keys(byDate).length ? 'dates' : 'priceValues';
// console.log('---- subHeader', _s, subHeader);
let valuesArr = [];
switch (subHeader) {
case 'priceValues':
valuesArr = Object.keys(bySeasonValue).reduce((accv, valKey) => {
const valRows = bySeasonValue[valKey];
const valRow = pick(valRows[0], ['adult_cost', 'child_cost', 'currency', 'unit_id', 'unit_name', 'group_size_min', 'group_size_max']);
// valRow.dates = valRows.map((v) => pick(v, ['id', 'use_dates_end', 'use_dates_start']));
valRow.rows = [valRows[0]];
valRow.originRows = valRows;
valRow.rowKey = valRows.map((v) => v.id).join(',');
valRow.headerDates = valRows.map((v) => pick(v, ['use_dates_end', 'use_dates_start']));
accv.push(valRow);
return accv;
}, []);
break;
case 'dates':
valuesArr = Object.keys(byDate).reduce((accv, dateKey) => {
const valRows = byDate[dateKey];
const valRow = pick(valRows[0], ['use_dates_end', 'use_dates_start']);
valRow.rows = valRows;
valRow.originRows = valRows;
valRow.rowKey = valRows.map((v) => v.id).join(',');
valRow.headerDates = [pick(valRows[0], ['use_dates_end', 'use_dates_start'])];
accv.push(valRow);
return accv;
}, []);
break;
default:
break;
}
const valUnderSeason = Object.keys(bySeasonValue).reduce((accv, valKey) => {
const valRows = bySeasonValue[valKey];
const valRow = pick(valRows[0], ['adult_cost', 'child_cost', 'currency', 'unit_id', 'unit_name', 'group_size_min', 'group_size_max']);
// valRow.dates = valRows.map((v) => pick(v, ['id', 'use_dates_end', 'use_dates_start']));
valRow.rows = valRows;
valRow.rowKey = valRows.map(v => v.id).join(',');
accv.push(valRow);
return accv;
}, []);
return { ...accp, [_s]: valUnderSeason, [_s + 'Data']: valuesArr };
}, {});
return { info: pitem.info, ...rowSeason, rowKey: pitem.info.id };
});
// console.log('---- tablesQuote', tablesQuote);
return tablesQuote;
};
export const splitTable_D = (use_year, dataSource, retTableOnly = true) => {
const chunked = chunkBy(use_year, dataSource);
// console.log(chunked);
const tables = addCityRow4Split(splitTable_SizeSets(chunked));
return retTableOnly ? tables : { ...chunked, tables };
};
export const splitTable_J = (use_year, dataSource, retTableOnly = true) => {
const chunked = chunkBy(use_year, dataSource);
// console.log(chunked);
const tables = addCityRow4Split(splitTable_SizeSets(chunked));
return retTableOnly ? tables : { ...chunked, tables };
};
export const splitTable_Q = (use_year, dataSource) => {
const chunked = chunkBy(use_year, dataSource, ['quote_season']);
return addCityRow4Season(splitTable_Season(chunked));
};
export const splitTable_7 = (use_year, dataSource) => {
const chunked = chunkBy(use_year, dataSource, ['quote_season']);
return addCityRow4Season(splitTable_Season(chunked));
};
export const splitTable_R = (use_year, dataSource, retTableOnly = true) => {
const chunked = chunkBy(use_year, dataSource);
// console.log(chunked);
const tables = addCityRow4Split(splitTable_SizeSets(chunked));
return retTableOnly ? tables : { ...chunked, tables };
};
export const splitTable_8 = (use_year, dataSource, retTableOnly = true) => {
const chunked = chunkBy(use_year, dataSource);
// console.log(chunked);
const tables = addCityRow4Split(splitTable_SizeSets(chunked));
return retTableOnly ? tables : { ...chunked, tables };
};
export const splitTable_6 = (use_year, dataSource, retTableOnly = true) => {
const chunked = chunkBy(use_year, dataSource, ['quote_season']);
const tables = splitTable_Season(chunked);
return retTableOnly ? tables : { ...chunked, tables };
};
export const splitTable_B = (use_year, dataSource, retTableOnly = true) => {
const chunked = chunkBy(use_year, dataSource);
// console.log(chunked);
const tables = addCityRow4Split(splitTable_SizeSets(chunked));
return retTableOnly ? tables : { ...chunked, tables };
};
export const addCityRow4Season = (table) => {
const byCity = groupBy(table, (ele) => `${ele.info.city_id}@${ele.info.city_name}`);
const withCityRow = Object.keys(byCity).reduce((acc, cityIdName) => {
const [cityId, cityName] = cityIdName.split('@');
acc.push({ info: { product_title: cityName, isCityRow: true,}, use_dates_end: '', use_dates_start: '', quote_season: 'SS', rowSpan: 1, rowKey: `c_${cityId}` });
return acc.concat(byCity[cityIdName]);
}, []);
return withCityRow;
};
export const addCityRow4Split = (splitTables) => {
const tables = splitTables.map(table => {
return { ...table, data: addCityRow4Season(table.data)}
});
return tables;
};

@ -2,7 +2,6 @@ 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';
import { isEmpty } from '@/utils/commons';
/**
* 产品管理 相关的预设数据
@ -53,7 +52,7 @@ export const useProductsTypes = (showAll = false) => {
{ 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' }, // landscape
{ 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' },
@ -70,19 +69,16 @@ export const useProductsTypesMapVal = (value) => {
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: '-1', value: '-1', label: t('products:auditState.New'), color: 'gray-500' },
{ 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: '3', value: '3', label: t('products:auditState.Rejected'), color: 'red-500' },
{ key: '1', value: '1', label: t('products:auditState.Published'), color: 'primary' },
// ELSE 未知
];
@ -92,123 +88,35 @@ export const useProductsAuditStates = () => {
return types;
};
export const useProductsAuditStatesMapVal = () => {
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 = [['city', 'city_list'], ['title']];
const infoAdmin = ['title', 'product_title', 'code', 'remarks', 'dept']; // 'display_to_c'
const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : [];
const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : [];
const subTypeD = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sub_type_D'] : [];
const sortOrder = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sort_order'] : [];
const infoDefault = [['code'], ['title']];
const infoAdmin = ['remarks', 'dept', 'display_to_c'];
const infoTypesMap = {
'6': [[...infoDisplay], []],
'B': [['km', ...infoDisplay], []],
'J': [[...infoRecDisplay, 'duration', ...infoDisplay, ...sortOrder], ['description']],
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay, ...sortOrder], ['description']],
'D': [[...infoRecDisplay, 'duration', ...infoDisplay, ...subTypeD, ...sortOrder], ['description']],
'7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']],
'R': [[...infoDisplay], ['description']],
'8': [[...infoDisplay], []],
'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) => {
if (isEmpty(_type)) {
return infoDefault;
}
const adminSet = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? infoAdmin : [];
return [
[...infoDefault[0], ...infoTypesMap[_type][0], ...adminSet],
[...infoDefault[1], ...infoTypesMap[_type][1]],
[...infoDefault[1], ...infoTypesMap[_type][1]]
];
};
return thisTypeFieldset(type);
};
export const useNewProductRecord = () => {
return {
info: {
'id': '',
'htid': 0,
'title': '',
'code': '',
'product_type_id': '',
'product_type_name': '',
'remarks': '',
'duration': 0,
'duration_unit': 'h',
'open_weekdays': ['1', '2', '3', '4', '5', '6', '7'],
'recommends_rate': 0,
'dept_id': 0,
'dept_name': '',
'display_to_c': '0',
'km': 0,
'city_id': 0,
'city_name': '',
'open_hours': '',
'lastedit_changed': '',
'create_date': '',
'created_by': '',
'edit_status': 2, // 信息的审核状态 1已发布2已审核
'sort_order': '',
'sub_type_D': '', // 包价类型, 值保存在`item_type`字段中
'item_type': '', // 产品子类型的值
'city_list': [],
},
lgc_details: [
{
'title': '',
'descriptions': '',
'lgc': 1,
'id': '',
'edit_status': 2,
},
],
quotation: [
{
'id': '',
'adult_cost': 0,
'child_cost': 0,
'currency': 'RMB',
'unit_id': '1',
'unit_name': '每团',
'group_size_min': 1,
'group_size_max': 2,
'use_dates_start': '',
'use_dates_end': '',
'weekdays': '',
'audit_state_id': -1,
'audit_state_name': '',
'lastedit_changed': 'new',
},
],
};
};
export const PackageTypes = [
{ key: '35001', value: '35001', label: '飞机接送' },
{ key: '35002', value: '35002', label: '车站接送' },
{ key: '35003', value: '35003', label: '码头接送' },
{ key: '35004', value: '35004', label: '一天游' },
{ key: '35005', value: '35005', label: '半天游' },
{ key: '35006', value: '35006', label: '夜间活动' },
{ key: '35007', value: '35007', label: '大车游' },
{ key: '35008', value: '35008', label: '单车单导' },
{ key: '35009', value: '35009', label: '单租车' },
{ key: '35010', value: '35010', label: '单导游' },
{ key: '35011', value: '35011', label: '火车站接送' },
{ key: '35012', value: '35012', label: '门票预定' },
{ key: '35013', value: '35013', label: '车导费' },
{ key: '35014', value: '35014', label: '其它(餐补等)' },
];
export const formatGroupSize = (min, max, suffix = false) => {
return max === 1000 ? min <= 1 ? '不分人等' : `${min}人以上` : (`${min}-${max}`+(suffix ? '人' : ''));
};
}

@ -17,17 +17,6 @@ export function usingStorage() {
}
}
const getValue = (key) => {
if (window.localStorage) {
return window.localStorage.getItem(key)
} else if (window.sessionStorage) {
return window.sessionStorage.getItem(key)
} else {
console.error('browser not support localStorage and sessionStorage.')
return ''
}
}
const setProperty = (key, value) => {
const webStorage = getStorage()
const typeAndKey = key.split(':')
@ -86,7 +75,6 @@ export function usingStorage() {
return {
...persistObject,
getValue,
setStorage: (key, value) => {
setProperty(key, value)
},

@ -1,145 +1,106 @@
import { createRoot } 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 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 InvoiceHistory from '@/views/invoice/History'
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 AirticketInvoice from '@/views/airticket/Invoice'
import AirticketInvoicePaid from '@/views/airticket/InvoicePaid'
import Trainticket from '@/views/trainticket/index'
import TrainticketPlan from '@/views/trainticket/plan'
import TrainticketInvoice from '@/views/trainticket/invoice'
import TrainticketInvoicePaid from '@/views/trainticket/invoicePaid'
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 { notifyAuth } from "./utils/lifecycle"
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import ImageViewer from '@/views/ImageViewer';
import CustomerImageViewer from '@/views/CustomerImageViewer';
import PickYear from './views/products/PickYear'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
import './i18n';
import './i18n'
const initRouter = async () => {
return createBrowserRouter([
{
path: '/',
element: <App />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <NoticeIndex /> },
{ path: 'account/change-password', element: <ChangePassword />},
{ path: 'account/profile', element: <AccountProfile />},
{ path: 'account/management', element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
{ path: 'account/role-list', element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
//
{ path: 'reservation/newest', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
{ path: 'reservation/:reservationId', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
//
{ path: 'feedback', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
//
{ path: 'report', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
//
{ path: 'notice', element: <NoticeIndex />},
{ path: 'notice/:CCP_BLID', element: <NoticeDetail />},
//
{ path: 'invoice',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
{ path: 'invoice/detail/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
{ path: 'invoice/history/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceHistory /></RequireAuth>},
{ path: 'invoice/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
{ path: 'airticket',element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
{ path: 'airticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
{ path: 'airticket/invoice',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoice /></RequireAuth>},
{ path: 'airticket/invoicepaid',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoicePaid /></RequireAuth>},
//
{ path: 'trainticket',element: <RequireAuth subject={PERM_TRAIN_TICKET} result={true}><Trainticket /></RequireAuth>},
{ path: 'trainticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketPlan /></RequireAuth>},
{ path: 'trainticket/invoice',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoice /></RequireAuth>},
{ path: 'trainticket/invoicepaid',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoicePaid /></RequireAuth>},
//
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/audit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/pick-year",element: <RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><PickYear /></RequireAuth>},
//
]
},
{
element: <Standlone />,
children: [
{ path: '/login', element: <Login /> },
{ path: '/logout', element: <Logout /> },
{ path: '/image-viewer/:GRI_SN/:GRI_No', element: <ImageViewer /> },
{ path: '/customer-image/:key', element: <CustomerImageViewer /> },
]
}
])
}
const initAppliction = async () => {
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await useAuthStore.getState().initAuth()
await notifyAuth()
}
}
const router = await initRouter()
const root = document.getElementById('root')
await initAppliction()
if (!root) throw new Error('No root element found')
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <NoticeIndex /> },
{ path: "account/change-password", element: <ChangePassword />},
{ path: "account/profile", element: <AccountProfile />},
{ path: "account/management", element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
{ path: "account/role-list", element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
{ path: "reservation/newest", element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
{ path: "reservation/:reservationId", element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
{ path: "feedback", element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
{ path: "feedback/:GRI_SN/:CII_SN/:RefNo", element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
{ path: "feedback/:GRI_SN/:RefNo", element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
{ path: "report", element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
{ path: "notice", element: <NoticeIndex />},
{ path: "notice/:CCP_BLID", element: <NoticeDetail />},
{ path: "invoice",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
{ path: "invoice/detail/:GMDSN/:GSN",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
{ path: "invoice/paid",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
{ path: "invoice/paid/detail/:flid", element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
{ path: "airticket",element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
{ path: "airticket/plan/:coli_sn",element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<ProductsDetail />},
{ path: "products/edit",element:<ProductsDetail />},
]
},
{
element: <Standlone />,
children: [
{ path: "/login", element: <Login /> },
{ path: "/logout", element: <Logout /> },
]
}
])
createRoot(root).render(
//<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider
future={{
v7_startTransition: true,
}}
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</ThemeContext.Provider>
//</React.StrictMode>
)
}
initAppliction()
ReactDOM.createRoot(document.getElementById("root")).render(
//<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</ThemeContext.Provider>
//</React.StrictMode>
)

@ -1,93 +1,24 @@
import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } from '@/config';
import { fetchJSON } from '@/utils/request'
import { usingStorage } from "@/hooks/usingStorage";
export const sendNotify = async (message) => {
const { userId, travelAgencyId } = usingStorage();
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup';
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: `${message}\\n\\nID: ${userId}, ${travelAgencyId} | ${PROJECT_NAME} (${BUILD_VERSION})`,
};
return fetchJSON(notifyUrl, params).then((json) => {
if (json.errcode === 0) {
console.info('发送通知成功');
} else {
throw new Error(json?.errmsg + ': ' + json.errcode);
}
});
};
import { PROJECT_NAME } from '@/config';
export const loadPageSpy = (title) => {
if (import.meta.env.DEV || window.$pageSpy) return
const PageSpyConfig = { api: 'page-spy.mycht.cn', project: PROJECT_NAME, title: title, autoRender: false };
const PageSpySrc = [
'https://page-spy.mycht.cn/page-spy/index.min.js'+`?${BUILD_VERSION}`,
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js'+`?${BUILD_VERSION}`,
'https://page-spy.mycht.cn/plugin/rrweb/index.min.js'+`?${BUILD_VERSION}`,
'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(() => {
//
window.$harbor = new DataHarborPlugin();
window.$rrweb = new RRWebPlugin();
[window.$harbor, window.$rrweb].forEach(p => {
PageSpy.registerPlugin(p)
})
window.$pageSpy = new PageSpy(PageSpyConfig);
window.onerror = async function (msg, url, lineNo, columnNo, error) {
// iframe rrweb Cannot set property attributeName of #<MutationRecord> which has only a getter
//
if (url && url.indexOf('https://page-spy.mycht.cn/plugin/rrweb/index.min.js') > -1) {
console.info('ignore rrweb error')
} else {
// 3
const now = Date.now()
await window.$harbor.uploadPeriods({
startTime: now - 3 * 60000,
endTime: now,
remark: `\`onerror\`自动上传. ${msg}`,
})
}
}
PageSpy.registerPlugin(new DataHarborPlugin({ maximum: 2 * 1024 * 1024 }));
// PageSpy
window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: PROJECT_NAME, title: title, autoRender: false });
});
};
export const uploadPageSpyLog = async () => {
if (import.meta.env.DEV) return true;
if (window.$pageSpy) {
try {
// await window.$harbor.upload() // { clearCache: true, remark: '' }
// 1 , upload : 413 Payload Too Large
const now = Date.now();
await window.$harbor.uploadPeriods({
startTime: now - 60 * 60000,
endTime: now,
});
return true;
} catch (error) {
return false;
}
} else {
return false;
}
export const uploadPageSpyLog = () => {
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
}
/**
* @deprecated
* @outdated
*/
export const PageSpyLog = () => {
return (
<>

@ -1,7 +1,6 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons'
import { isEmpty } from '@/utils/commons'
import { HT_HOST } from "@/config"
import { usingStorage } from '@/hooks/usingStorage'
@ -68,21 +67,7 @@ export const fetchTravelAgencyByName = async (name) => {
return errcode !== 0 ? {} : result
}
export const genRandomPassword = () => {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
const charactersLength = characters.length
for (let i = 0; i < 6; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength))
}
result += '@' + (Math.floor(Math.random() * 900) + 100)
return result
}
const useAccountStore = create(devtools((set) => ({
const useAccountStore = create((set, get) => ({
accountList: [],
@ -106,11 +91,13 @@ const useAccountStore = create(devtools((set) => ({
return postAccountPassword(formData)
},
newEmptyRole: () => ({
role_id: null,
role_name: '',
role_ids: ''
}),
newEmptyRole: () => {
return {
role_id: null,
role_name: '',
role_ids: ''
}
},
newEmptyAccount: () => {
return {
@ -152,18 +139,16 @@ const useAccountStore = create(devtools((set) => ({
},
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,
travel_agency_ids: travel_agency_ids,
realname: formValues.realname,
lgc: 2
}
const resultArray = await fetchAccountList(searchParams)
console.info(resultArray)
const mapAccoutList = resultArray.map((r) => {
return {
accountId: r.wu_id,
@ -186,6 +171,6 @@ const useAccountStore = create(devtools((set) => ({
accountList: mapAccoutList
}))
},
}), { name: 'accountStore' }))
}))
export default useAccountStore

@ -1,5 +1,5 @@
import { create } from "zustand";
import { fetchJSON, postForm } from "@/utils/request";
import { fetchJSON } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST, DATE_FORMAT } from "@/config";
import dayjs from "dayjs";
@ -10,20 +10,15 @@ const airTicketStore = create((set, get) => ({
setPlanList: planList => set({ planList }),
setPlanDetail: planDetail => set({ planDetail }),
setGuestList: guestList => set({ guestList }),
setVEIFlightBill: vEIFlightBill => set({ vEIFlightBill }),
setVeiPlanChangeTxt: veiPlanChangeTxt => set({ veiPlanChangeTxt }),
setAirPortList: airPortList => set({ airPortList }),
async getPlanList(vei_sn, GRI_Name, TimeStart, TimeEnd, plan_state, airticket_state) {
async getPlanList(vei_sn, GRI_Name, TimeStart, TimeEnd) {
const { setLoading, setPlanList } = get();
setLoading(true);
const searchParams = {
vei_sn: vei_sn,
vei_sn: 4272, //vei_sn,
FlightDate1: TimeStart,
FlightDate2: TimeEnd,
GRI_Name: GRI_Name,
FlightStatus: plan_state,
TicketIssued: airticket_state,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightPlan`, searchParams);
@ -35,192 +30,23 @@ const airTicketStore = create((set, get) => ({
async getPlanDetail(vei_sn, gri_sn) {
const { setPlanDetail } = get();
const searchParams = {
vei_sn: vei_sn,
gri_sn: gri_sn,
vei_sn: 6376, //vei_sn,
gri_sn: 369040, //gri_sn
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightPlanDetail`, searchParams);
const _result = errcode !== 0 ? [] : result;
setPlanDetail(_result);
//return _result.filter(item => isNotEmpty(item.GRI_No));
return "dsadsd";
},
async postFlightDetail(CLF_SN, GRI_SN, VEI_SN, original_values, info_object) {
const formData = new FormData();
formData.append("CLF_SN", CLF_SN ? CLF_SN : "");
formData.append("GRI_SN", GRI_SN);
formData.append("VEI_SN", VEI_SN);
for (const [key, value] of Object.entries(original_values)) {
formData.append(key, value); //先用原始数据填充一遍,确保复制了全部数据到新表
}
for (const [key, value] of Object.entries(info_object)) {
formData.set(key, value); //再用新值覆盖
}
formData.set("StartDate", dayjs(info_object.StartDate).format(DATE_FORMAT)); //再用新值覆盖
//是否出票的值true、false变为1或0
formData.set("TicketIssued", info_object.TicketIssued ? 1 : 0);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_info";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//删除航班信息
async delete_flight_info(CLF_SN) {
const searchParams = {
CLF_SN: CLF_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_info`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
async getGuestList(coli_sn) {
const { setGuestList } = get();
const searchParams = {
COLI_SN: coli_sn,
COLI_SN: 1097829, //coli_sn,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightGuestInfo`, searchParams);
const _result = errcode !== 0 ? [] : result;
setGuestList(_result);
},
//获取账单列表
async getVEIFlightBill(VEI_SN, GRI_Name, CheckStatus, FlightDate1, FlightDate2) {
const { setLoading, setVEIFlightBill } = get();
setLoading(true);
const searchParams = {
VEI_SN: VEI_SN,
GRI_Name: GRI_Name,
CheckStatus: CheckStatus,
FlightDate1: FlightDate1,
FlightDate2: FlightDate2,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetVEIFlightBill`, searchParams);
const _result = errcode !== 0 ? [] : result;
setVEIFlightBill(_result);
setLoading(false);
},
//保存费用
async postFlightCost(values) {
const formData = new FormData();
for (const [key, value] of Object.entries(values)) {
formData.append(key, value); //先用原始数据填充一遍,确保复制了全部数据到新表
}
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_cost";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//删除费用
async deleteFlightCost(CLC_SN) {
const searchParams = {
CLC_SN: CLC_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_cost`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
//获取变更信息
async getVeiPlanChange(VEI_SN, GRI_SN) {
const { setVeiPlanChangeTxt } = get();
const searchParams = {
VEI_SN: VEI_SN,
GRI_SN: GRI_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetVeiFlightPlanChange`, searchParams);
const _result = errcode !== 0 ? [] : result;
setVeiPlanChangeTxt(_result);
},
//提交变更确认
async postVeiFlightPlanConfirm(VEI_SN, GRI_SN, LMI_SN, ConfirmInfo) {
const formData = new FormData();
formData.append("VEI_SN", VEI_SN);
formData.append("GRI_SN", GRI_SN);
formData.append("LMI_SN", LMI_SN);
formData.append("ConfirmInfo", ConfirmInfo);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/VeiFlightPlanConfirm";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//提交账单
async postVEIFlightBillSubmit(VEI_SN, values) {
const formData = new FormData();
formData.append("vei_sn", VEI_SN);
formData.append("billdata", JSON.stringify(values));
const postUrl = HT_HOST + "/Service_BaseInfoWeb/VEIFlightBillSubmit";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//通知顾问查看机票信息
async ticketIssuedNotifications(LMI_SN, CLF_SN, OPI_SN, FlightMemo_messages) {
const searchParams = {
CLF_SN: CLF_SN,
OPI_SN: OPI_SN,
LMI_SN: LMI_SN,
FlightMemo_messages: FlightMemo_messages,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/TicketIssuedNotifications`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
//获取机场列表
async getAirPortList() {
const { setAirPortList } = get();
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetAirPortInfo`);
const _result = errcode !== 0 ? [] : result;
setAirPortList(_result);
},
airLineList: [
{ label: "CA-国航", value: "国航" },
{ label: "MU-东方航空", value: "东方航空" },
{ label: "FM-上海航空", value: "上海航空" },
{ label: "CZ-南方航空", value: "南方航空" },
{ label: "HO-吉祥航空", value: "吉祥航空" },
{ label: "HU-海南航空", value: "海南航空" },
{ label: "ZH-深圳航空", value: "深圳航空" },
{ label: "MF-厦门航空", value: "厦门航空" },
{ label: "3U-四川航空", value: "四川航空" },
{ label: "SC-山东航空", value: "山东航空" },
{ label: "JD-首都航空", value: "首都航空" },
{ label: "BK-奥凯航空", value: "奥凯航空" },
{ label: "GS-天津航空", value: "天津航空" },
{ label: "CN-大新华", value: "大新华" },
{ label: "KN-中联航", value: "中联航" },
{ label: "TV-西藏航空", value: "西藏航空" },
{ label: "8L-祥鹏航空", value: "祥鹏航空" },
{ label: "KY-昆明航空", value: "昆明航空" },
{ label: "EU-成都航空", value: "成都航空" },
{ label: "G5-华夏航空", value: "华夏航空" },
{ label: "NS-河北航空", value: "河北航空" },
{ label: "QW-青岛航空", value: "青岛航空" },
{ label: "Y8-扬子江", value: "扬子江" },
{ label: "PN-西部航空", value: "西部航空" },
{ label: "DZ-东海航空", value: "东海航空" },
{ label: "GT-桂林航空", value: "桂林航空" },
{ label: "9H-长安航空", value: "长安航空" },
{ label: "GY-多彩航空", value: "多彩航空" },
{ label: "DR-瑞丽航空", value: "瑞丽航空" },
{ label: "GJ-长龙航空", value: "长龙航空" },
{ label: "GX-广西北部", value: "广西北部" },
],
}));
export default airTicketStore;

@ -1,15 +1,14 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { appendRequestParams, fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config"
import { loadPageSpy } from '@/pageSpy'
import { usingStorage } from '@/hooks/usingStorage'
import { isEmpty } from "@/utils/commons";
import { lifecycleware } from '@/utils/lifecycle'
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_I18N = 'i18nextLng'
const WILDCARD_TOKEN = '*'
@ -39,65 +38,48 @@ export const fetchPermissionListByUserId = async (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(devtools((set, get) => ({
const useAuthStore = create(lifecycleware((set, get) => ({
...initialState,
initAuth: async () => {
const { loadUserPermission } = get()
const { setStorage, getValue, loginToken } = usingStorage()
const language = getValue(KEY_I18N)
appendRequestParams("lgc", language === "zh" ? 2 : 1)
// Dev 模式使用 localStorage会有 token 失效情况,需要手动删除
// Prod 环境没有该问题
const userJson = await fetchUserDetail(loginToken)
onAuth: async () => {
const { startTokenInterval, loadUserPermission } = get()
const { userId, loginToken } = usingStorage()
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userJson.LMI_SN)
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: isEmpty(userJson.real_name) ? userJson.LoginName : userJson.real_name,
rolesName: userJson.roles_name,
emailAddress: userJson.LMI_listmail,
travelAgencyName: isEmpty(userJson.VName) ? userJson.LMI_VEI_SN : userJson.VName
}
}))
loadPageSpy(`${userJson.LoginName}-${userJson.VName}`)
appendRequestParams('lmi_sn', userId)
await loadUserPermission(userId)
startTokenInterval()
},
authenticate: async (usr, pwd) => {
const { initAuth } = get()
const { onAuth } = get()
const { setStorage } = usingStorage()
const { token: loginToken } = await fetchLoginToken(usr, pwd)
const userDetail = await fetchUserDetail(loginToken)
setStorage(KEY_LOGIN_TOKEN, loginToken)
setStorage(KEY_USER_ID, userDetail.LMI_SN)
setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN)
await initAuth()
await onAuth()
set(() => ({
tokenTimeout: false,
loginStatus: 302
}))
},
@ -122,34 +104,50 @@ const useAuthStore = create(devtools((set, get) => ({
},
logout: () => {
const { currentUser } = get()
const { tokenInterval } = get()
const { clearStorage } = usingStorage()
clearStorage()
set(() => ({
...initialState,
currentUser: {
username: currentUser.username
clearInterval(tokenInterval)
set(initialState)
},
startTokenInterval: () => {
const { logout } = get()
async function checkTokenTimeout() {
const { LastReqDate } = await fetchLastRequet()
const lastReqDate = new Date(LastReqDate)
const now = new Date()
const diffTime = now.getTime() - lastReqDate.getTime()
const diffHours = diffTime/1000/60/60
if (diffHours > 1) {
logout()
}
}
const interval = setInterval(() => checkTokenTimeout(), 1000*60*10)
set(() => ({
tokenInterval: interval
}))
},
// 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) => {
@ -171,16 +169,6 @@ const useAuthStore = create(devtools((set, get) => ({
})
},
// 根据某项数据来判断是否有权限
//
// INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
// VALUES ('审核CH直销产品', '[125, 375]', 'data')
//
// const PERM_PRODUCTS_AUDIT_CH = '[125, 375]'
isAllowed: (perm, data) => {
return true
},
}), { name: 'authStore' }))
})))
export default useAuthStore

@ -110,13 +110,13 @@ const useFeedbackStore = create(
const allGroup = groupBy(_result, 'EOI_GRI_SN');
const filterV = Object.keys(allGroup).reduce((r, gsn) => {
const v2 = allGroup[gsn].filter((v) => v.EOI_CII_SN);
const withAllGuide = allGroup[gsn].map((row) => ({ ...row, CityGuide: row.GriName.map((rg) => `${rg.GuideCity}: ${rg.GuideName}`).join(' ; ') }));
const withAllGuide = allGroup[gsn].map((row) => ({ ...row, CityGuide: row.GriName_AsJOSN.map((rg) => `${rg.GuideCity}: ${rg.GuideName}`).join(' ; ') }));
return r.concat(v2.length > 0 ? v2 : withAllGuide);
}, []);
setFeedbackList(filterV);
setLoading(false);
},
}), { name: 'feedbackStore'})
}))
);
export default useFeedbackStore;

@ -7,6 +7,6 @@ export const useFormStore = create(
setFormValues: (values) => set((state) => ({ formValues: { ...state.formValues, ...values } })),
formValuesToSub: {},
setFormValuesToSub: (values) => set((state) => ({ formValuesToSub: { ...state.formValuesToSub, ...values } })),
}), { name: 'formStore' })
}))
);
export default useFormStore;

@ -129,7 +129,7 @@ const useInvoiceStore = create(
setLoading(false);
},
}), { name: 'invoiceStore'})
}))
);
export default useInvoiceStore;

@ -48,6 +48,6 @@ export const useNoticeStore = create(
setNoticeUnRead(noticeUnRead);
},
}), { name: 'noticeStore' })
}))
);
export default useNoticeStore;

@ -1,53 +1,34 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import dayjs from 'dayjs'
import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { groupBy, generateId, isNotEmpty } from '@/utils/commons';
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);
const { errcode, result } = await postForm(`${HT_HOST}/agency/products/copy`, formData);
return errcode === 0 ? true : false;
};
export const getAgencyProductsAction = async (param) => {
const _param = { ...param, use_year: String(param.use_year || '').replace('all', ''), audit_state: String(param.audit_state || '').replace('all', '') };
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 getAgencyAllExtrasAction = async (param) => {
const _param = { ...param, use_year: String(param.use_year || '').replace('all', ''), };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_products_extras`, _param);
const extrasMapped = result.reduce((acc, curr) => ({...acc, [curr.product_id]: curr.extras}), {});
return errcode !== 0 ? {} : extrasMapped;
}
/**
* @param {object} body { id, travel_agency_id, extras: [{id, title, code}] }
*
*/
export const addProductExtraAction = async (body) => {
// console.log('addProductExtraAction', body);
// return true; // test: 先不更新到HT
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body);
return errcode === 0 ? true : false;
};
@ -56,7 +37,6 @@ export const addProductExtraAction = async (body) => {
*
*/
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;
};
@ -66,14 +46,11 @@ export const delProductExtrasAction = async (body) => {
* @param {object} param { id, travel_agency_id, use_year }
*/
export const getAgencyProductExtrasAction = async (param) => {
const _param = { ...param, use_year: String(param.use_year || '').replace('all', '') };
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,
@ -89,109 +66,30 @@ export const postProductsQuoteAuditAction = async (auditState, quoteRow) => {
// return errcode !== 0 ? {} : result;
};
export const postAgencyProductsAuditAction = async (auditState, agency) => {
export const postProductsAuditAction = async (auditState, infoRow) => {
const postbody = {
audit_state: auditState,
travel_agency_id: agency.travel_agency_id,
use_year: agency.use_year,
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/agency_products_audit`, formData);
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/travel-agency-products-audit`, formData);
return json;
};
/**
* 供应商提交审核
*/
export const postAgencyAuditAction = async (travel_agency_id, use_year) => {
const postbody = {
use_year,
travel_agency_id,
};
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const { errcode, result } = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_submit`, formData);
return { errcode, result, success: errcode === 0 };
// const { errcode, result } = json;
// return errcode !== 0 ? {} : result;
};
/**
* 保存一个产品
*/
export const postProductsSaveAction = async (products) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_save`, products);
return { errcode, result, success: errcode === 0 };
}
/**
* 删除产品报价
*/
export const deleteQuotationAction = async (id) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_quotation_delete`, {id});
return { errcode, result, success: errcode === 0 };
}
/**
* 获取合同备注
*/
export const fetchRemarkList = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_get`, params)
return { errcode, result, success: errcode === 0 }
}
/**
* 保存合同备注
*/
export const postRemarkList = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_add`, params)
return { errcode, result, success: errcode === 0 }
}
/**
* 产品价格快照
*/
export const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
/**
* 修改产品的类型
*/
export const moveProductTypeAction = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_move`, params)
return errcode !== 0 ? [] : result;
};
const defaultRemarkList = [
{id: 0, "product_type_id": "6","Memo": ""},
{id: 0, "product_type_id": "B","Memo": ""},
{id: 0, "product_type_id": "J","Memo": ""},
{id: 0, "product_type_id": "Q","Memo": ""},
{id: 0, "product_type_id": "7","Memo": ""},
{id: 0, "product_type_id": "R","Memo": ""},
{id: 0, "product_type_id": "D","Memo": ""}
]
const initialState = {
loading: false,
searchValues: {}, // 客服首页: 搜索条件
agencyList: [], // 客服首页: 搜索结果
activeAgency: {}, // 审核/编辑 页: 当前的供应商
activeAgencyState: null,
agencyProducts: {}, // 审核/编辑 页: 供应商产品列表
editingProduct: {}, // 编辑页: 当前编辑的产品
quotationList: [], // 编辑页: 当前产品报价列表
editing: false,
switchParams: {}, // 头部切换参数
}
searchValues: {},
agencyList: [],
activeAgency: {},
agencyProducts: {},
editingProduct: {},
};
export const useProductsStore = create(
devtools((set, get) => ({
// 初始化状态
@ -202,229 +100,11 @@ export const useProductsStore = create(
setSearchValues: (searchValues) => set({ searchValues }),
setAgencyList: (agencyList) => set({ agencyList }),
setActiveAgency: (activeAgency) => set({ activeAgency }),
setActiveAgencyState: (activeAgencyState) => set({ activeAgencyState }),
setAgencyProducts: (agencyProducts) => set({ agencyProducts }),
setEditingProduct: (product) => {
set(() => ({
editingProduct: product,
quotationList: (product?.quotation??[]).map(q => {
return {
...q,
key: generateId(),
fresh: false
}
})
}))
},
setEditing: (editing) => set({ editing }),
setSwitchParams: (switchParams) => set({ switchParams }),
appendNewProduct: (productItem) => {
const { setActiveAgency, agencyProducts, setAgencyProducts } = get();
const typeGroup = agencyProducts[productItem.info.product_type_id] || [];
const newIndex = typeGroup.findIndex((item) => item.info.id === productItem.info.id);
if (newIndex !== -1) {
typeGroup.splice(newIndex, 1, productItem);
} else {
typeGroup.unshift(productItem);
}
return set({
agencyProducts: { ...agencyProducts, [productItem.info.product_type_id]: typeGroup },
});
},
setEditingProduct: (editingProduct) => set({ editingProduct }),
reset: () => set(initialState),
getRemarkList: async() => {
const {switchParams} = get()
const { result, success } = await fetchRemarkList({
travel_agency_id: switchParams.travel_agency_id, use_year: switchParams.use_year
})
if (success) {
const mapRemarkList = defaultRemarkList.map(remark => {
const filterResult = result.filter(r => r.product_type_id === remark.product_type_id)
if (filterResult.length > 0) return filterResult[0]
else return remark
})
return Promise.resolve(mapRemarkList)
} else {
return Promise.resolve('获取合同备注失败')
}
},
saveOrUpdateRemark: async(remarkList) => {
const {switchParams} = get()
const mapRemarkList = remarkList.map(remark => {
return {
id: remark.id,
travel_agency_id: switchParams.travel_agency_id,
use_year: switchParams.use_year,
product_type_id: remark.product_type_id,
Memo: remark.Memo,
}
})
const { result, success } = await postRemarkList(mapRemarkList)
if (success) {
return Promise.resolve(result)
} else {
return Promise.resolve('保存合同备注失败')
}
},
newEmptyQuotation: (useDates) => ({
id: null,
adult_cost: 0,
child_cost: 0,
currency: 'RMB',
unit_id: '0',
group_size_min: 1,
group_size_max: 10,
use_dates: useDates,
weekdayList: [],
fresh: true // 标识是否是新记录,新记录才用添加列表
}),
appendQuotationList: (defList) => {
const { activeAgency, editingProduct, quotationList } = get()
const generatedList = []
defList.forEach(definition => {
definition?.useDateList.map(useDateItem => {
const mappedPriceList = definition?.priceList.map(price => {
return {
id: null,
adult_cost: price.priceInput.audultPrice,
child_cost: price.priceInput.childrenPrice,
group_size_min: price.priceInput.numberStart,
group_size_max: price.priceInput.numberEnd,
currency: definition.currency,
unit_id: definition.unitId,
// 保持和 API 返回格式一致,日期要转换为字符串
use_dates_start: useDateItem.useDate[0].format('YYYY-MM-DD'),
use_dates_end: useDateItem.useDate[1].format('YYYY-MM-DD'),
weekdays: definition.weekend.join(','),
WPI_SN: editingProduct.info.id,
WPP_VEI_SN: activeAgency.travel_agency_id,
lastedit_changed: {},
audit_state_id: -1,
key: generateId(),
fresh: false
}
})
generatedList.push(...mappedPriceList)
})
})
const mergedList = [...quotationList,...generatedList]
set(() => ({
quotationList: mergedList
}))
return mergedList
},
saveOrUpdateQuotation: (formValues) => {
const { activeAgency, editingProduct, quotationList } = get()
let mergedList = []
formValues.WPI_SN = editingProduct.info.id
formValues.WPP_VEI_SN = activeAgency.travel_agency_id
formValues.use_dates_start = formValues.use_dates[0].format('YYYY-MM-DD')
formValues.use_dates_end = formValues.use_dates[1].format('YYYY-MM-DD')
formValues.weekdays = formValues.weekdayList.join(',')
if (formValues.fresh) {
formValues.key = generateId()
formValues.lastedit_changed = {}
formValues.audit_state_id = -1 // 新增,
formValues.fresh = false // 添加到列表后就不是新纪录,保存要修改原来记录
mergedList = [...quotationList,...[formValues]]
} else {
mergedList = quotationList.map(prevQuotation => {
if (prevQuotation.key === formValues.key) {
const changedObject = {}
for (const [key, value] of Object.entries(formValues)) {
if (key === 'use_dates' || key === 'id' || key === 'key' || key === 'weekdayList'
|| key === 'WPI_SN' || key === 'WPP_VEI_SN') continue
const preValue = prevQuotation[key]
const hasChanged = preValue !== value
if (hasChanged) {
changedObject[key] = preValue
}
}
return {
...prevQuotation,
audit_state_id: -1,
adult_cost: formValues.adult_cost,
child_cost: formValues.child_cost,
currency: formValues.currency,
unit_id: formValues.unit_id,
group_size_min: formValues.group_size_min,
group_size_max: formValues.group_size_max,
use_dates_start: formValues.use_dates_start,
use_dates_end: formValues.use_dates_end,
weekdays: formValues.weekdays,
lastedit_changed: changedObject
}
} else {
return prevQuotation
}
})
}
set(() => ({
quotationList: mergedList
}))
return mergedList
},
deleteQuotation: async(quotation) => {
const { editingProduct, quotationList, agencyProducts } = get()
const productTypeId = editingProduct.info.product_type_id;
const quotationId = quotation.id
const newQuotationList = quotationList.filter(q => {
return q.key != quotation.key
})
const newProductList = agencyProducts[productTypeId].map(p => {
if (p.info.id == editingProduct.info.id) {
return {
...editingProduct,
quotation: newQuotationList
}
} else {
return p
}
})
set({
agencyProducts: {
...agencyProducts,
[productTypeId]: newProductList
},
quotationList: newQuotationList
})
if (isNotEmpty(quotationId)) {
const { success } = await deleteQuotationAction(quotationId)
if (success) {
return Promise.resolve(newQuotationList)
} else {
return Promise.reject(newQuotationList)
}
}
},
// side effects
searchAgency: async (param) => {
const { setLoading, setAgencyList } = get();
@ -435,22 +115,12 @@ export const useProductsStore = create(
},
getAgencyProducts: async (param) => {
const { setLoading, setActiveAgency, setActiveAgencyState, setAgencyProducts, editingProduct, setEditingProduct } = get();
const { setLoading, setActiveAgency, setAgencyProducts } = get();
setLoading(true);
setAgencyProducts({});
// setEditingProduct({});
const res = await getAgencyProductsAction(param);
const productsData = groupBy(res.products, (row) => row.info.product_type_id);
setAgencyProducts(productsData);
setActiveAgency(res.agency);
setActiveAgencyState(res.agency.audit_state_id);
if (editingProduct?.info?.id) {
const item = (productsData[editingProduct.info.product_type_id] || []).find((item) => item.info.id === editingProduct.info.id);
setEditingProduct(item);
} else {
setEditingProduct({});
}
setLoading(false);
},
@ -458,6 +128,6 @@ export const useProductsStore = create(
const res = await getAgencyProductExtrasAction(param);
// todo:
},
}), { name: 'productStore' })
}))
);
export default useProductsStore;

@ -64,6 +64,6 @@ export const useReportStore = create(
setCommendScoresData(errcode === 0 ? Result : {});
// setLoading(false);
},
}), { name: 'reportStore'})
}))
);
export default useReportStore;

@ -1,5 +1,4 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config"
import { prepareUrl } from '@/utils/commons'
@ -9,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) => {
@ -32,10 +31,10 @@ 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(devtools((set, get) => ({
const useReservationStore = create((set, get) => ({
cityList: [],
@ -92,26 +91,26 @@ const useReservationStore = create(devtools((set, get) => ({
}))
},
fetchReservationList: (formValues, current=1) => {
fetchReservationList: (formVal, 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', formValues.referenceNo)
.append('DateStart', formValues.startdate)
.append('DateEnd', formValues.enddate)
.append('NotConfirm', formValues.unconfirmed)
.append('GroupNo', formVal.referenceNo)
.append('DateStart', formVal.startdate)
.append('DateEnd', formVal.enddate)
.append('NotConfirm', '')//status)// Todo: 待解决
.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) => {
const mapReservationList = (json?.Result??[]).map((data, index) => {
return {
key: data.vas_gri_sn,
reservationId: data.vas_gri_sn,
@ -133,32 +132,32 @@ const useReservationStore = create(devtools((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) => {
const guideList = (json?.Result??[]).map((data, index) => {
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) => {
@ -168,8 +167,8 @@ const useReservationStore = create(devtools((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,
@ -210,7 +209,7 @@ const useReservationStore = create(devtools((set, get) => ({
getReservationDetail(travelAgencyId, reservationDetail.reservationId)
return json
}
})
});
},
setupCityGuide: (cityId, guideId) => {
@ -230,7 +229,7 @@ const useReservationStore = create(devtools((set, get) => ({
if (json.errcode != 0) {
throw new Error(json.errmsg + ': ' + json.errcode)
}
})
});
},
@ -242,7 +241,7 @@ const useReservationStore = create(devtools((set, get) => ({
.append('VEI_SN', travelAgencyId)
.append('GRI_SN', selectedReservation.reservationId)
.append('LGC', 1)
.build()
.build();
return fetchJSON(fetchUrl)
.then(json => {
@ -253,6 +252,10 @@ const useReservationStore = create(devtools((set, get) => ({
return data.GuideName
}).join(',')
runInAction(() => {
selectedReservation.guide = reservationGuide
})
set((state) => ({
selectedReservation: {
...state.selectedReservation,
@ -266,6 +269,6 @@ const useReservationStore = create(devtools((set, get) => ({
}
})
}
}), { name: 'reservationStore' }))
}))
export default useReservationStore
export default useReservationStore

@ -1,211 +0,0 @@
import { create } from "zustand";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST, DATE_FORMAT } from "@/config";
import dayjs from "dayjs";
const trainTicketStore = create((set, get) => ({
loading: false,
setLoading: loading => set({ loading }),
setPlanList: planList => set({ planList }),
setPlanDetail: planDetail => set({ planDetail }),
setGuestList: guestList => set({ guestList }),
setVEIFlightBill: vEIFlightBill => set({ vEIFlightBill }),
setVeiPlanChangeTxt: veiPlanChangeTxt => set({ veiPlanChangeTxt }),
async getPlanList(vei_sn, GRI_Name, TimeStart, TimeEnd, plan_state, airticket_state) {
const { setLoading, setPlanList } = get();
setLoading(true);
const searchParams = {
vei_sn: vei_sn,
FlightDate1: TimeStart,
FlightDate2: TimeEnd,
GRI_Name: GRI_Name,
FlightStatus: plan_state,
TicketIssued: airticket_state,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetTrainPlan`, searchParams);
const _result = errcode !== 0 ? [] : result;
setPlanList(_result);
setLoading(false);
},
async getPlanDetail(vei_sn, gri_sn) {
const { setPlanDetail } = get();
const searchParams = {
vei_sn: vei_sn,
gri_sn: gri_sn,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetTrainPlanDetail`, searchParams);
const _result = errcode !== 0 ? [] : result;
setPlanDetail(_result);
//return _result.filter(item => isNotEmpty(item.GRI_No));
return "dsadsd";
},
async postFlightDetail(CLF_SN, GRI_SN, VEI_SN, original_values, info_object) {
const formData = new FormData();
formData.append("CLF_SN", CLF_SN ? CLF_SN : "");
formData.append("GRI_SN", GRI_SN);
formData.append("VEI_SN", VEI_SN);
for (const [key, value] of Object.entries(original_values)) {
formData.append(key, value); //先用原始数据填充一遍,确保复制了全部数据到新表
}
for (const [key, value] of Object.entries(info_object)) {
formData.set(key, value); //再用新值覆盖
}
formData.set("StartDate", dayjs(info_object.StartDate).format(DATE_FORMAT)); //再用新值覆盖
//是否出票的值true、false变为1或0
formData.set("TicketIssued", info_object.TicketIssued ? 1 : 0);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_info";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//删除火车信息
async delete_flight_info(CLF_SN) {
const searchParams = {
CLF_SN: CLF_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_info`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
async getGuestList(coli_sn) {
const { setGuestList } = get();
const searchParams = {
COLI_SN: coli_sn,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightGuestInfo`, searchParams);
const _result = errcode !== 0 ? [] : result;
setGuestList(_result);
},
//获取账单列表
async getVEIFlightBill(VEI_SN, GRI_Name, CheckStatus, FlightDate1, FlightDate2) {
const { setLoading, setVEIFlightBill } = get();
setLoading(true);
const searchParams = {
VEI_SN: VEI_SN,
GRI_Name: GRI_Name,
CheckStatus: CheckStatus,
FlightDate1: FlightDate1,
FlightDate2: FlightDate2,
ServiceType: 2,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetVEIFlightBill`, searchParams);
const _result = errcode !== 0 ? [] : result;
setVEIFlightBill(_result);
setLoading(false);
},
//保存费用
async postFlightCost(values) {
const formData = new FormData();
for (const [key, value] of Object.entries(values)) {
formData.append(key, value); //先用原始数据填充一遍,确保复制了全部数据到新表
}
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_cost";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//删除费用
async deleteFlightCost(CLC_SN) {
const searchParams = {
CLC_SN: CLC_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_cost`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
//获取变更信息
async getVeiPlanChange(VEI_SN, GRI_SN) {
const { setVeiPlanChangeTxt } = get();
const searchParams = {
VEI_SN: VEI_SN,
GRI_SN: GRI_SN,
ServiceType: 2,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetVeiFlightPlanChange`, searchParams);
const _result = errcode !== 0 ? [] : result;
setVeiPlanChangeTxt(_result);
},
//提交变更确认
async postVeiFlightPlanConfirm(VEI_SN, GRI_SN, LMI_SN, ConfirmInfo) {
const formData = new FormData();
formData.append("VEI_SN", VEI_SN);
formData.append("GRI_SN", GRI_SN);
formData.append("LMI_SN", LMI_SN);
formData.append("ServiceType", 2);
formData.append("ConfirmInfo", ConfirmInfo);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/VeiFlightPlanConfirm";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//提交账单
async postVEIFlightBillSubmit(VEI_SN, values) {
const formData = new FormData();
formData.append("vei_sn", VEI_SN);
formData.append("ServiceType", 2);
formData.append("billdata", JSON.stringify(values));
const postUrl = HT_HOST + "/Service_BaseInfoWeb/VEIFlightBillSubmit";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//通知顾问查看车票信息
async ticketIssuedNotifications(LMI_SN, CLF_SN, OPI_SN, FlightMemo_messages,ticketImages) {
const searchParams = {
CLF_SN: CLF_SN,
OPI_SN: OPI_SN,
LMI_SN: LMI_SN,
ServiceType: 2,
FlightMemo_messages: FlightMemo_messages,
ticketImages: JSON.stringify(ticketImages),
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/TicketIssuedNotifications`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
// 定义座位表对象
seatTable: [
{ code: "9", name: "商务座", enName: "Business Class Seat" },
{ code: "P", name: "特等座", enName: "Superior Seat" },
{ code: "M", name: "一等座", enName: "First Class Seat" },
{ code: "O", name: "二等座", enName: "Second Class Seat" },
{ code: "6", name: "高级软卧", enName: "Superior Soft Sleeper" },
{ code: "5", name: "包厢硬卧", enName: "Hard Sleeper in Private Compartment" },
{ code: "4", name: "软卧", enName: "Soft Sleeper" },
{ code: "F", name: "动卧", enName: "High - Speed Train Sleeper" },
{ code: "3", name: "硬卧", enName: "Hard Sleeper" },
{ code: "2", name: "软座", enName: "Soft Seat" },
{ code: "1", name: "硬座", enName: "Hard Seat" },
{ code: "Q", name: "观光座", enName: "Sightseeing Seat" },
{ code: "H", name: "其它", enName: "Other" },
{ code: "WZ", name: "无座", enName: "Standing - room Only" },
{ code: "YDW", name: "一等卧", enName: "First - Class Sleeper" },
{ code: "EDW", name: "二等卧", enName: "Second - Class Sleeper" },
],
}));
export default trainTicketStore;

@ -82,10 +82,25 @@ export function isNotEmpty(val) {
return val !== undefined && val !== null && val !== "";
}
// export function isEmpty(val) {
// return val === undefined || val === null || val === "";
// }
export function prepareUrl(url) {
return new UrlBuilder(url);
}
// export function debounce(fn, delay = 500) {
// let timer;
// return e => {
// e.persist();
// clearTimeout(timer);
// timer = setTimeout(() => {
// fn(e);
// }, delay);
// };
// }
export function throttle(fn, delay, atleast) {
let timeout = null,
startTime = new Date();
@ -139,7 +154,7 @@ export function isEmpty(val) {
* 数组排序
*/
export const sortBy = key => {
return (a, b) => (getNestedValue(a, key) > getNestedValue(b, key) ? 1 : getNestedValue(b, key) > getNestedValue(a, key) ? -1 : 0);
return (a, b) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0);
};
/**
@ -162,13 +177,6 @@ export const sortArrayByOrder = (items, keyName, keyOrder) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
};
export function unique(arr) {
const x = new Set(arr);
return [...x];
}
export const uniqWith = (arr, fn) => arr.filter((element, index) => arr.findIndex((step) => fn(element, step)) === index);
/**
* 合并Object, 递归地
*/
@ -247,47 +255,23 @@ export function omit(object, keysToOmit) {
/**
* 深拷贝
*/
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;
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;
}
/**
* 向零四舍五入, 固定精度设置
*/
@ -383,7 +367,7 @@ export function at(obj, path) {
const indexes = path.split(".").map(i => i);
result = [obj];
for (let i = 0; i < indexes.length; i++) {
result = [result[0]?.[indexes[i]] || undefined];
result = [result[0][indexes[i]]];
}
}
return result;
@ -629,8 +613,3 @@ export const formatColonTime = text => {
const minutes = text.substring(2);
return `${hours}:${minutes}`;
};
// 生成唯一 36 位数字,用于新增记录 ID 赋值React key 属性等
export const generateId = () => (
new Date().getTime().toString(36) + Math.random().toString(36).substring(2, 9)
)

@ -10,19 +10,16 @@ export const addAuthLinstener = (fn) => {
}
export const notifyInit = async () => {
for (const listener of initListener) {
await listener()
}
initListener.forEach(async (fn) => {
await fn()
})
}
export const notifyAuth = async (obj) => {
for (const listener of authListener) {
await listener(obj)
}
authListener.forEach(async (fn) => await fn(obj))
}
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段。
// 失败,无法同步调用异步方法!
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段
export const lifecycleware = (fn) => (set, get, store) => {
addInitLinstener(() => {

@ -1,54 +1,75 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react'
import {
Popover, Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp,
Button, Form, Input
} from 'antd'
import { DownOutlined } from '@ant-design/icons'
import 'antd/dist/reset.css'
import AppLogo from '@/assets/highlights_travel_600_550.png'
import { isEmpty } from '@/utils/commons'
import { useTranslation } from 'react-i18next'
import zhLocale from 'antd/locale/zh_CN'
import enLocale from 'antd/locale/en_US'
import 'dayjs/locale/zh-cn'
import { Outlet, Link, useHref, useNavigate, useLocation, NavLink } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Layout, Menu, ConfigProvider, theme, Dropdown, Space, Row, Col, Badge, Typography, Modal, Input, Button, App as AntApp } from 'antd';
import { DownOutlined } from '@ant-design/icons';
import 'antd/dist/reset.css';
import AppLogo from '@/assets/logo-gh.png';
import { isEmpty, isNotEmpty } from '@/utils/commons';
import Language from '../i18n/LanguageSwitcher';
import { useTranslation } from 'react-i18next';
import zhLocale from 'antd/locale/zh_CN';
import enLocale from 'antd/locale/en_US';
import 'dayjs/locale/zh-cn';
import ErrorBoundary from '@/components/ErrorBoundary'
import { BUILD_VERSION, GIT_HEAD, PERM_PRODUCTS_INFO_PUT } from '@/config'
import useNoticeStore from '@/stores/Notice'
import { BUILD_VERSION, } from '@/config';
import useNoticeStore from '@/stores/Notice';
import useAuthStore from '@/stores/Auth'
import { useThemeContext } from '@/stores/ThemeContext'
import { fetchUserDetail} from '@/stores/Auth'
import { usingStorage } from '@/hooks/usingStorage'
import { useDefaultLgc } from '@/i18n/LanguageSwitcher'
import { appendRequestParams } from '@/utils/request'
import LogUploader from '@/components/LogUploader'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET } from '@/config'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET } from '@/config'
const { Header, Content, Footer } = Layout
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 [userDetail, setUserDetail] = useState({})
const [isPermitted, currentUser] = useAuthStore(
(state) => [state.isPermitted, state.currentUser])
const [authenticate, tokenTimeout, isPermitted] = useAuthStore(
(state) => [state.authenticate, state.tokenTimeout, state.isPermitted])
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)
useEffect(() => {
if (isNotEmpty(loginToken)) {
fetchUserDetail(loginToken)
.then(u => {
setUserDetail({
username: u.LoginName,
realname: u.real_name,
travelAgencyName: u.VName,
})
})
}
}, [loginToken])
useEffect(() => {
if (needToLogin) {
navigate('/login')
}
}, [href])
const onSubmit = () => {
authenticate(userDetail?.username, password)
.catch(ex => {
console.error(ex)
alert(t('Validation.LoginFailed'))
})
setPassword('')
}
const splitPath = href.split('/')
let defaultPath = 'notice'
@ -56,42 +77,55 @@ function App() {
defaultPath = splitPath[1]
}
const { language } = useDefaultLgc()
const [antdLng, setAntdLng] = useState(enLocale)
const {
token: { colorBgContainer },
} = theme.useToken()
const [antdLng, setAntdLng] = useState(enLocale);
useEffect(() => {
setAntdLng(i18n.language === 'en' ? enLocale : zhLocale)
appendRequestParams('lgc', language)
setAntdLng(i18n.language === 'en' ? enLocale : zhLocale);
}, [i18n.language])
//
const isProductPermitted = isPermitted(PERM_PRODUCTS_MANAGEMENT) || isPermitted(PERM_PRODUCTS_INFO_PUT)
const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/pick-year'
return (
<ConfigProvider locale={antdLng}
theme={{
token: {
colorPrimary: colorPrimary,
colorPrimary: '#00b96b',
// "sizeStep": 3,
// "sizeUnit": 3,
},
algorithm: theme.defaultAlgorithm,
}}>
<AntApp>
<FloatButton.Group
shape='square'
style={{
insetInlineEnd: 94,
}}
>
<LogUploader />
<FloatButton.BackTop />
</FloatButton.Group>
<ErrorBoundary>
<Layout className='min-h-screen h-dvh'>
<Header className='sticky top-0 z-10 w-full'>
<Modal
centered
closable={false}
maskClosable={false}
footer={null}
open={tokenTimeout}
>
<Title level={3}>{t('LoginTimeout')}</Title>
<div>{t('LoginTimeoutTip')}</div>
<Space direction='horizontal'>
<Input.Password value={password}
onChange={(e) => setPassword(e.target.value)}
onPressEnter={onSubmit}
addonBefore={userDetail?.username} />
<Button
onClick={onSubmit}
>{t('Submit')}</Button></Space>
</Modal>
<Layout
style={{
minHeight: '100vh',
}}>
<Header className='header' style={{ position: 'sticky', top: 0, zIndex: 1, width: '100%' }}>
<Row gutter={{ md: 24 }} justify='end' align='middle'>
<Col span={15}>
<Col span={16}>
<NavLink to='/'>
<img src={AppLogo} className='float-left h-12 my-2 mr-6 ml-0' alt='App logo' />
<img src={AppLogo} className='logo' alt='App logo' />
</NavLink>
<Menu
theme='dark'
@ -103,8 +137,7 @@ function App() {
isPermitted(PERM_OVERSEA) ? { key: 'feedback', label: <Link to='/feedback'>{t('menu.Feedback')}</Link> } : null,
isPermitted(PERM_OVERSEA) ? { key: 'report', label: <Link to='/report'>{t('menu.Report')}</Link> } : null,
isPermitted(PERM_AIR_TICKET) ? { key: 'airticket', label: <Link to='/airticket'>{t('menu.Airticket')}</Link> } : null,
isPermitted(PERM_TRAIN_TICKET) ? { key: 'trainticket', label: <Link to='/trainticket'>{t('menu.Trainticket')}</Link> } : null,
isProductPermitted ? { key: 'products', label: <Link to={productLink}>{t('menu.Products')}</Link> } : null,
{ key: 'products', label: <Link to='/products'>{t('menu.Products')}</Link> },
{
key: 'notice',
label: (
@ -117,10 +150,10 @@ function App() {
]}
/>
</Col>
<Col span={7}>
<h3 className='text-white mb-0 line-clamp-1 text-end'>
{currentUser?.travelAgencyName}
</h3>
<Col span={4}>
<Title level={3} style={{ color: 'white', marginBottom: '0', display: 'flex', justifyContent: 'end' }}>
{userDetail?.travelAgencyName}
</Title>
</Col>
<Col span={2}>
<Dropdown
@ -139,18 +172,27 @@ function App() {
>
<a onClick={e => e.preventDefault()}>
<Space>
<div className='line-clamp-1'>{currentUser?.realname}</div>
{userDetail?.realname}
<DownOutlined />
</Space>
</a>
</Dropdown>
</Col>
<Col span={2}>
<Language />
</Col>
</Row>
</Header>
<Content className='p-6 m-0 min-h-72 bg-white overflow-auto'>
<Content
style={{
padding: 24,
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}>
{needToLogin ? <>login...</> : <Outlet />}
</Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}({GIT_HEAD})</Footer>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
</Layout>
</ErrorBoundary>
</AntApp>

@ -1,33 +0,0 @@
//
import { useState, useEffect } from "react";
import { Alert } from "antd";
import { useParams } from "react-router-dom";
import { ImageViewer } from "@/components/ImageUploader";
const CustomerImageViewer = () => {
const [ossKey, setOssKey] = useState("");
const [showUploader, setShowUploader] = useState(false);
const { key } = useParams();
useEffect(() => {
setOssKey(key);
setShowUploader(true);
}, []);
return (
<>
{showUploader ? (
<>
<Alert message="Information" description="You can view all travel-related Photos on this page, provided by your tour guides." type="info" showIcon />
<br />
<ImageViewer osskey={ossKey} overlist={true} />
</>
) : (
<Alert message="Error" description="Photos not found" type="error" showIcon />
)}
</>
);
};
export default CustomerImageViewer;

@ -1,39 +0,0 @@
//
import React, { useState, useEffect } from 'react';
import { Input, Button, Card, Typography, Space, Alert } from 'antd';
import { useParams } from 'react-router-dom';
import ImageUploader from '@/components/ImageUploader';
const { Title, Text } = Typography;
const ImageViewer = () => {
const { GRI_SN, GRI_No } = useParams();
const [ossKey, setOssKey] = useState('');
const [showUploader, setShowUploader] = useState(false);
useEffect(() => {
if (GRI_SN && GRI_No) {
const key = `ghh/${GRI_SN}-${GRI_No}/passport_image`;
setOssKey(key);
setShowUploader(true);
}
}, [GRI_SN, GRI_No]);
return (
<div style={{ padding: '20px' }}>
<Title level={2}>{GRI_SN}-{GRI_No}</Title>
{showUploader && (
<ImageUploader osskey={ossKey} />
)}
{!showUploader && (
<Text>无法从URL中提取订单信息</Text>
)}
</div>
);
};
export default ImageViewer;

@ -1,116 +1,101 @@
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { Button, Form, Input, Row, Radio, App, Typography } from "antd";
import { useTranslation } from "react-i18next";
import useAuthStore from "@/stores/Auth";
import { appendRequestParams } from "@/utils/request";
import { useNavigate, useLocation } from 'react-router-dom'
import { useEffect } from 'react'
import { Button, Checkbox, Form, Input, Row, App } from 'antd'
import { useTranslation } from 'react-i18next'
import useAuthStore from '@/stores/Auth'
import useNoticeStore from '@/stores/Notice'
function Login() {
const [authenticate, loginStatus, defaultRoute] = useAuthStore((state) => [
state.authenticate,
state.loginStatus,
state.defaultRoute,
]);
const [authenticate, loginStatus, defaultRoute] =
useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute])
const { t, i18n } = useTranslation();
const { notification } = App.useApp();
const navigate = useNavigate();
const [form] = Form.useForm();
const { t, i18n } = useTranslation()
const { notification } = App.useApp()
const navigate = useNavigate()
const [form] = Form.useForm()
const handleLngChange = (lng) => {
appendRequestParams("lgc", lng === "zh" ? 2 : 1);
i18n.changeLanguage(lng);
};
const defaultLng = i18n.language ?? "zh";
appendRequestParams("lgc", defaultLng === "zh" ? 2 : 1);
useEffect(() => {
useEffect (() => {
if (loginStatus === 302) {
navigate(defaultRoute);
navigate(defaultRoute)
}
}, [loginStatus]);
}, [loginStatus])
const onFinish = (values) => {
authenticate(values.username, values.password).catch((ex) => {
console.error(ex);
notification.error({
message: t("Validation.Title"),
description: t("Validation.LoginFailed"),
placement: "top",
duration: 4,
});
});
};
authenticate(values.username, values.password)
.catch(ex => {
console.error(ex)
notification.error({
message: t('Validation.Title'),
description: t('Validation.LoginFailed'),
placement: 'top',
duration: 4,
})
})
}
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
};
console.log('Failed:', errorInfo);
}
return (
<>
<Typography.Title className="text-center" level={3}>
Highlights Hub
</Typography.Title>
<Row justify="center" align="middle" className="min-h-96">
<Form
name="login"
layout="vertical"
form={form}
size="large"
labelCol={{
span: 8,
}}
<Row justify='center' align='middle' style={{ minHeight: 500 }}>
<Form
name='basic'
form={form}
size='large'
labelCol={{
span: 8,
}}
wrapperCol={{
span: 16,
}}
style={{
maxWidth: 600,
}}
initialValues={{
remember: true,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete='off'
>
<Form.Item
label={t('Username')}
name='username'
rules={[
{
required: true,
message: t('Validation.UsernameIsEmpty'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('Password')}
name='password'
rules={[
{
required: true,
message: t('Validation.PasswordIsEmpty'),
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item
wrapperCol={{
span: 24,
}}
className="max-w-xl"
initialValues={{
language: defaultLng,
offset: 8,
span: 16,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label={t("Username")}
name="username"
rules={[
{
required: true,
message: t("Validation.UsernameIsEmpty"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("Password")}
name="password"
rules={[
{
required: true,
message: t("Validation.PasswordIsEmpty"),
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item name="language">
<Radio.Group onChange={(e) => handleLngChange(e.target.value)}>
<Radio value="zh">中文</Radio>
<Radio value="en">English</Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="w-full">
{t("Login")}
</Button>
</Form.Item>
</Form>
</Row>
</>
);
<Button type='primary' htmlType='submit' style={{width: '100%'}}>
{t('Login')}
</Button>
</Form.Item>
</Form>
</Row>
)
}
export default Login;
export default Login

@ -1,36 +1,56 @@
import { Outlet } from "react-router-dom";
import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from "antd";
import "antd/dist/reset.css";
import AppLogo from "@/assets/highlights_travel_600_550.png";
import { useThemeContext } from "@/stores/ThemeContext";
import { BUILD_VERSION, GIT_HEAD } from "@/config";
import { Outlet } from 'react-router-dom'
import { Layout, ConfigProvider, theme, Typography, Row, Col, App as AntApp } from 'antd'
import 'antd/dist/reset.css'
import AppLogo from '@/assets/logo-gh.png'
import Language from '../i18n/LanguageSwitcher'
const { Header, Content, Footer } = Layout;
const { Title } = Typography
const { Header, Content, Footer } = Layout
function Standlone() {
const { colorPrimary } = useThemeContext();
const {
token: { colorBgContainer },
} = theme.useToken()
return (
<ConfigProvider
theme={{
token: {
colorPrimary: colorPrimary,
colorPrimary: '#00b96b',
},
algorithm: theme.defaultAlgorithm,
}}>
<AntApp>
<Layout className="min-h-screen">
<Header className="sticky top-0 z-10 w-full text-center">
<img src={AppLogo} className="h-12 my-2 mr-6 ml-0" alt="App logo" />
<Layout
style={{
minHeight: '100vh',
}}>
<Header className='header' style={{ position: 'sticky', top: 0, zIndex: 1, width: '100%' }}>
<Row gutter={{ md: 24 }} justify='center'>
<Col span={4}>
<img src={AppLogo} className='logo' alt='App logo' />
</Col>
<Col span={18}><Title style={{ color: 'white', marginTop: '3.5px' }}>Global Highlights Hub</Title></Col>
<Col span={2}>
<Language />
</Col>
</Row>
</Header>
<Content className="p-6 m-0 min-h-72 bg-white">
<Content
style={{
padding: 24,
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}>
<Outlet />
</Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}({GIT_HEAD})</Footer>
<Footer></Footer>
</Layout>
</AntApp>
</ConfigProvider>
);
)
}
export default Standlone;
export default Standlone

@ -9,10 +9,7 @@ function ChangePassword() {
const { t } = useTranslation()
const navigate = useNavigate()
const [changeUserPassword, defaultRoute] = useAuthStore((state) => [
state.changeUserPassword,
state.defaultRoute,
])
const changeUserPassword = useAuthStore((state) => state.changeUserPassword)
const { notification } = App.useApp()
const [form] = Form.useForm()
@ -44,13 +41,15 @@ function ChangePassword() {
return (
<>
<Row justify="center" align="middle" className='min-h-96'>
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
<Form
name="basic"
form={form}
layout="vertical"
size="large"
className='max-w-xl'
style={{
maxWidth: 600,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
@ -97,7 +96,7 @@ function ChangePassword() {
<Button type="primary" htmlType="submit">
{t('Submit')}
</Button>
<Button onClick={() => navigate(defaultRoute)}>
<Button onClick={() => navigate('/reservation/newest')}>
{t('Cancel')}
</Button>
</Space>

@ -1,7 +1,7 @@
import SearchForm from '@/components/SearchForm'
import useAccountStore, { fetchRoleList, fetchTravelAgencyByName, genRandomPassword } from '@/stores/Account'
import useAccountStore, { fetchRoleList, fetchTravelAgencyByName } from '@/stores/Account'
import useFormStore from '@/stores/Form'
import { isEmpty, debounce } from '@/utils/commons'
import { isEmpty } from '@/utils/commons'
import { ExclamationCircleFilled } from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Select, Space, Table, Typography, Switch } from 'antd'
import dayjs from 'dayjs'
@ -24,12 +24,12 @@ function Management() {
dataIndex: 'realname',
},
{
title: t('account:email'),
dataIndex: 'email',
title: t('account:travelAgencyName'),
dataIndex: 'travelAgencyName',
},
{
title: t('account:travelAgency'),
dataIndex: 'travelAgencyName',
title: t('account:email'),
dataIndex: 'email',
},
{
title: t('account:roleName'),
@ -60,7 +60,6 @@ function Management() {
showDisableConfirm(account, checked)
}} />
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
<Button type='link' key='editAccount' onClick={() => onAccountSeleted(account)}>{t('account:action.edit')}</Button>
</Space>
)
}
@ -129,12 +128,6 @@ function Management() {
const onAccountFinish = (values) => {
saveOrUpdateAccount(values)
.then(() => {
notification.info({
message: 'Notification',
description: '账号保存成功',
placement: 'top',
})
setAccountModalOpen(false)
handelAccountSearch()
})
.catch(ex => {
@ -200,12 +193,11 @@ function Management() {
}
const showResetPasswordConfirm = (account) => {
const confirmTitle = t('account:action.resetPassword.tile')
const randomPassword = genRandomPassword()
const randomPassword = account.username + '@' + (Math.floor(Math.random() * 900) + 100)
modal.confirm({
title: confirmTitle,
title: 'Do you want to reset password?',
icon: <ExclamationCircleFilled />,
content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
content: `Username: ${account.username}, Realname: ${account.realname}`,
onOk() {
resetAccountPassword(account.userId, randomPassword)
.then(() => {
@ -231,16 +223,18 @@ function Management() {
htmlType: 'submit',
}}
title={t('account:detail')}
open={isAccountModalOpen} onCancel={() => setAccountModalOpen(false)}
destroyOnHidden
forceRender
open={isAccountModalOpen} onOk={() => setAccountModalOpen(false)} onCancel={() => setAccountModalOpen(false)}
destroyOnClose={true}
clearOnDestroy={true}
modalRender={(dom) => (
<Form
name='AccountForm'
form={accountForm}
layout='vertical'
size='large'
className='max-w-2xl'
style={{
maxWidth: 600,
}}
onFinish={onAccountFinish}
onFinishFailed={onAccountFailed}
autoComplete='off'
@ -289,7 +283,7 @@ function Management() {
<Input />
</Form.Item>
<Form.Item
label={t('account:travelAgency')}
label={t('account:travelAgencyName')}
name='travelAgencyId'
rules={[
{
@ -305,7 +299,7 @@ function Management() {
loading={dataLoading}
showSearch
filterOption={false}
onSearch={debounce(handleTravelAgencySearch, 800)}
onSearch={handleTravelAgencySearch}
notFoundContent={null}
>
</Select>
@ -328,16 +322,16 @@ function Management() {
</Select>
</Form.Item>
</Modal>
<Space direction='vertical' className='w-full'>
<Space direction='vertical' style={{ width: '100%' }}>
<Title level={3}>{t('account:accountList')}</Title>
<SearchForm
fieldsConfig={{
shows: ['username', 'agency'],
shows: ['username', 'realname'],
fieldProps: {
username: { label: t('account:username') + '/' + t('account:realname') },
agency: { label: t('account:travelAgency') },
username: { label: t('account:username') },
realname: { label: t('account:realname') },
},
sort: { username: 1, agency: 2},
sort: { username: 1, realname: 2, dates: 3},
}}
onSubmit={() => {
handelAccountSearch()
@ -362,6 +356,7 @@ function Management() {
showSizeChanger: true,
showTotal: (total) => { return t('Total') + `${total}` }
}}
onChange={(pagination) => { onSearchClick(pagination.current) }}
columns={accountListColumns} dataSource={accountList}
/>
</Col>

@ -1,24 +1,41 @@
import { Descriptions, Col, Row } from 'antd'
import { useEffect, useState } from 'react'
import { Descriptions, Col, Row } from 'antd';
import { useTranslation } from 'react-i18next'
import useAuthStore from '@/stores/Auth'
import { fetchUserDetail } from '@/stores/Auth'
import { usingStorage } from '@/hooks/usingStorage'
function Profile() {
const { t } = useTranslation()
const currentUser = useAuthStore(state => state.currentUser)
const [userDetail, setUserDetail] = useState({})
const { loginToken } = usingStorage()
useEffect (() => {
fetchUserDetail(loginToken)
.then(json => {
setUserDetail({
username: json.LoginName,
realname: json.real_name,
rolesName: json.roles_name,
emailAddress: json.LMI_listmail,
travelAgencyName: json.VName,
})
})
}, [])
return (
<Row>
<Col span={12} offset={6}>
<Descriptions title={t('userProfile')} layout="vertical" column={2}>
<Descriptions.Item label={t("Username")}>{currentUser?.username}</Descriptions.Item>
<Descriptions.Item label={t("Realname")}>{currentUser?.realname}</Descriptions.Item>
<Descriptions.Item label={t("Email")}>{currentUser?.emailAddress}</Descriptions.Item>
<Descriptions.Item label={t("Company")}>{currentUser?.travelAgencyName}</Descriptions.Item>
<Descriptions.Item label={t("Username")}>{userDetail?.username}</Descriptions.Item>
<Descriptions.Item label={t("Realname")}>{userDetail?.realname}({userDetail?.rolesName})</Descriptions.Item>
<Descriptions.Item label={t("Email")}>{userDetail?.emailAddress}</Descriptions.Item>
<Descriptions.Item label={t("Company")}>{userDetail?.travelAgencyName}</Descriptions.Item>
</Descriptions>
</Col>
</Row>
);
}
export default Profile
export default Profile;

@ -3,7 +3,7 @@ import { PERM_ROLE_NEW } from '@/config'
import useAccountStore, { fetchPermissionList, fetchPermissionListByRoleId, fetchRoleList } from '@/stores/Account'
import { isEmpty } from '@/utils/commons'
import {
PushpinTwoTone,
SyncOutlined,
} from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Space, Table, Tag, TreeSelect, Typography } from 'antd'
import dayjs from 'dayjs'
@ -34,7 +34,7 @@ function RoleList() {
function actionRender(_, role) {
if (role.role_id == 1) {
return (<Button type='text'><PushpinTwoTone twoToneColor="#c0192a" /></Button>)
return (<Tag icon={<SyncOutlined spin />} color='warning'>不能修改</Tag>)
} else {
return (
<Button type='link' key='edit' onClick={() => onRoleSeleted(role)}>{t('account:action.edit')}</Button>
@ -68,7 +68,6 @@ function RoleList() {
['oversea', '海外供应商'],
['domestic', '国内供应商'],
['air-ticket', '机票供应商'],
['train-ticket', '火车票供应商'],
['products', '产品价格'],
['page', '默认页面'],
]);
@ -133,7 +132,6 @@ function RoleList() {
const onRoleFinish = (values) => {
saveOrUpdateRole(values)
.then(() => {
setRoleModalOpen(false)
fetchRoleList()
.then(r => {
setRoleAllList(r)
@ -150,7 +148,6 @@ function RoleList() {
}
const onRoleFailed = (error) => {
console.log('Failed:', error)
// form.resetFields()
}
@ -163,16 +160,18 @@ function RoleList() {
htmlType: 'submit',
}}
title={t('account:detail')}
open={isRoleModalOpen} onCancel={() => setRoleModalOpen(false)}
destroyOnHidden
forceRender
open={isRoleModalOpen} onOk={() => setRoleModalOpen(false)} onCancel={() => setRoleModalOpen(false)}
destroyOnClose={true}
clearOnDestroy={true}
modalRender={(dom) => (
<Form
name='RoleForm'
form={roleForm}
layout='vertical'
size='large'
className='max-w-xl'
style={{
maxWidth: 600,
}}
onFinish={onRoleFinish}
onFinishFailed={onRoleFailed}
autoComplete='off'
@ -199,7 +198,10 @@ function RoleList() {
name='res_array'
>
<TreeSelect treeData={permissionTreeData} value={permissionValue}
popupClassName='max-w-xl overflow-auto'
dropdownStyle={{
maxHeight: 600,
overflow: 'auto',
}}
placement='bottomLeft'
showSearch
allowClear
@ -210,10 +212,12 @@ function RoleList() {
treeCheckable={true}
showCheckedStrategy={TreeSelect.SHOW_CHILD}
placeholder={'Please select'}
className='w-full' />
style={{
width: '100%',
}} />
</Form.Item>
</Modal>
<Space direction='vertical' className='w-full'>
<Space direction='vertical' style={{ width: '100%' }}>
<Title level={3}>{t('account:roleList')}</Title>
<Row>
<Col span={24}>
@ -237,6 +241,7 @@ function RoleList() {
showSizeChanger: true,
showTotal: (total) => { return t('Total') + `${total}` }
}}
onChange={(pagination) => { onSearchClick(pagination.current) }}
columns={roleListColumns} dataSource={roleAllList}
/>
</Col>

@ -1,12 +1,11 @@
import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, List, Table, Button } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined, AuditOutlined } from "@ant-design/icons";
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, List, Table } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
import { DATE_FORMAT } from "@/config";
import { TableExportBtn } from "@/components/Data";
import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage";
@ -15,7 +14,6 @@ const planListColumns = [
title: "团名",
key: "GRI_No",
dataIndex: "GRI_No",
// sorter: (a, b) => b.GRI_No - a.GRI_No,
},
{
title: "组团人",
@ -48,88 +46,69 @@ const planListColumns = [
dataIndex: "ToCity",
},
{
title: "航",
title: "航",
key: "FlightNo",
dataIndex: "FlightNo",
},
{
title: "起飞时间",
key: "FlightStart",
dataIndex: "FlightStart",
key: "FlightTimeStart",
dataIndex: "FlightTimeStart",
render: text => formatColonTime(text),
},
{
title: "落地时间",
key: "FlightEnd",
dataIndex: "FlightEnd",
key: "FlightTimeEnd",
dataIndex: "FlightTimeEnd",
render: text => formatColonTime(text),
},
{
title: "出票处理",
key: "TicketIssued",
dataIndex: "TicketIssued",
render: (text, record) => record.TicketIssuedName,
},
{
title: "计划状态",
key: "FlightStatus",
dataIndex: "FlightStatus",
render: (text, record) => record.FlightStatusName,
title: "是否出票",
key: "COLD_PlanVEI_SN",
dataIndex: "COLD_PlanVEI_SN",
render: (text, record) => "否",
},
{
title: "操作",
key: "FlightInfo",
dataIndex: "FlightInfo",
render: (text, record) => <NavLink to={`/airticket/plan/${record.COLI_SN}/${record.GRI_SN}`}>{"编辑"}</NavLink>,
render: (text, record) => <NavLink to={`/airticket/plan/${record.COLI_SN}`}>{"编辑"}</NavLink>,
},
];
const Airticket = props => {
const href = useHref();
const navigate = useNavigate();
const { phonenumber } = useParams();
const { travelAgencyId } = usingStorage();
const [getPlanList, planList, loading] = airTicketStore(state => [state.getPlanList, state.planList, state.loading]);
const [phone_number, setPhone_number] = useState(phonenumber);
const showTotal = total => `合计 ${total} `;
useEffect(() => {
!planList && getPlanList(travelAgencyId, "", dayjs().startOf("M").format(DATE_FORMAT), dayjs().add(3, "M").endOf("M").format(DATE_FORMAT), "-1", "-1");
}, []);
useEffect(() => {}, []);
const oncall = () => {};
return (
<Space direction="vertical" style={{ width: "100%" }}>
<SearchForm
initialValue={{
dates: [dayjs().startOf("M"), dayjs().endOf("M")],
}}
fieldsConfig={{
shows: ["referenceNo", "dates"],
fieldProps: {
referenceNo: { label: "搜索计划" },
dates: { label: "出发日期" },
},
}}
onSubmit={(err, formVal, filedsVal) => {
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime);
}}
/>
<Row>
<Col md={20} lg={20} xxl={20}>
<SearchForm
initialValue={{
dates: [dayjs().startOf("M"), dayjs().add(3, "M").endOf("M")],
}}
fieldsConfig={{
shows: ["referenceNo", "dates", "airticket_state", "plan_state"],
fieldProps: {
referenceNo: { label: "搜索计划" },
dates: { label: "出发日期", col: 8 },
},
}}
onSubmit={(err, formVal, filedsVal) => {
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
}}
/>
</Col>
<Col md={4} lg={4} xxl={4}>
<Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoice`)}>
报账
</Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoicepaid`)}>
汇款记录
</Button>
</Space>
</Col>
</Row>
<Row gutter={16}>
<Col md={24} lg={24} xxl={24}>
<Table bordered={true} rowKey="id" columns={planListColumns} dataSource={planList} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
<TableExportBtn btnTxt="导出计划" label={`机票计划`} {...{ columns: planListColumns, dataSource: planList }} />
</Col>
<Col md={24} lg={24} xxl={24}></Col>
</Row>

@ -1,255 +0,0 @@
import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Steps, Statistic, Col, Row, Space, Checkbox, Table, Button, App, Typography } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, FrownTwoTone, LikeTwoTone } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
import BackBtn from "@/components/BackBtn";
import { TableExportBtn } from "@/components/Data";
import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage";
const Invoice = props => {
const navigate = useNavigate();
const { notification } = App.useApp();
const { travelAgencyId } = usingStorage();
const [getVEIFlightBill, vEIFlightBill, loading, postVEIFlightBillSubmit] = airTicketStore(state => [state.getVEIFlightBill, state.vEIFlightBill, state.loading, state.postVEIFlightBillSubmit]);
const showTotal = total => `合计 ${total} `;
const [selectedValues, setSelectedValues] = useState([]);
const vEIFlightBillColumns = [
{
title: "团名",
key: "GRI_No",
dataIndex: "GRI_No",
render: (text, record) => (
<Typography.Text title={record.Memo}>
{record.GRI_No} {record.WL}
</Typography.Text>
),
},
{
title: "状态",
key: "CostType",
dataIndex: "CostType",
},
{
title: "出发日期",
key: "StartDate",
dataIndex: "StartDate",
sorter: (a, b) => {
const dateA = new Date(a.StartDate);
const dateB = new Date(b.StartDate);
return dateB.getTime() - dateA.getTime();
},
},
{
title: "出发",
key: "FromCity",
dataIndex: "FromCity",
render: (text, record) => (record.CostType == "出票" ? `${record.FromCity}` : "-"),
},
{
title: "抵达",
key: "ToCity",
dataIndex: "ToCity",
render: (text, record) => (record.CostType == "出票" ? `${record.ToCity}` : "-"),
},
{
title: "航班",
key: "FlightNo",
dataIndex: "FlightNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "PNR",
key: "PNR",
dataIndex: "PNR",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "票号",
key: "TicketNo",
dataIndex: "TicketNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "机票类型",
key: "FlightType",
dataIndex: "FlightType",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "机票价格",
children: [
{
title: vEIFlightBill && vEIFlightBill.reduce((acc, curr) => acc + curr.Cost, 0),
dataIndex: "Cost",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
],
key: "Cost",
},
{
title: "服务费",
children: [
{
title: vEIFlightBill && vEIFlightBill.reduce((acc, curr) => acc + curr.ServiceFee, 0),
dataIndex: "ServiceFee",
},
],
},
{
title: "折扣",
key: "Discount",
dataIndex: "Discount",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "审核状态",
children: [
{
title: (
<Button type="link" onClick={() => checkALL()}>
全选
</Button>
),
dataIndex: "CheckStatus", //2
render: (text, record) =>
record.CheckStatus < 2 ? (
<Checkbox onChange={event => handleCheckboxChange(event, record)} checked={checkboxStates(record.CLC_SN)}>
待提交
</Checkbox>
) : (
<Steps
size="small"
current={record.CheckStatus - 1}
items={[
{
title: "提交账单",
},
{
title: "顾问审核",
},
{
title: "财务处理",
},
{
title: "账单支付",
},
]}
/>
),
},
],
sorter: (a, b) => {
return b.CheckStatus - a.CheckStatus;
},
},
];
//
const checkboxStates = CLC_SN => {
return selectedValues.some(v => v.CLC_SN === CLC_SN);
};
// checkbox
const handleCheckboxChange = (event, data) => {
const value = { CLC_SN: data.CLC_SN, WL: data.WL, OPI_SN: data.OPI_SN, OPI_Email: data.OPI_Email, GRI_SN: data.GRI_SN, GRI_Name: data.GRI_Name, Cost: data.Cost, ServiceFee: data.ServiceFee };
if (event.target.checked) {
setSelectedValues([...selectedValues, value]); //
} else {
setSelectedValues(selectedValues.filter(v => v.CLC_SN !== value.CLC_SN)); //
}
};
//
const postInvoice = () => {
postVEIFlightBillSubmit(travelAgencyId, selectedValues)
.then(() => {
notification.success({
message: `成功`,
description: "账单提交成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
setSelectedValues([]); //
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
};
//
const checkALL = () => {
if (isEmpty(vEIFlightBill)) return;
const allChecked = selectedValues.length === vEIFlightBill.filter(item => item.CheckStatus < 2).length;
setSelectedValues(
allChecked
? []
: vEIFlightBill
.filter(item => item.CheckStatus < 2)
.map(item => ({ CLC_SN: item.CLC_SN, WL: item.WL, OPI_SN: item.OPI_SN, OPI_Email: item.OPI_Email, GRI_SN: item.GRI_SN, GRI_Name: item.GRI_Name, Cost: item.Cost, ServiceFee: item.ServiceFee }))
); //
};
useEffect(() => {}, []);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row>
<Col md={20} lg={20} xxl={20}>
<SearchForm
initialValue={{
dates: [dayjs().startOf("M"), dayjs().endOf("M")],
}}
fieldsConfig={{
shows: ["referenceNo", "dates", "invoiceCheckStatus"],
fieldProps: {
referenceNo: { label: "搜索计划" },
dates: { label: "出发日期", col: 8 },
},
}}
onSubmit={(err, formVal, filedsVal) => {
getVEIFlightBill(travelAgencyId, formVal.referenceNo, formVal.invoiceCheckStatus, formVal.startdate, formVal.endtime);
}}
/>
</Col>
<Col md={4} lg={4} xxl={4}>
<BackBtn to={"/airticket"} />
</Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered={true} rowKey="CLC_SN" columns={vEIFlightBillColumns} dataSource={vEIFlightBill} loading={loading} pagination={{ defaultPageSize: 100, showTotal: showTotal }} />
<TableExportBtn btnTxt="导出账单" label={`机票账单`} {...{ columns: vEIFlightBillColumns, dataSource: vEIFlightBill }} />
</Col>
<Col md={24} lg={24} xxl={24}></Col>
</Row>
<Row>
<Col md={24} lg={18} xxl={18}></Col>
<Col md={24} lg={2} xxl={2}>
<Statistic title="已选机票价格" value={selectedValues.reduce((acc, curr) => acc + curr.Cost, 0)} />
</Col>
<Col md={24} lg={2} xxl={2}>
<Statistic title="已选服务费" value={selectedValues.reduce((acc, curr) => acc + curr.ServiceFee, 0)} />
</Col>
<Col md={24} lg={2} xxl={2}>
<Button type="primary" size="large" onClick={postInvoice} disabled={selectedValues.length == 0}>
提交账单
</Button>
</Col>
</Row>
</Space>
);
};
export default Invoice;

@ -1,137 +0,0 @@
import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, Checkbox, Table, Button, App } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, FrownTwoTone, LikeTwoTone } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime, formatDate, isNotEmpty } from "@/utils/commons";
import { DATE_FORMAT } from "@/config";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
import BackBtn from "@/components/BackBtn";
import { TableExportBtn } from "@/components/Data";
import useInvoiceStore from "@/stores/Invoice";
import { fetchInvoicePaidDetail } from "@/stores/Invoice";
import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage";
const InvoicePaid = props => {
const navigate = useNavigate();
const { notification } = App.useApp();
const { travelAgencyId } = usingStorage();
const [invoicePaidDetail, setInvoicePaidDetail] = useState([]);
const [invoiceNO, setInvoiceNO] = useState([]); //
const [loading, invoicePaid, fetchInvoicePaid] = useInvoiceStore(state => [state.loading, state.invoicePaid, state.fetchInvoicePaid]);
const showTotal = total => `Total ${total} items`;
const showTotal_detail = total => `Total ${total} items`;
useEffect(() => {
// fetchInvoicePaid(travelAgencyId, "", dayjs().subtract(2, "M").startOf("M").format(DATE_FORMAT), dayjs().endOf("M").format(DATE_FORMAT));
}, []);
const invoicePaidColumns = [
{
title: "编号",
dataIndex: "fl_finaceNo",
key: "fl_finaceNo",
},
{
title: "报账日期",
key: "fl_adddate",
dataIndex: "fl_adddate",
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
},
{
title: "团数",
key: "fcount",
dataIndex: "fcount",
},
{
title: "总额",
key: "pSum",
dataIndex: "pSum",
//render: (text, record) => (isNotEmpty(record.GMD_Currency) ? record.GMD_Currency + " " + text : text),
},
{
title: "查看",
key: "pSum",
dataIndex: "pSum",
render: (text, record) => (
<Button
type="link"
onClick={() => {
fetchInvoicePaidDetail(travelAgencyId, record.key).then(res => setInvoicePaidDetail(res));
setInvoiceNO(record.fl_finaceNo);
}}>
查看明细
</Button>
),
},
];
const invoicePaidDetailColumns = [
{
title: "团号",
dataIndex: "fl2_GroupName",
key: "fl2_GroupName",
},
{
title: "金额",
key: "fl2_price",
dataIndex: "fl2_price",
},
{
title: "报账日期",
key: "fl2_ArriveDate",
dataIndex: "fl2_ArriveDate",
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
},
{
title: "顾问",
dataIndex: "fl2_wl",
key: "fl2_wl",
},
];
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row>
<Col md={20} lg={20} xxl={20}>
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
}}
fieldsConfig={{
shows: ["dates"],
fieldProps: {
dates: { col: 10, label: "报账日期" },
},
}}
onSubmit={(err, formVal) => {
fetchInvoicePaid(travelAgencyId, "", formVal.startdate, formVal.enddate);
setInvoicePaidDetail([]);
}}
/>
</Col>
<Col md={4} lg={4} xxl={4}>
<BackBtn to={"/airticket"} />
</Col>
</Row>
<Row>
<Col md={24} lg={16} xxl={16}>
<Divider orientation="left">汇款列表</Divider>
<Table bordered columns={invoicePaidColumns} dataSource={invoicePaid} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
</Col>
<Col md={24} lg={4} xxl={4}></Col>
</Row>
<Row>
<Col md={24} lg={20} xxl={20}>
<Divider orientation="left">账单明细 {invoiceNO}</Divider>
<Table bordered columns={invoicePaidDetailColumns} dataSource={invoicePaidDetail} pagination={{ defaultPageSize: 100, showTotal: showTotal_detail }} />
<TableExportBtn btnTxt="导出账单明细" label={`机票账单`} {...{ columns: invoicePaidDetailColumns, dataSource: invoicePaidDetail }} />
</Col>
<Col md={24} lg={4} xxl={4}></Col>
</Row>
</Space>
);
};
export default InvoicePaid;

@ -1,332 +1,91 @@
import { useState, useEffect } from "react";
import { Checkbox, Divider, DatePicker, Modal, Form, Input, Col, Row, Space, Collapse, Table, Button, Select, App, Popconfirm, Switch, Radio, List } from "antd";
import { PhoneOutlined, FrownTwoTone, LikeTwoTone, ArrowUpOutlined, ArrowDownOutlined, PlusOutlined } from "@ant-design/icons";
import { Grid, Divider, Layout, Form, Input, Col, Row, Space, Collapse, Table, Button } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined, ArrowUpOutlined, ArrowDownOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons";
import { OFFICEWEBVIEWERURL } from "@/config";
import dayjs from "dayjs";
import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage";
import BackBtn from "@/components/BackBtn";
const AirticketPlan = props => {
const { coli_sn, gri_sn } = useParams();
const { travelAgencyId, loginToken, userId } = usingStorage();
const [
getPlanDetail,
planDetail,
getGuestList,
guestList,
loading,
postFlightDetail,
postFlightCost,
deleteFlightCost,
getVeiPlanChange,
veiPlanChangeTxt,
postVeiFlightPlanConfirm,
ticketIssuedNotifications,
delete_flight_info,
getAirPortList,
airPortList,
airLineList,
] = airTicketStore(state => [
state.getPlanDetail,
state.planDetail,
state.getGuestList,
state.guestList,
state.loading,
state.postFlightDetail,
state.postFlightCost,
state.deleteFlightCost,
state.getVeiPlanChange,
state.veiPlanChangeTxt,
state.postVeiFlightPlanConfirm,
state.ticketIssuedNotifications,
state.delete_flight_info,
state.getAirPortList,
state.airPortList,
state.airLineList,
]);
const reservationUrl = `https://p9axztuwd7x8a7.mycht.cn/Service_BaseInfoWeb/FlightPlanDocx?GRI_SN=${gri_sn}&VEI_SN=${travelAgencyId}&token=${loginToken}`;
const { coli_sn } = useParams();
const { travelAgencyId, loginToken } = usingStorage();
const [getPlanDetail, planDetail, getGuestList, guestList, loading] = airTicketStore(state => [state.getPlanDetail, state.planDetail, state.getGuestList, state.guestList, state.loading]);
const reservationUrl = `https://p9axztuwd7x8a7.mycht.cn/Service_BaseInfoWeb/FlightPlanDocx?GRI_SN=${coli_sn}&VEI_SN=${travelAgencyId}`;
const reservationPreviewUrl = OFFICEWEBVIEWERURL + encodeURIComponent(reservationUrl);
const [form] = Form.useForm();
const { notification } = App.useApp();
//console.log(reservationPreviewUrl);
//
const guestList_select = () => {
return (
guestList &&
guestList.map(item => {
return { label: `${item.MEI_Name} , ${item.MEI_PassportNo}`, value: `${item.MEI_Name} , ${item.MEI_PassportNo} , ${item.MEI_Country} , ${item.MEI_Gender} , ${item.MEI_age} , ${item.MEI_Birthday}` };
})
);
};
const guestList_OnChange = e => {
ticket_form.setFieldsValue({ Memo: `${e.target.value}` });
};
//
const airPortList_select = () => {
return (
airPortList &&
airPortList.map(item => {
return { label: `${item.AirPort_Code} - ${item.AirPort_Name}`, value: item.AirPort_Name };
})
);
};
//
const costListColumns = [
{
title: "客人信息/备注",
key: "Memo",
dataIndex: "Memo",
},
{
title: "状态",
key: "CostType",
dataIndex: "CostType",
},
{
title: "票号",
key: "TicketNo",
dataIndex: "TicketNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "PNR",
key: "PNR",
dataIndex: "PNR",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "机票类型",
key: "FlightType",
dataIndex: "FlightType",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "机票价格",
key: "Cost",
dataIndex: "Cost",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "服务费",
key: "ServiceFee",
dataIndex: "ServiceFee",
},
{
title: "折扣",
key: "Discount",
dataIndex: "Discount",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "编辑",
key: "CLC_SN",
dataIndex: "CLC_SN",
render: (text, record) =>
record.CheckStatus <= 2 ? (
<Space>
<a onClick={() => showModal(record)}>编辑</a>
<Popconfirm title="删除" description="请确认是否删除?" onConfirm={() => handleDelete(record.CLC_SN)} okText="是" cancelText="否">
<Button danger type="link">
删除
</Button>
</Popconfirm>
</Space>
) : (
record.CheckStatusName
),
},
];
console.log(reservationPreviewUrl);
const Airticket_form = airInfo => {
// const airInfo = props.airInfo;
// const [airinfo_form] = Form.useForm();
const Airticket_form = props => {
const aitInfo = props.airInfo;
return (
<>
<Form
// form={airinfo_form}
name={"ticket_form_" + airInfo.id}
name="basic"
labelCol={{
span: 6,
span: 8,
}}
wrapperCol={{
span: 16,
}}
initialValues={{ ...airInfo, StartDate: dayjs(airInfo.StartDate) }}
onFinish={values => {
postFlightDetail(airInfo.CLF_SN, airInfo.GRI_SN, airInfo.VEI_SN, airInfo, values)
.then(() => {
notification.success({
message: `成功`,
description: "机票信息保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
style={{
maxWidth: 600,
}}
autoComplete="off">
<Divider orientation="left">航班信息</Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label="出发日期、航班、城市、时间" required>
<Space>
<Form.Item name="StartDate" noStyle rules={[{ required: true, message: "请输入出发日期!" }]}>
<DatePicker
style={{
minWidth: 160,
}}
/>
</Form.Item>
<Form.Item name="FlightNo" noStyle rules={[{ required: true, message: "请输入航班号!" }]}>
<Input placeholder="航班号" />
</Form.Item>
<Form.Item name="FromCity" noStyle rules={[{ required: true, message: "请输入出发城市!" }]}>
<Input placeholder="出发" />
</Form.Item>
<Form.Item name="FlightStart" noStyle rules={[{ required: true, message: "请输入出发时间!" }]}>
<Input placeholder="出发时间" />
</Form.Item>
-
<Form.Item name="ToCity" noStyle rules={[{ required: true, message: "请输入抵达城市!" }]}>
<Input placeholder="抵达" />
</Form.Item>
<Form.Item name="FlightEnd" noStyle rules={[{ required: true, message: "请输入抵达时间!" }]}>
<Input placeholder="抵达时间" />
</Form.Item>
</Space>
</Form.Item>
<Form.Item label="机场、航站楼、仓位、行李重量" required>
<Space>
<Form.Item name="FromAirport" noStyle rules={[{ required: true, message: "请输入出发机场!" }]}>
<Select
showSearch
placeholder="出发机场"
style={{
minWidth: 160,
}}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
options={airPortList_select()}
/>
</Form.Item>
<Form.Item name="FromTerminal" noStyle rules={[{ required: true, message: "请输入出发航站楼!" }]}>
<Input placeholder="航站楼" />
</Form.Item>
-
<Form.Item name="ToAirport" noStyle rules={[{ required: true, message: "请输入抵达机场!" }]}>
<Select
showSearch
placeholder="抵达机场"
style={{
minWidth: 160,
}}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
options={airPortList_select()}
/>
</Form.Item>
<Form.Item name="ToTerminal" noStyle rules={[{ required: true, message: "请输入抵达航站楼!" }]}>
<Input placeholder="航站楼" />
</Form.Item>
<Form.Item name="FlightCabin" noStyle rules={[{ required: true, message: "请输入仓位!" }]}>
<Input placeholder="仓位" />
</Form.Item>
<Form.Item name="Baggage" noStyle>
<Input placeholder="行李说明 20KG" />
</Form.Item>
</Space>
</Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Space direction="vertical">
<Form.Item name="TicketIssued">
<Switch checkedChildren="已处理" unCheckedChildren="未处理" />
</Form.Item>
<Button type="primary" htmlType="submit">
1. 保存机票信息
</Button>
</Space>
</Col>
</Row>
<Divider orientation="left">出票信息</Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Table bordered={true} rowKey="CLC_SN" columns={costListColumns} dataSource={airInfo.Flightcost_AsJOSN} loading={loading} pagination={false} />
</Col>
<Col md={24} lg={4} xxl={4}>
<Space direction="vertical">
<Button type="primary" onClick={() => showModal(airInfo)}>
2. 添加出票信息
</Button>
</Space>
</Col>
</Row>
</Form>
<Form
labelCol={{
span: 6,
}}
wrapperCol={{
span: 16,
}}
initialValues={{ ...airInfo }}
onFinish={values => {
ticketIssuedNotifications(userId, airInfo.CLF_SN, airInfo.OPI_SN, values.FlightMemo_messages)
.then(() => {
notification.success({
message: `成功`,
description: "提醒信息已发出!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
//airinfo_form.setFieldValue('FlightMemo_messages','')
})
.catch(() => {
notification.error({
message: `错误`,
description: "提醒失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
initialValues={{
remember: true,
}}
// onFinish={onFinish}
// onFinishFailed={onFinishFailed}
autoComplete="off">
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label="提醒信息" name="FlightMemo_messages">
<Input placeholder="没有提醒请留空,信息会抄送给上下站地接" />
</Form.Item>
<Form.Item label="已发提醒" name="FlightMemo">
<Input.TextArea rows={4} readOnly disabled />
</Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button type="primary" htmlType="submit">
3. 通知顾问
</Button>
</Col>
</Row>
<Form.Item label="日期" name="username" rules={[{ required: true }]}>
<Input value={aitInfo.StartDate} />
</Form.Item>
<Form.Item label="城市" name="password" rules={[{ required: true }]}>
<Space.Compact>
<Input placeholder="出发" prefix={<ArrowUpOutlined />} value={aitInfo.FromCity} />
<Input placeholder="抵达" prefix={<ArrowDownOutlined />} value={aitInfo.ToCity} />
</Space.Compact>
</Form.Item>
<Form.Item label="航班" name="password" rules={[{ required: true }]}>
<Space.Compact>
<Input placeholder="航空公司" />
<Input placeholder="航班号" value={aitInfo.FlightNo} />
</Space.Compact>
</Form.Item>
<Form.Item label="出发" name="password" rules={[{ required: true }]}>
<Space.Compact>
<Input placeholder="机场" />
<Input placeholder="航站楼" />
<Input placeholder="出发时间" value={formatColonTime(aitInfo.FlightTimeStart)} />
</Space.Compact>
</Form.Item>
<Form.Item label="抵达" name="password" rules={[{ required: true }]}>
<Space.Compact>
<Input placeholder="机场" />
<Input placeholder="航站楼" />
<Input placeholder="抵达时间" value={formatColonTime(aitInfo.FlightTimeEnd)} />
</Space.Compact>
</Form.Item>
<Form.Item label="仓位和行李" name="password">
<Space.Compact>
<Input placeholder="仓位" />
<Input placeholder="行李" />
</Space.Compact>
</Form.Item>
{/* <Form.Item
wrapperCol={{
offset: 8,
span: 16,
}}>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item> */}
</Form>
<Divider />
<Table bordered={true} rowKey="id" columns={guestListColumns} dataSource={guestList} loading={loading} pagination={false} />
</>
);
};
@ -336,397 +95,102 @@ const AirticketPlan = props => {
? planDetail.map(item => {
return {
key: item.id,
label: `${item.StartDate} ${item.FlightNo}(${item.FromAirport}${item.FlightStart}-${item.ToAirport}${item.FlightEnd})(${item.FlightCabin})`,
extra: (
<Popconfirm
title="请确认要删除航班记录"
description=""
onConfirm={() => {
delete_flight_info(item.CLF_SN); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" size="small" disabled={item.Flightcost_AsJOSN.length == 0 ? false : true}>
删除
</Button>
</Popconfirm>
),
children: Airticket_form(item),
label: `${item.StartDate} 计划: ${item.FlightInfo}`,
children: <Airticket_form airInfo={item} />,
};
})
: [];
};
// begin
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOpen_confirmInfo, setisModalOpen_confirmInfo] = useState(false);
const [isTicketType, setisTicketType] = useState(true);
const [isAddNew, setisAddNew] = useState(true); //
const [ticket_form] = Form.useForm();
const [confirmInfo_form] = Form.useForm();
const showModal = ticket => {
setIsModalOpen(true);
ticket_form.resetFields();
if (isEmpty(ticket.CostType)) ticket.CostType = "出票";
ticket.CostType == "出票" ? setisTicketType(true) : setisTicketType(false); //
isEmpty(ticket.CLC_SN) ? setisAddNew(true) : setisAddNew(false); //
ticket_form.setFieldsValue(ticket);
if (isEmpty(ticket.Memo)) ticket_form.setFieldsValue({ Memo: "" });
};
const handleOk = (close_modal = true) => {
ticket_form
.validateFields()
.then(values => {
//
console.log("Received values of form: ", values);
postFlightCost(values)
.then(() => {
notification.success({
message: `成功`,
description: "保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
getPlanDetail(travelAgencyId, gri_sn);
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
if (close_modal) setIsModalOpen(false);
})
.catch(info => {
console.log("Validate Failed:", info);
});
};
const handleCancel = () => {
ticket_form.resetFields();
setIsModalOpen(false);
};
const handleDelete = CLC_SN => {
deleteFlightCost(CLC_SN)
.then(() => {
notification.success({
message: `成功`,
description: "删除成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
getPlanDetail(travelAgencyId, gri_sn);
})
.catch(() => {
notification.error({
message: `错误`,
description: "删除失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
};
const onChangeType = value => {
if (value == "出票") {
setisTicketType(true);
} else {
setisTicketType(false);
}
};
//
const showModal_confirmInfo = ConfirmInfo => {
setisModalOpen_confirmInfo(true);
confirmInfo_form.setFieldsValue({ ConfirmInfo: ConfirmInfo });
};
const handleCancel_confirmInfo = () => {
setisModalOpen_confirmInfo(false);
confirmInfo_form.resetFields();
};
const handleOk_confirmInfo = () => {
confirmInfo_form
.validateFields()
.then(values => {
console.log("Received values of form: ", values.ConfirmInfo);
postVeiFlightPlanConfirm(travelAgencyId, gri_sn, userId, values.ConfirmInfo)
.then(() => {
notification.success({
message: `成功`,
description: "保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
getVeiPlanChange(travelAgencyId, gri_sn);
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
confirmInfo_form.resetFields();
setisModalOpen_confirmInfo(false);
})
.catch(info => {
console.log("Validate Failed:", info);
});
};
// end
const guestListColumns = [
{
title: "姓名",
key: "MEI_Name",
dataIndex: "MEI_Name",
},
{
title: "证件类型",
key: "MEI_PassportType",
dataIndex: "MEI_PassportType",
},
{
title: "证件号",
key: "MEI_PassportNo",
dataIndex: "MEI_PassportNo",
},
{
title: "证件有效期",
key: "MEI_PassportValidDate",
dataIndex: "MEI_PassportValidDate",
},
{
title: "性别",
key: "MEI_Gender",
dataIndex: "MEI_Gender",
},
{
title: "年龄",
key: "MEI_age",
dataIndex: "MEI_age",
},
{
title: "国籍",
key: "MEI_Country",
dataIndex: "MEI_Country",
},
{
title: "票号",
key: "MEI_SN",
dataIndex: "MEI_SN",
},
{
title: "PNR",
key: "MEI_SN",
dataIndex: "MEI_SN",
},
{
title: "机票费用RMB含基建和税",
key: "MEI_SN",
dataIndex: "MEI_SN",
},
{
title: "折扣",
key: "MEI_SN",
dataIndex: "MEI_SN",
},
{
title: "手续费",
key: "MEI_SN",
dataIndex: "MEI_SN",
},
{
title: "机票类型(成人/儿童/婴儿)",
key: "MEI_SN",
dataIndex: "MEI_SN",
},
];
useEffect(() => {
getPlanDetail(travelAgencyId, gri_sn); //
getGuestList(coli_sn); //
getVeiPlanChange(travelAgencyId, gri_sn); //
getAirPortList(); //
getPlanDetail(travelAgencyId, coli_sn);
getGuestList(travelAgencyId, coli_sn);
console.log(detail_items());
}, []);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row>
<Col md={20} lg={20} xxl={20}></Col>
<Col md={4} lg={4} xxl={4}>
<BackBtn to={"/airticket"} />
</Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24} style={{ height: "100%" }}>
<iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl + "&v=" + Math.random()} style={{ width: "100%", height: "600px" }}></iframe>
{/* <iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl} frameBorder="0" style={{ width: "100%", height: "600px" }}></iframe> */}
<Button type="link" target="_blank" href={reservationUrl}>
下载
</Button>
</Col>
</Row>
<Row>
<Divider orientation="center">{planDetail ? `${planDetail[0].GRI_No} - ${planDetail[0].WL}` : ""}</Divider>
<Divider orientation="left">出票信息</Divider>
<Col md={24} lg={24} xxl={24}>
<Collapse items={detail_items()} />
</Col>
<Col md={24} lg={24} xxl={24}>
<br />
<p style={{ textAlign: "right" }}>
<Popconfirm
title="请确认要增加航班记录"
description=""
onConfirm={async () => {
await postFlightDetail("", gri_sn, travelAgencyId, { FlightNo: "新的记录", FlightStatus: 1 }, []); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" icon={<PlusOutlined />}>
新增航班记录
</Button>
</Popconfirm>
</p>
</Col>
</Row>
<Row>
<Divider orientation="left">计划变更</Divider>
<Col md={24} lg={12} xxl={12}>
<Space direction="vertical" style={{ width: "90%" }}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ChangeText} />
<Button
type="primary"
onClick={() => {
showModal_confirmInfo(veiPlanChangeTxt && veiPlanChangeTxt.ChangeText);
}}
disabled={isEmpty(veiPlanChangeTxt) || isEmpty(veiPlanChangeTxt.ChangeText)}>
确认变更
</Button>
</Space>
</Col>
<Col md={24} lg={12} xxl={12}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ConfirmInfo} />
</Col>
</Row>
<Modal title="变更" open={isModalOpen_confirmInfo} onOk={handleOk_confirmInfo} onCancel={handleCancel_confirmInfo}>
<Form
form={confirmInfo_form}
labelCol={{
span: 5,
}}>
<Form.Item label="确认信息" name="ConfirmInfo" rules={[{ required: true }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="费用信息"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
okText="保存"
cancelText="关闭"
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
{isAddNew ? (
<>
<Button type="primary" onClick={() => handleOk(false)}>
添加并继续新增
</Button>{" "}
<Button type="primary" onClick={() => handleOk(true)}>
添加并关闭
</Button>
</>
) : (
<OkBtn />
)}
</>
)}>
<Form
form={ticket_form}
labelCol={{
span: 5,
}}>
<Form.Item label="状态" name="CostType">
<Select
style={{
width: 160,
}}
onChange={onChangeType}
options={[
{
value: "出票",
label: "出票",
},
{
value: "改签",
label: "改签",
},
{
value: "退票",
label: "退票",
},
]}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="PNR" name="PNR" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="票号" name="TicketNo" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label="机票类型" name="FlightType" rules={[{ required: true }]}>
<Select
style={{
width: 160,
}}
options={[
{
value: "成人",
label: "成人",
},
{
value: "儿童",
label: "儿童",
},
{
value: "婴儿",
label: "婴儿",
},
]}
/>
</Form.Item>
<Form.Item label="机票价格" name="Cost" rules={[{ required: true }]}>
<Input
placeholder="含基建和税"
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
</>
)}
<Form.Item label="服务费" name="ServiceFee" rules={[{ required: true }]}>
<Input
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="折扣" name="Discount" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
placeholder="如 0.9"
/>
</Form.Item>
<Form.Item label="选择客人" name="MEI_Name66">
<Radio.Group
onChange={e => guestList_OnChange(e)}
style={{
minWidth: 320,
}}>
<List
bordered
dataSource={guestList_select()}
renderItem={item => (
<List.Item>
<Radio value={item.value}>{item.label}</Radio>
</List.Item>
)}></List>
</Radio.Group>
{/* <Select onChange={value => guestList_OnChange(value)} options={guestList_select()} placeholder="如果列表里面没有客人信息,请手动录到备注里" /> */}
</Form.Item>
</>
)}
<Form.Item label="客人信息/备注" name="Memo">
<Input.TextArea rows={4} disabled={isTicketType} />
</Form.Item>
<Form.Item name="CLF_SN" hidden>
<input />
</Form.Item>
<Form.Item name="GRI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="VEI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="CLC_SN" hidden>
<input />
</Form.Item>
</Form>
</Modal>
</Space>
);
};
export default AirticketPlan;

@ -6,13 +6,13 @@ import * as config from '@/config';
import { getFeedbackDetail, getCustomerFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from '@/components/BackBtn';
import { usingStorage } from '@/hooks/usingStorage';
import {ImageUploader} from '@/components/ImageUploader';
const { Title, Text, Paragraph } = Typography;
function Detail() {
const navigate = useNavigate();
const { GRI_SN, RefNo, CII_SN } = useParams();
const {travelAgencyId, loginToken} = usingStorage();
const {travelAgencyId, token} = usingStorage();
const desc = ['none', 'Unacceptable', 'Poor', 'Fair', 'Very Good', 'Excellent'];
const { notification } = App.useApp();
const [form] = Form.useForm();
@ -21,11 +21,6 @@ function Detail() {
const [feedbackReview, setFeedbackReview] = useState({});
const [feedbackImages, setFeedbackImages] = useState([]);
const [feedbackInfo, setFeedbackInfo] = useState({});
const [ossKey, setOssKey] = useState('');
useEffect(() => {
const key = `ghh/${GRI_SN}-${RefNo}/tourguide_image/travel-agency-${travelAgencyId}`;
setOssKey(key);
}, []);
useEffect(() => {
// console.info('Detail.useEffect: ' + GRI_SN);
@ -173,14 +168,17 @@ function Detail() {
name='ghhfile'
// accept="image/*"
multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${loginToken}`}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${token}`}
fileList={fileList}
listType='picture-card'
onChange={handleChange}
onRemove={handRemove}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload photos</div>
</div>
</Upload>
</Form.Item>
<ImageUploader osskey={ossKey} style={{margin: '16px'}}/>
<Form.Item
name='info_content'
rules={[

@ -6,13 +6,12 @@ import * as config from "@/config";
import { getFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
import {ImageUploader} from '@/components/ImageUploader';
const { Title, Text, Paragraph } = Typography;
function Detail() {
const navigate = useNavigate();
const { GRI_SN,RefNo } = useParams();
const {travelAgencyId, loginToken} = usingStorage();
const {travelAgencyId, token} = usingStorage();
const desc = ["none", "Unacceptable", "Poor", "Fair", "Very Good", "Excellent"];
const { notification } = App.useApp();
const [form] = Form.useForm();
@ -21,11 +20,6 @@ function Detail() {
const [feedbackReview, setFeedbackReview] = useState({});
const [feedbackImages, setFeedbackImages] = useState([]);
const [feedbackInfo, setFeedbackInfo] = useState({});
const [ossKey, setOssKey] = useState('');
useEffect(() => {
const key = `ghh/${GRI_SN}-${RefNo}/tourguide_image/travel-agency-${travelAgencyId}`;
setOssKey(key);
}, []);
useEffect(() => {
// console.info("Detail.useEffect: " + GRI_SN);
@ -172,14 +166,17 @@ function Detail() {
name="ghhfile"
// accept="image/*"
multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${loginToken}`}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${token}`}
fileList={fileList}
listType="picture-card"
onChange={handleChange}
onRemove={handRemove}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload photos</div>
</div>
</Upload>
</Form.Item>
<ImageUploader osskey={ossKey} />
<Form.Item
name="info_content"
rules={[

@ -1,21 +1,21 @@
import { useParams, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Button, Typography, Card, Form, Upload, Input, Divider, DatePicker, Select, App, Descriptions, Image } from "antd";
import { PlusOutlined,AuditOutlined } from "@ant-design/icons";
import { PlusOutlined } from "@ant-design/icons";
import { isNotEmpty } from "@/utils/commons";
import * as config from "@/config";
import dayjs from "dayjs";
import { fetchInvoiceDetail, postEditInvoiceDetail, postAddInvoice } from "@/stores/Invoice";
import { removeFeedbackImages } from "@/stores/Feedback";
import BackBtn from "@/components/BackBtn";
import { fetchInvoiceDetail, postEditInvoiceDetail, postAddInvoice } from '@/stores/Invoice';
import { removeFeedbackImages } from '@/stores/Feedback';
import BackBtn from '@/components/BackBtn';
import { usingStorage } from "@/hooks/usingStorage";
const { Title, Text } = Typography;
const { Title,Text } = Typography;
function Detail() {
const navigate = useNavigate();
const { GMDSN, GSN } = useParams();
const { userId, travelAgencyId, loginToken } = usingStorage();
const {userId, travelAgencyId, token} = usingStorage();
const [form] = Form.useForm();
const [dataLoading, setDataLoading] = useState(false);
const [edited, setEdited] = useState(true); //
@ -24,13 +24,13 @@ function Detail() {
const [invoicePicList, setInvoicePicList] = useState([]);
const [invoiceZDDetail, setInvoiceZDDetail] = useState([]);
const [invoiceCurrencyList, setInvoiceCurrencyList] = useState([]);
const [invoiceProductList, setInvoiceProductList] = useState([]);
const [invoiceGroupInfo, setInvoiceGroupInfo] = useState({});
const [invoiceZDDetail, setInvoiceZDDetail] = useState([]);
const [invoiceCurrencyList, setInvoiceCurrencyList] = useState([]);
const [invoiceProductList, setInvoiceProductList] = useState([]);
const [invoiceGroupInfo, setInvoiceGroupInfo] = useState({});
const [invoiceFormData, setInvoiceFormData] = useState({});
const [invoicekImages, setInvoicekImages] = useState([]);
const [invoiceFormData, setInvoiceFormData] = useState({});
const [invoicekImages, setInvoicekImages] = useState([]);
useEffect(() => {
console.info("Detail.useEffect: " + GMDSN + "/" + GSN);
@ -40,12 +40,12 @@ function Detail() {
function defaultShow() {
setDataLoading(true);
fetchInvoiceDetail(travelAgencyId, GMDSN, GSN)
fetchInvoiceDetail(travelAgencyId, GMDSN, GSN)
.then(json => {
setInvoiceZDDetail(json.invoiceZDDetail);
setInvoiceGroupInfo(json.invoiceGroupInfo);
setInvoiceProductList(json.invoiceProductList);
setInvoiceCurrencyList(json.invoiceCurrencyList);
setInvoiceZDDetail(json.invoiceZDDetail);
setInvoiceGroupInfo(json.invoiceGroupInfo);
setInvoiceProductList(json.invoiceProductList);
setInvoiceCurrencyList(json.invoiceCurrencyList);
let ZDDetail = json.invoiceZDDetail;
if (isNotEmpty(ZDDetail)) {
@ -53,8 +53,8 @@ function Detail() {
const formData = ZDDetail.map((data, index) => {
if (data.GMD_Dealed == false && arrLen == index + 1) {
//
const _formData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "", info_gmdsn: data.GMD_SN };
setInvoiceFormData(_formData);
const _formData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "", info_gmdsn: data.GMD_SN };
setInvoiceFormData(_formData);
return _formData;
}
});
@ -79,7 +79,7 @@ function Detail() {
});
}
if (data.GMD_Dealed == false && arrLen == index + 1) {
setInvoicekImages(picList);
setInvoicekImages(picList);
}
return picList;
});
@ -99,7 +99,7 @@ function Detail() {
});
}
const fileList = invoicekImages;
const fileList = (invoicekImages);
//
let arrimg = [];
if (isNotEmpty(fileList)) {
@ -120,8 +120,8 @@ function Detail() {
if (fieldVaule) {
postEditInvoiceDetail(userId, fieldVaule.info_gmdsn, fieldVaule.info_Currency, fieldVaule.info_money, fieldVaule.info_date, fieldVaule.info_images, "").then(data => {
// console.log(data);
let param = { info_money: fieldVaule.info_money, info_Currency: fieldVaule.info_Currency, info_date: fieldVaule.info_date };
setInvoiceFormData(param);
let param = { info_money: fieldVaule.info_money, info_Currency: fieldVaule.info_Currency, info_date: fieldVaule.info_date };
setInvoiceFormData(param);
if (data.errcode == 0) {
setEdited(true);
notification.success({
@ -136,7 +136,7 @@ function Detail() {
};
const handleChange = info => {
//console.log(info);
console.log(info);
let newFileList = [...info.fileList];
newFileList = newFileList.map(file => {
if (file.response && file.response.result) {
@ -144,11 +144,11 @@ function Detail() {
}
return file;
});
setInvoicekImages(newFileList);
setInvoicekImages(newFileList);
};
const handRemove = info => {
//console.log(info);
console.log(info);
removeFeedbackImages(info.url);
return true;
};
@ -167,7 +167,7 @@ function Detail() {
}
function addInvoice() {
postAddInvoice(userId, travelAgencyId, GSN, "", 0, "", "[]", "")
postAddInvoice(userId, travelAgencyId, GSN, "", 0, "", "[]", "")
.then(data => {})
.finally(() => {
defaultShow();
@ -193,20 +193,20 @@ function Detail() {
}
}
const invoiceStatus = FKState => {
const invoiceStatus = (FKState) => {
switch (FKState - 1) {
case 1:
return "Submitted";
case 2:
return "Travel Advisor";
case 3:
return "Finance Dept";
case 4:
return "Paid";
default:
return "";
}
};
case 1:
return 'Submitted';
case 2:
return 'Travel Advisor';
case 3:
return 'Finance Dept';
case 4:
return 'Paid';
default:
return '';
}
};
//
function bindSubmitForm() {
@ -218,13 +218,13 @@ function Detail() {
<Row key={data.GMD_SN} gutter={16}>
<Col span={4}></Col>
<Col span={16}>
<Card type="inner" title={"Invoice " + (index + 1)}>
<Card type="inner" title={"Invoice " + ++index}>
<Row gutter={16}>
<Col span={12}>
<Descriptions column={1}>
<Descriptions.Item label="Amount">{data.GMD_Cost}</Descriptions.Item>
<Descriptions.Item label="Currency">{data.GMD_Currency}</Descriptions.Item>
<Descriptions.Item label="Due Date">{data.GMD_PayDate}</Descriptions.Item>
<Descriptions.Item label="Due Dat">{data.GMD_PayDate}</Descriptions.Item>
<Descriptions.Item label="Status">{invoiceStatus(data.FKState)}</Descriptions.Item>
</Descriptions>
</Col>
@ -232,15 +232,13 @@ function Detail() {
<Image.PreviewGroup>
{invoicePicList[index] &&
invoicePicList[index].map(item => {
if (item.url) {
return <Image key={item.uid} width={90} src={item.url} />;
}
return <Image key={item.uid} width={90} src={item.url} />;
})}
</Image.PreviewGroup>
</Col>
</Row>
</Card>
{addButton(index + 1 == invoiceZDDetail.length)}
{addButton(index++ == invoiceZDDetail.length)}
</Col>
<Col span={4}></Col>
</Row>
@ -253,7 +251,7 @@ function Detail() {
<Col span={16}>
<Card
type="inner"
title={"Invoice " + (index + 1)}
title={"Invoice " + ++index}
extra={
<Button type="link" onClick={() => setEdited(false)}>
Edit
@ -285,25 +283,21 @@ function Detail() {
]}>
<Select placeholder="Select Currency type" onChange={onCurrencyChange} options={bindCurrency()}></Select>
</Form.Item>
<Form.Item
name="info_date"
label="Due Month"
<Form.Item name="info_date" label="Due Month"
rules={[
{
required: true,
message: "please select Due Month!",
required:true,
message:"please select Due Month!",
},
]}>
]}
>
<DatePicker picker="month" />
</Form.Item>
<Text type="secondary">
Payment is arranged during the last week of each month. If the invoice is issued after the 20th, please select the following month for payment. For urgent payments, please contact the
travel advisor.{" "}
</Text>
<Text type="secondary">Payment is arranged during the last week of each month. If the invoice is issued after the 20th, please select the following month for payment. For urgent payments, please contact the travel advisor. </Text>
<Form.Item name="info_gmdsn" hidden={true}>
<input />
</Form.Item>
<br />
<br/>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
@ -315,12 +309,13 @@ function Detail() {
<Upload
name="ghhfile"
multiple={true}
action={config.OVERSEA_HOST + `/service-fileServer/FileUpload?GRI_SN=${GSN}&VEI_SN=${travelAgencyId}&FilePathName=invoice&token=${loginToken}`}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GSN}&VEI_SN=${travelAgencyId}&FilePathName=invoice&token=${token}`}
fileList={fileList}
listType="picture-card"
onChange={handleChange}
onRemove={handRemove}
accept=".jpg,.png,.peg,.bmp">
accept=".jpg,.png,.peg,.bmp"
>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload Invoice</div>
@ -357,10 +352,7 @@ function Detail() {
<Title level={4}>Reference Number: {invoiceGroupInfo.VGroupInfo}</Title>
</Col>
<Col span={4}>
<BackBtn />
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/history/0/338787`)}>
Billing Records
</Button>
<BackBtn />
</Col>
</Row>
<Title level={5}></Title>
@ -370,4 +362,4 @@ function Detail() {
);
}
export default Detail;
export default (Detail);

@ -1,130 +0,0 @@
import { useParams, NavLink, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Table, Image,App } from "antd";
import { formatDate, isNotEmpty } from "@/utils/commons";
import SearchForm from "@/components/SearchForm";
import dayjs from "dayjs";
import BackBtn from "@/components/BackBtn";
import { fetchInvoiceDetail } from "@/stores/Invoice";
import useInvoiceStore from "@/stores/Invoice";
import { usingStorage } from "@/hooks/usingStorage";
function History() {
const { travelAgencyId } = usingStorage();
const { GMDSN, GSN } = useParams();
const [dataLoading, setDataLoading] = useState(false);
const [invoiceZDDetail, setInvoiceZDDetail] = useState([]);
const { notification } = App.useApp();
useEffect(() => {
defaultShow();
}, [GMDSN, GSN]);
function defaultShow() {
setDataLoading(true);
fetchInvoiceDetail(travelAgencyId, GMDSN, GSN)
.then((json) => {
//console.log("id:"+travelAgencyId+",gmdsn:"+GMDSN+",GSN:"+GSN+"#13"+json);
setInvoiceZDDetail(json.invoiceZDDetail);
})
.catch((ex) => {
notification.error({
message: `Notification`,
description: ex.message,
placement: "top",
duration: 4,
});
})
.finally(() => {
setDataLoading(false);
});
}
const invoicePaidColumns = [
{
title: "ID",
dataIndex: "GMD_SN",
key: "GMD_SN",
},
{
title: "Due Date",
key: "GMD_PayDate",
dataIndex: "GMD_PayDate",
render: (text, record) =>
isNotEmpty(text) ? formatDate(new Date(text)) : "",
},
{
title: "Amount",
key: "GMD_Cost",
dataIndex: "GMD_Cost",
},
{
title: "Currency",
key: "GMD_Currency",
dataIndex: "GMD_Currency",
},
{
title: "Status",
key: "FKState",
dataIndex: "FKState",
render: (text, record) => (isNotEmpty(record.FKState) ? invoiceStatus(record.FKState) : text),
},
{
title: "Invoice",
key: "GMD_Pic",
dataIndex: "GMD_Pic",
render: showPIc,
},
];
const invoiceStatus = (FKState) => {
switch (FKState - 1) {
case 1:
return "Submitted";
case 2:
return "Travel Advisor";
case 3:
return "Finance Dept";
case 4:
return "Paid";
default:
return "";
}
};
function showPIc(text, record) {
let strPic = record.GMD_Pic;
//console.log(JSON.parse(strPic));
if (isNotEmpty(strPic)) {
return JSON.parse(strPic).map((item, index) => {
return <Image key={index} width={90} src={item.url} />;
});
} else {
return "";
}
}
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col span={20}></Col>
<Col span={4}>
<BackBtn />
</Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table
rowKey={"GMD_SN"}
bordered
columns={invoicePaidColumns}
dataSource={invoiceZDDetail}
/>
</Col>
</Row>
</Space>
);
}
export default History;

@ -72,35 +72,33 @@ function Index() {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col md={16} sm={16} xs={24} >
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
}}
fieldsConfig={{
shows: ['referenceNo', 'invoiceStatus', 'dates'],
fieldProps: {
referenceNo: { col: 7 },
invoiceStatus: { col: 4},
dates: { col: 10 },
},
}}
onSubmit={(err, formVal, filedsVal) => {
fetchInvoiceList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.enddate, formVal.invoiceStatus);
}}
/>
</Col>
<Col md={8} sm={8} xs={24} >
<Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/395074`)}>
Misc. Invoice
</Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/paid`)}>
Bank statement
</Button>
</Space>
</Col>
</Row>
<Col flex="auto">
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
}}
fieldsConfig={{
shows: ['referenceNo', 'invoiceStatus', 'dates'],
fieldProps: {
referenceNo: { col: 7 },
invoiceStatus: { col: 4},
dates: { col: 10 },
},
}}
onSubmit={(err, formVal, filedsVal) => {
fetchInvoiceList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.enddate, formVal.invoiceStatus);
}}
/>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/338787`)}>
Misc. Invoice
</Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/paid`)}>
Bank statement
</Button>
</Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={(invoiceList)} />

@ -1,34 +1,36 @@
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Typography, Divider } from "antd";
import { fetchNoticeDetail } from "@/stores/Notice";
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
import { NavLink, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Row, Col, Space, Typography, Divider } from 'antd';
import * as comm from '@/utils/commons';
import { useTranslation } from 'react-i18next';
import { fetchNoticeDetail } from '@/stores/Notice';
import BackBtn from '@/components/BackBtn';
import { usingStorage } from '@/hooks/usingStorage';
const { Title, Paragraph } = Typography;
function Detail() {
const { t } = useTranslation();
const { CCP_BLID } = useParams();
const { userId } = usingStorage();
const {userId} = usingStorage();
const [noticeInfo, setNoticeInfo] = useState({});
useEffect(() => {
// console.info("notice detail .useEffect " + CCP_BLID);
fetchNoticeDetail(userId, CCP_BLID).then((res) => {
setNoticeInfo(res);
});
}, []);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Space direction='vertical' style={{ width: '100%' }}>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={16}>
<Title level={1}>{noticeInfo.CCP_BLTitle}</Title>
<Divider orientation="right">{noticeInfo.CCP_LastEditTime}</Divider>
<Divider orientation='right'>{noticeInfo.CCP_LastEditTime}</Divider>
<Paragraph>
<div className="whitespace-pre-line">
{noticeInfo.CCP_BLContent}
</div>
<div dangerouslySetInnerHTML={{ __html: comm.escape2Html(noticeInfo.CCP_BLContent) }}></div>
</Paragraph>
</Col>
<Col span={4}>

@ -1,47 +1,78 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { App, Empty, Button, Collapse, Table, Space, Alert } from 'antd';
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, groupBy, isEmpty, isNotEmpty } from '@/utils/commons';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config';
import Header from './Detail/Header';
import dayjs from 'dayjs';
import { usingStorage } from '@/hooks/usingStorage';
import { ClockCircleOutlined, PlusCircleFilled } from '@ant-design/icons';
import ProductQuotationLogPopover, { columnsSets } from './Detail/ProductQuotationLogPopover';
import { isEmpty } from '@/utils/commons';
// import PrintContractPDF from './PrintContractPDF';
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 (
<div className='flex justify-end items-center gap-4 h-full'>
<div className='grow'>
<h2 className='m-0 leading-tight'>{title}<Divider type={'vertical'} />{(use_year || '').replace('all', '')}</h2>
</div>
{/* <Button size='small'>{t('Copy')}</Button> */}
{/* <Button size='small'>{t('Import')}</Button> */}
<Link className='px-2' to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`}>{t('Edit')}</Link>
<Button size='small' type={'primary'} onClick={() => handleAuditItem('2', agency)}>
{t('products:auditStateAction.Published')}
</Button>
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditItem('2', agency)}>
{t('products:auditStateAction.Approved')}
</Button> */}
<Button size='small' type={'primary'} danger ghost onClick={() => handleAuditItem('3', agency)}>
{t('products:auditStateAction.Rejected')}
</Button>
{/* todo: export, 审核完成之后才能导出 */}
<Button size='small'>{t('Print')} PDF</Button>
{/* <PrintContractPDF /> */}
</div>
);
};
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 [logOpenPriceRow, setLogOpenPriceRow] = useState(null); // price id
const handleAuditPriceItem = (state, row, rowIndex) => {
const handleAuditPriceItem = (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(); // debug: ,
// const newData = structuredClone(renderData);
const newData = cloneDeep(renderData);
newData.splice(rowIndex, 1, {...row, audit_state_id: state, });
setRenderData(newData);
refresh();
}
}
})
@ -56,114 +87,55 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
};
const rowStyle = (r, tri) => {
const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; //
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' : ''; // 线
// && isNotEmpty(r.lastedit_changed)
const editedCls = (r.audit_state_id === 0 ) ? '!bg-amber-100' : ''; // ,
const newCls = (r.audit_state_id === -1 ) ? '!bg-sky-100' : ''; // ,
const editedCls_ = isNotEmpty(r.lastedit_changed) ? (r.audit_state_id === 0 ? '!bg-red-100' : '!bg-sky-100') : '';
const lodHighlightCls = (r.id === logOpenPriceRow ) ? '!bg-violet-300 !text-violet-900' : '';
return [trCls, bigTrCls, newCls, editedCls, lodHighlightCls].join(' ');
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 <Link to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`} onClick={() => setEditingProduct(r.info)}>{title}</Link>;
} },
...(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: '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 || '';
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit`
: isPermitted(PERM_PRODUCTS_OFFER_PUT)
? `/products/edit`
: '';
return (
<div className=''>
{isNotEmpty(itemLink) ? (
<div className='' onClick={() => setEditingProduct({ info: r.info })}>
<Link to={itemLink}>{title}</Link>
</div>
) : (
title
)}
</div>
);
},
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}` : ''),
},
...columnsSets(t),
{ 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} `;
const stateCls = `text-${stateMapVal[`${r.audit_state_id}`]?.color} `;
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
},
},
{
title: '',
key: 'action',
render: (_, r, ri) =>
[-1, 0, 3].includes(Number(r.audit_state_id)) ? (
<>
<Space className='w-full [&>*:last-child]:ms-auto'>
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
{Number(r.audit_state_id) === 0 && (
<div className='flex gap-2'>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</div>
)}
</RequireAuth>
<ProductQuotationLogPopover
method={'history'}
{...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
onOpenChange={(open) => setLogOpenPriceRow(open ? r.id : null)}
/>
</Space>
</>
render: (_, r) =>
r.audit_state_id <= 0 ? (
<Space>
<Button onClick={() => handleAuditPriceItem('2', r)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r)}></Button>
</Space>
) : null,
},
// {
// title: '',
// key: 'action2',
// width: '6rem',
// className: 'bg-white align-bottom',
// onCell: (r, index) => ({ rowSpan: r.rowSpan }),
// render: (_, r) => {
// const showPublicBtn = null; // r.pendingQuotation ? <Popover title='' trigger={['click']}> <Button size='small' className='ml-2' onClick={() => { }}></Button></Popover> : null;
// const btn2 = !r.showPublicBtn ? (
// <>
// <ProductQuotationSnapshotPopover
// // <ProductQuotationLogPopover
// method={'snapshot'}
// {...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
// triggerProps={{ type: 'primary', ghost: true, size: 'small' }}
// placement='bottom'
// className='max-w-[1000px]'
// />
// </>
// ) : null;
// return <div className='flex flex-col gap-2 justify-end'>{btn2}</div>;
// },
// },
];
return (
<Table
size={'small'}
className='border-collapse'
rowHoverable={false}
rowClassName={rowStyle}
pagination={false}
{...{ columns }}
dataSource={renderData}
rowKey={(r) => r.id}
/>
);
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, dataSource }} rowKey={(r) => r.id} />;
};
/**
@ -179,89 +151,67 @@ const TypesPanels = (props) => {
useEffect(() => {
// ; , ; ,
const hasDataTypes = Object.keys(agencyProducts);
let tempKey = '';
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => {
const _children = agencyProducts[ele.value].reduce(
.map((ele) => ({
...ele,
extra: t('Table.Total', { total: agencyProducts[ele.value].length }),
children: (
<PriceTable
// loading={loading}
productType={ele.value}
dataSource={agencyProducts[ele.value].reduce(
(r, c, ri) =>
r.concat(
c.quotation.map((q, i) => ({
...q,
// weekdays: q.weekdays
// .split(',')
// .filter(Boolean)
// .map((w) => t(`weekdaysShort.${w}`))
// .join(', '),
weekdays: q.weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`weekdaysShort.${w}`))
.join(', '),
info: c.info,
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}),
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...r, [clgc.lgc]: clgc}), {}),
rowSpan: i === 0 ? c.quotation.length : 0,
rowSpanI: [ri, i],
showPublicBtn: c.quotation.some(q2 => [0, 3].includes(q2.audit_state_id)),
}))
),
[]
);
tempKey = _children.length > 0 && tempKey==='' ? ele.key : tempKey;
const _childrenByState = groupBy(_children, 'audit_state_id');
// if (_children.length > 0) console.log('PriceTable\n'+ele.value+'\n', _children)
return {
...ele,
extra: <Space>
{_childrenByState['1']?.length > 0 && <Alert showIcon type='success' className='py-1 text-xs' message={_childrenByState['1']?.length || 0} />}
{_childrenByState['2']?.length > 0 && <Alert showIcon type='success' className='py-1 text-xs' message={_childrenByState['2']?.length || 0} icon={<ClockCircleOutlined />} />}
{_childrenByState['0']?.length > 0 && <Alert showIcon type='warning' className='py-1 text-xs' message={_childrenByState['0']?.length || 0} />}
{_childrenByState['3']?.length > 0 && <Alert showIcon type='error' className='py-1 text-xs' message={_childrenByState['3']?.length || 0} />}
{_childrenByState['-1']?.length > 0 && <Alert showIcon type='info' className='py-1 text-xs' message={_childrenByState['-1']?.length || 0} icon={<PlusCircleFilled />} />}
<span>{t('Table.Total', { total: _children.length })}</span>
</Space>,
children: (
<PriceTable
// loading={loading}
productType={ele.value}
dataSource={_children}
)}
refresh={props.refresh}
/>
),
}});
}));
setShowTypes(_show);
setActiveKey(isEmpty(_show) ? [] : [tempKey]);
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
return () => {};
}, [productsTypes, agencyProducts]);
const onCollapseChange = (_activeKey) => {
setActiveKey(_activeKey);
};
return isEmpty(agencyProducts) ? <Empty /> : <Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />;
return <Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />;
};
const Audit = ({ ...props }) => {
const { notification, modal } = App.useApp()
const isPermitted = useAuthStore(state => state.isPermitted);
const { travel_agency_id, use_year, audit_state } = useParams();
const [activeAgency, getAgencyProducts] = useProductsStore((state) => [state.activeAgency, state.getAgencyProducts]);
const [loading, setLoading] = useProductsStore(state => [state.loading, state.setLoading]);
const { travelAgencyId } = usingStorage();
const [loading, activeAgency, getAgencyProducts] = useProductsStore((state) => [state.loading, state.activeAgency, state.getAgencyProducts]);
const handleGetAgencyProducts = async ({pick_year, pick_agency, pick_state}={}) => {
const year = pick_year || use_year || dayjs().year();
const agency = pick_agency || travel_agency_id || travelAgencyId;
const state = pick_state ?? audit_state;
getAgencyProducts({ travel_agency_id: agency, use_year: year, audit_state: state }).catch(ex => {
setLoading(false);
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
});
const handleGetAgencyProducts = () => {
getAgencyProducts({ travel_agency_id, use_year, audit_state });
};
useEffect(() => {
handleGetAgencyProducts();
return () => {};
}, [travel_agency_id]);
return (
<>
<SecondHeaderWrapper loading={loading} backTo={isPermitted(PERM_PRODUCTS_MANAGEMENT) ? `/products` : false} header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} />} >
<SecondHeaderWrapper header={<Header title={activeAgency.travel_agency_name} agency={activeAgency} refresh={handleGetAgencyProducts} />} loading={loading} >
{/* debug: 0 */}
{/* <PrintContractPDF /> */}
<TypesPanels refresh={handleGetAgencyProducts} />

@ -1,58 +1,733 @@
import { useState } from 'react';
import { App, Divider, Empty, Flex } from 'antd';
import { isEmpty } from '@/utils/commons';
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
import Header from './Detail/Header';
import React, { useState, useEffect, useRef, useMemo } from 'react';
import { Button, Card, Col, Row, Breadcrumb, Table, Popconfirm, Form, Input, InputNumber, Tag, Modal, Select, Tree } from 'antd';
import { Link } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import Date from '@/components/date';
import { searchAgencyAction, getAgencyProductsAction } from '@/stores/Products/Index';
import { useProductsTypes } from '@/hooks/useProductsSets';
import Extras from './Detail/Extras';
import { groupBy } from '@/utils/commons';
import { useParams } from 'react-router-dom';
import useProductsStore from '@/stores/Products/Index';
import dayjs from 'dayjs';
import { usingStorage } from '@/hooks/usingStorage';
import ProductsTree from './Detail/ProductsTree';
import ProductInfo from './Detail/ProductInfo';
import NewProductModal from './Detail/NewProductModal';
import { useHTLanguageSets } from '@/hooks/useHTLanguageSets';
import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
import BatchImportPrice from '@/components/BatchImportPrice';
function Detail() {
const { notification, modal } = App.useApp();
const { travel_agency_id, audit_state, use_year } = useParams();
const [addProductVisible, setAddProductVisible] = useState(false);
const [agencyProducts, switchParams] = useProductsStore((state) => [state.agencyProducts, state.switchParams]);
const [getAgencyProducts, activeAgency] = useProductsStore((state) => [state.getAgencyProducts, state.activeAgency]);
const [loading, setLoading] = useProductsStore((state) => [state.loading, state.setLoading]);
const { travelAgencyId } = usingStorage();
const handleGetAgencyProducts = async ({ pick_year, pick_agency, pick_state } = {}) => {
const year = pick_year || use_year || switchParams.use_year || dayjs().year();
const agency = pick_agency || travel_agency_id || travelAgencyId;
const state = pick_state ?? audit_state;
const param = { travel_agency_id: agency, use_year: year, audit_state: state };
// setEditingProduct({});
getAgencyProducts(param).catch((ex) => {
setLoading(false);
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
const { t } = useTranslation();
const [form] = Form.useForm();
const [editingid, setEditingid] = useState('');
const [tags, setTags] = useState([]);
const [isModalVisible, setIsModalVisible] = useState(false);
const [selectedTag, setSelectedTag] = useState(null);
const [saveData, setSaveData] = useState(null);
const [datePickerVisible, setDatePickerVisible] = useState(false);
const [batchImportPriceVisible, setBatchImportPriceVisible] = 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 { travel_agency_id } = useParams();
const { language } = useDefaultLgc();
const HTLanguageSets = useHTLanguageSets();
const { Search } = Input;
const [expandedKeys, setExpandedKeys] = useState([]);
const [searchValue, setSearchValue] = useState('');
const [autoExpandParent, setAutoExpandParent] = useState(true);
const [dataList, setDataList] = useState([]);
const [defaultData, setDefaultData] = useState([]);
const productProject = {
"6": [],
"B": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "km", name: t('products:KM') },
{ code: "remarks", name: t('products:Remarks') }
],
"J": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "dept_name", name: t('products:Dept') },
{ code: "display_to_c", name: t('products:DisplayToC') },
{ code: "remarks", name: t('products:Remarks') },
],
"Q": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "dept_name", name: t('products:Dept') },
{ code: "display_to_c", name: t('products:DisplayToC') },
{ code: "remarks", name: t('products:Remarks') },
],
"D": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "dept_name", name: t('products:Dept') },
{ code: "display_to_c", name: t('products:DisplayToC') },
{ code: "remarks", name: t('products:Remarks') },
],
"7": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
{ code: "recommends_rate", name: t('products:recommendationRate') },
{ code: "duration", name: t('products:Duration') },
{ code: "open_weekdays", name: t('products:OpenWeekdays') },
{ code: "remarks", name: t('products:Remarks') },
],
"R": [
{ code: "code", name: t('products:Code') },
{ code: "city_name", name: t('products:City') },
]
}
const [selectedCategory, setSelectedCategory] = useState(productProject.B);
useEffect(() => {
setLanguageStatus(language);
const matchedLanguage = HTLanguageSets.find(HTLanguage => HTLanguage.key === language.toString());
const languageLabel = matchedLanguage.label
// setTags([languageLabel])
setLanguageLabel(languageLabel)
setSelectedTag(languageLabel)
setRemainderLanguage(HTLanguageSets.filter(item => item.key !== language.toString()))
}, []);
useEffect(() => {
const fetchData = async () => {
const a = { travel_agency_id };
const res = await getAgencyProductsAction(a);
const groupedProducts = groupBy(res.products, (row) => row.info.product_type_id);
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}`,
}))
}));
};
const treeData = generateTreeData(productsTypes, groupedProducts);
setTreeData(treeData);
setProductsData(groupedProducts);
setDefaultData(treeData);
setDataList(flattenTreeData(treeData));
};
fetchData();
}, [productsTypes]);
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 = (
<span style={{ color: 'red' }}>{searchValue}</span>
);
return index > -1 ? (
<span>
{beforeStr}
{highlighted}
{afterStr}
</span>
) : (
<span>{node.title}</span>
);
};
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) => {
form.setFieldsValue({ ...record });
setEditingid(record.id);
};
const cancel = () => {
setEditingid('');
};
const handleSave = async (id) => {
try {
const { info, ...restRow } = await form.validateFields();
const newData = [...quotation];
const index = newData.findIndex((item) => id === item.id);
if (index > -1) {
const item = newData[index];
newData.splice(index, 1, { ...item, ...restRow });
delete newData[index].quotation
delete newData[index].extras
setQuotation(newData);
setEditingid('');
} else {
newData.push(restRow);
setQuotation(newData);
setEditingid('');
}
} catch (errInfo) {
console.log('Validate Failed:', errInfo);
}
};
const handleDelete = (id) => {
const newData = [...quotation];
const index = newData.findIndex((item) => id === item.id);
newData.splice(index, 1);
setQuotation(newData);
};
const handleAdd = () => {
const newData = {
id: `${quotation.length + 1}`,
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 handleDateChange = ({ dateRange, selectedDays }) => {
//
const weekdays = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'];
let weekDayCount = selectedDays.map(day => weekdays.indexOf(day) + 1).sort().join(',');
if (!weekDayCount || weekDayCount.length === 0) {
weekDayCount = "全年";
}
const newData = [...quotation];
const index = newData.findIndex((item) => currentid === item.id);
if (index > -1) {
newData[index].weekdays = weekDayCount;
setQuotation(newData);
}
setSelectedDateData({ dateRange, selectedDays })
};
const handleDateOk = () => {
const { dateRange } = selectedDateData;
const dateRangeList = dateRange.split('-');
const use_dates_start = dateRangeList[0];
const use_dates_end = dateRangeList[1];
if (currentid !== null) {
const newData = [...quotation];
const index = newData.findIndex((item) => currentid === item.id);
if (index > -1) {
newData[index].use_dates_start = use_dates_start;
newData[index].use_dates_end = use_dates_end;
setQuotation(newData);
setCurrentid(null);
}
}
setDatePickerVisible(false);
}
const handleBatchImportOK = () => {
setBatchImportPriceVisible(false);
}
const EditableCell = ({ editing, dataIndex, title, inputType, record, children, handleDateSelect, ...restProps }) => {
let inputNode = inputType === 'number' ? <InputNumber /> : <Input />;
if (dataIndex === 'validityPeriod' && editing) {
return (
<td {...restProps}>
{children}
<Button onClick={() => handleDateSelect(record.id)}>选择日期</Button>
</td>
);
}
if (dataIndex === 'unit_name' && editing) {
inputNode = (
<Select>
<Select.Option value="每人">每人</Select.Option>
<Select.Option value="每团">每团</Select.Option>
</Select>
);
}
if (dataIndex === 'currency' && editing) {
inputNode = (
<Select>
<Select.Option value="每人">RMB</Select.Option>
<Select.Option value="每团">MY</Select.Option>
</Select>
);
}
if (dataIndex === 'group_size' && editing) {
return (
<td {...restProps} style={{ height: 115, display: 'flex', alignItems: 'center' }}>
<InputNumber
min={0}
value={record.group_size_min}
onChange={(value) => handleInputGroupSize('group_size_min', record.id, 'group_size', value)}
style={{ width: '50%', marginRight: '10px' }}
/>
<span>-</span>
<InputNumber
min={0}
value={record.group_size_max}
onChange={(value) => handleInputGroupSize('group_size_max', record.id, 'group_size', value)}
style={{ width: '50%', marginLeft: '10px' }}
/>
</td>
);
}
return (
<td {...restProps}>
{editing ? (
<Form.Item
name={dataIndex}
style={{ margin: 0 }}
rules={[{ required: true, message: `Please Input ${title}!` }]}
>
{inputNode}
</Form.Item>
) : (
children
)}
</td>
);
};
const handleInputGroupSize = (name, id, dataIndex, value) => {
const newData = [...quotation];
const index = newData.findIndex((item) => id === item.id);
if (index > -1) {
const item = newData[index];
newData[index] = { ...item, }
if (name === 'group_size_min') {
newData[index] = { ...item, group_size_min: value };
} else {
newData[index] = { ...item, group_size_max: value };
}
setQuotation(newData);
}
}
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) => {
const editable = isEditing(record);
return editable ? (
<span>
<a href="#!" onClick={() => handleSave(record.id)} style={{ marginRight: 8 }}>{t('products:save')}</a>
<Popconfirm title={t('products:sureCancel')} onConfirm={cancel}><a>{t('products:cancel')}</a></Popconfirm>
</span>
) : (
<span>
<a disabled={editingid !== ''} onClick={() => edit(record)} style={{ marginRight: 8 }}>{t('products:edit')}</a>
<Popconfirm title={t('products:sureDelete')} onConfirm={() => handleDelete(record.id)}>
<a>{t('products:delete')}</a>
</Popconfirm>
</span>
);
},
},
];
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;
})
setRemainderLanguage(tempRemainderLanguage)
setTags([...tags, selectedTag])
setSelectedTag(null);
setIsModalVisible(false);
}
const handleCancel = () => setIsModalVisible(false);
const handleTagChange = (value) => {
console.log("handleTagChange", value)
setSelectedTag(value);
console.log("setSelectedTag", selectedTag)
};
const handleChange = (field, value) => {
console.log("languageStatus", languageStatus)
console.log("...lgc_details[languageStatus]", { ...lgc_details[languageStatus] })
// lgc_details
const updatedLgcDetails = {
...lgc_details,
[languageStatus]: { ...lgc_details[languageStatus], [field]: value, lgc: languageStatus }
};
setLgc_details(updatedLgcDetails)
console.log("AAAAAAAAAAAAAA", lgc_details);
};
//
const handleNodeSelect = (_, { node }) => {
setTags([languageLabel])
//
if (selectedNodeid === node.key) return;
const fatherKey = node.key.split('-')[0];
setSelectedCategory(productProject[fatherKey])
console.log("remainderLanguage",remainderLanguage)
let initialQuotationData = null;
let infoData = null;
let lgcDetailsData = null;
productsData[fatherKey].forEach(element => {
if (element.info.title === node.title) {
initialQuotationData = element.quotation;
infoData = element.info;
lgcDetailsData = element.lgc_details;
return true;
}
});
console.log("infoData", infoData)
// lgc_details
let newLgcDetails = {};
lgcDetailsData.forEach(element => {
newLgcDetails[element.lgc] = element;
});
// lgc_details
setLgc_details(newLgcDetails);
setQuotation(initialQuotationData);
console.log("descriptions", lgc_details)
// 使 setTimeout lgc_details
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: {
title: newLgcDetails[language]?.title || '',
descriptions: newLgcDetails[language]?.descriptions || ''
}
});
};
const onSave = (values) => {
const tempData = values;
tempData['quotation'] = quotation;
// tempData['extras'] = bindingData;
// tempData['lgc_details'] = languageStatus;
setSaveData(tempData);
console.log("保存的数据", tempData)
};
return (
<SecondHeaderWrapper
loading={loading}
backTo={false}
header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} handleNewProduct={() => setAddProductVisible(true)} />}>
<>
<Flex gap={10} className='h-full'>
{/* onNodeSelect={handleNodeSelect} */}
<ProductsTree className='basis-80 sticky top-0 overflow-y-auto shrink-0' style1={{ height: 'calc(100vh - 150px)' }} />
<Divider type={'vertical'} className='mx-1 h-auto' />
<div className=' flex-auto overflow-auto '>
<ProductInfo />
</div>
</Flex>
</>
</SecondHeaderWrapper>
<div>
<Row>
<Col span={6}>
<Card style={{ width: "20%", position: "fixed", maxHeight: "80vh", overflowY: "auto" }}>
<Search style={{ marginBottom: 8 }} placeholder="Search" onChange={onChange} />
<Tree
onSelect={handleNodeSelect}
treeData={treeData}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={onExpand}
titleRender={titleRender}
/>
</Card>
</Col>
<Col span={18}>
<Form form={form} name="control-hooks" onFinish={onSave}>
<Card
style={{ width: "80%" }}
title={
<Breadcrumb items={[
{ title: <Link to={'/'}>供应商</Link> },
{ title: <Link to={'/products'}>综费</Link> },
{ title: '文章列表' }
]} />
}
>
<h2>{t('products:productProject')}</h2>
<Row gutter={16}>
{selectedCategory.map((item, index) => (
<Col span={8} id={index}>
<Form.Item name={['info', item.code]} label={item.name}>
{item.code === "duration" ? (
<Input suffix="H"/>
) : (
<Input />
)}
</Form.Item>
</Col>
))}
</Row>
<Card title={
<div>
{tags.map(tag => (
<Tag
id={tag}
onClick={() => handleTagClick(tag)}
color={tag === selectedTag ? 'blue' : undefined}
style={{ cursor: 'pointer' }}
>
{tag}
</Tag>
))}
<Tag onClick={showModal} style={{ cursor: 'pointer' }}>+</Tag>
</div>
}>
<Modal title="选择语言" visible={isModalVisible} onOk={handleOk} onCancel={handleCancel}>
<Select
showSearch
style={{ width: "80%" }}
placeholder="选择语言"
optionFilterProp="children"
onChange={handleTagChange}
value={remainderLanguage.some((item) => item.label === selectedTag) ? selectedTag : undefined}
>
{
remainderLanguage.map((value, label) => (
<Select.Option key={value.label} value={value.label}>
{value.label}
</Select.Option>
))
}
</Select>
</Modal>
<Form.Item label={t('products:Name')} name={['lgc_details', 'title']}>
<Input
style={{ width: "30%" }}
onChange={(e) => handleChange('title', e.target.value)}
/>
</Form.Item>
<Form.Item label={t('products:Description')} name={['lgc_details', 'descriptions']}>
<Input.TextArea
rows={4}
onChange={(e) => handleChange('descriptions', e.target.value)}
/>
</Form.Item>
</Card>
</Card>
<Card style={{ width: "80%" }}>
<h2>{t('products:supplierQuotation')}</h2>
<Form.Item name="quotation">
<Table
components={{ body: { cell: EditableCell } }}
bordered
dataSource={quotation}
columns={mergedColumns}
rowClassName="editable-row"
pagination={{ onChange: cancel }}
/>
<Button onClick={handleAdd} type="primary" style={{ marginBottom: 16 }}>
{t('products:addQuotation')}
</Button>
<Button onClick={handleBatchImport} type="primary" style={{ marginBottom: 16 }}>批量添加</Button>
</Form.Item>
</Card>
<Card style={{ width: "80%" }}>
<Extras productId={2} />
</Card>
<Button type="primary" htmlType="submit" style={{ marginTop: 16, float: "right", marginRight: "20%" }}>
{t('products:save')}
</Button>
<Button type="primary" htmlType="submit" style={{ marginTop: 16, float: "right", marginRight: "5%" }}>
提交审核
</Button>
</Form>
</Col>
</Row>
{datePickerVisible && (
<Modal
title="选择日期"
visible={datePickerVisible}
onOk={handleDateOk}
onCancel={() => setDatePickerVisible(false)}
>
<Date onDateChange={handleDateChange} />
</Modal>
)}
{
batchImportPriceVisible && (
<Modal
title="批量添加价格"
visible={batchImportPriceVisible}
onOk={handleBatchImportOK}
onCancel={() => setBatchImportPriceVisible(false)}
width="80%"
>
<BatchImportPrice/>
</Modal>
)
}
</div>
);
}
export default Detail;

@ -1,100 +0,0 @@
import { useState } from 'react'
import { Form, Modal, Input, Button, Flex, App } from 'antd'
import { useTranslation } from 'react-i18next'
import useProductsStore from '@/stores/Products/Index'
import { useProductsTypesMapVal } from '@/hooks/useProductsSets'
import RequireAuth from '@/components/RequireAuth'
import { PERM_PRODUCTS_OFFER_PUT } from '@/config'
export const ContractRemarksModal = () => {
const { t } = useTranslation()
const { notification } = App.useApp()
const productsTypesMapVal = useProductsTypesMapVal()
const [getRemarkList, saveOrUpdateRemark] = useProductsStore((state) => [
state.getRemarkList, state.saveOrUpdateRemark
])
const [isRemarksModalOpen, setRemarksModalOpen] = useState(false)
const [remarksForm] = Form.useForm()
const onRemarksFinish = () => {
const remarkList = remarksForm.getFieldsValue().remarkList
saveOrUpdateRemark(remarkList)
.then(() => {
setRemarksModalOpen(false)
notification.info({
message: 'Notification',
description: '合同备注保存成功',
placement: 'top',
})
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
})
})
}
const handleContractRemarks = () => {
getRemarkList()
.then(list => {
remarksForm.setFieldsValue({remarkList:list})
setRemarksModalOpen(true)
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
})
})
}
const getFieldLabel = (field) => {
const remarkList = remarksForm.getFieldsValue([['remarkList']]).remarkList
return productsTypesMapVal[remarkList[field.key].product_type_id]?.label
}
return (
<>
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
<Button size='small' onClick={handleContractRemarks}>{t('products:ContractRemarks')}</Button>
</RequireAuth>
<Modal
centered
title={t('products:ContractRemarks')}
width={'640px'}
open={isRemarksModalOpen}
onOk={() => onRemarksFinish()}
onCancel={() => setRemarksModalOpen(false)}
destroyOnHidden
forceRender
>
<Form
labelCol={{ span: 3 }}
wrapperCol={{ span: 20 }}
form={remarksForm}
name='remarksForm'
autoComplete='off'
>
<Form.List name='remarkList'>
{(fields) => (
<Flex gap='middle' vertical>
{fields.map((field) => (
<Form.Item label={getFieldLabel(field)} name={[field.name, 'Memo']} key={field.key}>
<Input.TextArea rows={2}></Input.TextArea>
</Form.Item>
))}
</Flex>
)}
</Form.List>
</Form>
</Modal>
</>
);
};
export default ContractRemarksModal

@ -1,229 +0,0 @@
import { useState, useEffect } from 'react';
import { App, Form, Modal, DatePicker, Divider, Switch, Space, Flex, Radio } from 'antd';
import { isEmpty, objectMapper } from '@/utils/commons';
import { useTranslation } from 'react-i18next';
import DeptSelector from '@/components/DeptSelector';
import ProductsTypesSelector from '@/components/ProductsTypesSelector';
import VendorSelector from '@/components/VendorSelector';
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';
import ProductsSelector from '@/components/ProductsSelector';
dayjs.extend(arraySupport);
export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const {
sourceAgency: { travel_agency_id },
sourceYear: use_year,
} = source;
// const isPermitted = useAuthStore((state) => state.isPermitted);
const [showTypeOrItem, setShowTypeOrItem] = useState(1);
useEffect(() => {
onFormInstanceReady(form);
}, []);
const onValuesChange = (changeValues, allValues) => {
if ('copyType' in changeValues) {
setShowTypeOrItem(changeValues.copyType === 'type' ? 1 : 2);
}
};
return (
<Form layout='horizontal' form={form} name='form_in_modal' initialValues={{...initialValues, copyType: 'type'}} onValuesChange={onValuesChange}>
<Flex gap={8}>
<div className='basis-96 shrink-0 flex-auto'>
{action === '#' && (
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
<Form.Item name={`dept`} label={t('products:Dept')} rules={[{ required: false, message: t('products:CopyFormMsg.requiredDept') }]}>
<DeptSelector isLeaf={true} />
</Form.Item>
</RequireAuth>
)}
<Form.Item name={'copyType'}>
<Radio.Group optionType="button" options={[{ key: 'type', value: 'type', label: t('按类型复制(多选)') }, { key: 'item', value: 'item', label: t('仅复制指定产品(多选)') }]}></Radio.Group>
</Form.Item>
<Form.Item name={`products_types`} label={t('products:ProductType')} dependencies={['products_list']} hidden={showTypeOrItem!==1} >
<ProductsTypesSelector maxTagCount={1} mode={'multiple'} placeholder={t('All')} />
</Form.Item>
<Form.Item
name={'products_list'}
label={t('products:ProductName')} dependencies={['products_types']} hidden={showTypeOrItem!==2} >
<ProductsSelector params={{ travel_agency_id, use_year }} mode={'multiple'} placeholder={t('All')} />
</Form.Item>
<Divider orientation='left'>{t('products:CopyFormMsg.copyTo')}:</Divider>
{action === '#' && (
<Form.Item
name='agency'
label={`${t('products:CopyFormMsg.target')}${t('products:Vendor')}`}
rules={[{ required: true, message: t('products:CopyFormMsg.requiredVendor') }]}>
<VendorSelector mode={null} placeholder={t('products:Vendor')} />
</Form.Item>
)}
<Divider orientation='left'></Divider>
<Form.Item noStyle>
<Space.Compact className='w-full gap-2'>
<Form.Item
name={'source_use_year'}
label={`${t('products:CopyFormMsg.Source')}${t('products:UseYear')}`}
initialValue={dayjs([source.sourceYear, 1, 1])}
rules={[{ required: true }]}>
<DatePicker picker='year' allowClear />
</Form.Item>
<Form.Item name={'target_use_year'} label={`${t('products:CopyFormMsg.target')}${t('products:UseYear')}`} rules={[{ required: true }]}>
<DatePicker picker='year' allowClear />
{/* disabledDate={(current) => current <= dayjs([source.sourceYear, 12, 31])} */}
</Form.Item>
</Space.Compact>
</Form.Item>
<Form.Item name={'with_quote'} label={`${t('products:CopyFormMsg.withQuote')}`}>
<Switch checkedChildren={'含报价金额'} unCheckedChildren={'仅人等+日期'} />
</Form.Item>
</div>
<Form.Item noStyle shouldUpdate>
{() => showTypeOrItem===2 ? (
<div className='max-h-96 overflow-auto divide-x-0 divide-y divide-solid divide-stone-200'>
{!isEmpty(form.getFieldValue('products_list')) && <b>已选择的产品 预览:</b>}
{(form.getFieldValue('products_list') || []).map((item, index) => (
<div key={item.value}>
{index + 1}.&nbsp;{item.label}
</div>
))}
</div>
) : (<></>)}
</Form.Item>
</Flex>
</Form>
);
};
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 : '-1';
},
},
'dept': {
key: 'dept',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'with_quote': { key: 'with_quote', transform: (value) => (value ? 1 : 0) },
'products_list': { key: 'product_id_list', transform: (value) => (Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '') },
};
let dest = {};
const { agency, year, products_list, ...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;
// ,
param.products_types = param.copyType === 'item' ? '' : param.products_types;
param.product_id_list = param.copyType === 'type' ? '' : param.product_id_list;
setCopyLoading(true);
// debug:
// console.log('ready params', param);
// setCopyLoading(false);
// throw new Error('');
// const toID = param.target_agency;
const success = await copyAgencyDataAction({ ...param, source_agency: source.sourceAgency.travel_agency_id }).catch((ex) => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
});
});
setCopyLoading(false);
success ? message.success(t('Success')) : message.error(t('Failed'));
if (success && typeof onSubmit === 'function') {
onSubmit(param);
}
// setCopyModalVisible(false);
// navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
};
return (
<Modal
width={800}
open={open}
title={`${t('Copy')}${t('products:#')}${t('products:Offer')}`}
okText='确认'
// cancelText='Cancel'
okButtonProps={{
autoFocus: true,
}}
confirmLoading={copyLoading}
onCancel={() => {
onCancel();
formInstance?.resetFields();
}}
destroyOnClose destroyOnHidden
onOk={async () => {
try {
const values = await formInstance?.validateFields();
// formInstance?.resetFields();
const dest = formValuesMapper(values);
handleCopyAgency(dest);
} catch (error) {
console.log('Failed:', error);
}
}}>
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
<div className='py-2'>
{t('products:CopyFormMsg.Source')}: {source.sourceAgency.travel_agency_name}
<Divider type={'vertical'} />
{source.sourceYear}
</div>
</RequireAuth>
<CopyProductsForm
action={action}
source={source}
initialValues={initialValues}
onFormInstanceReady={(instance) => {
setFormInstance(instance);
}}
/>
</Modal>
);
};
export default CopyProductsFormModal;

@ -2,23 +2,19 @@ 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 { getAgencyProductExtrasAction, getAgencyProductsAction, 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';
import { usingStorage } from '@/hooks/usingStorage';
import useProductsStore from '@/stores/Products/Index';
const NewAddonModal = ({ onPick, ...props }) => {
// const { travel_agency_id, use_year } = useParams();
const { travel_agency_id, use_year } = useParams();
const { t } = useTranslation();
const { notification, message } = App.useApp();
const [{ travel_agency_id, use_year }] = useProductsStore((state) => [state.switchParams]);
const productsTypesMapVal = useProductsTypesMapVal();
const [open, setOpen] = useState(false);
@ -31,9 +27,10 @@ const NewAddonModal = ({ onPick, ...props }) => {
const { starttime, endtime, year, ...param } = copyObject;
setSearchLoading(true);
setSearchResult([]);
// debug: audit_state: '1',
const search_year = year || use_year;
const result = await searchPublishedProductsAction({ ...param, use_year: search_year, travel_agency_id });
setSearchResult(result);
const result = await getAgencyProductsAction({ ...param, travel_agency_id, use_year: search_year, audit_state: '0', });
setSearchResult(result?.products || []);
setSearchLoading(false);
};
const handleAddExtras = async (item) => {
@ -42,23 +39,23 @@ const NewAddonModal = ({ onPick, ...props }) => {
}
};
// todo:
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: 'ptype', dataIndex: ['info', 'product_type_id'], width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text].label },
{ key: 'title', dataIndex: ['info', '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) => (
<Button className='text-primary' onClick={() => handleAddExtras(record)}>
绑定此项目
附加此项目
</Button>
),
},
@ -75,9 +72,9 @@ const NewAddonModal = ({ onPick, ...props }) => {
<Modal width={'95%'} style={{ top: 20 }} open={open} title={'添加附加'} footer={false} onCancel={() => setOpen(false)} destroyOnClose>
<SearchForm
fieldsConfig={{
shows: ['year', 'keyword', 'products_types', 'city'], // 'dates',
shows: [ 'year', 'keyword'], // 'dates',
fieldProps: {
year: { rules: [{ required: true }] },
dates: { label: t('products:CreateDate') },
keyword: { label: t('products:Title'), col: 4 },
},
// sort: { keyword: 100 },
@ -95,7 +92,6 @@ const NewAddonModal = ({ onPick, ...props }) => {
<Table
size={'small'}
key={'searchProductsTable'}
rowKey={'id'}
loading={searchLoading}
dataSource={searchResult}
columns={searchResultColumns}
@ -113,48 +109,52 @@ const Extras = ({ productId, onChange, ...props }) => {
const { t } = useTranslation();
const { notification, message } = App.useApp();
// const { travel_agency_id, use_year } = useParams();
const { travelAgencyId } = usingStorage();
const [{travel_agency_id, use_year}] = useProductsStore((state) => [state.switchParams]);
const { travel_agency_id, use_year } = useParams();
const [extrasData, setExtrasData] = useState([]);
const handleGetAgencyProductExtras = async () => {
setExtrasData([]);
// console.log('handleGetAgencyProductExtras', productId);
const data = await getAgencyProductExtrasAction({ id: productId, travel_agency_id: travel_agency_id || travelAgencyId, use_year });
const data = await getAgencyProductExtrasAction({ id: productId, travel_agency_id, use_year });
setExtrasData(data);
};
const handleNewAddOn = async (item) => {
// setExtrasData(prev => [].concat(prev, [item]));
setExtrasData(prev => [].concat(prev, [item]));
// todo: ;
const _item = pick(item, ['id', 'title', 'code']);
const _item = pick(item.info, ['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, del_extras_ids: [item.id] });
const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, extras: [item.info.id] });
delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
await handleGetAgencyProductExtras();
};
useEffect(() => {
if (productId) handleGetAgencyProductExtras();
handleGetAgencyProductExtras();
return () => {};
}, [productId]);
}, []);
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}`, // todo:
},
// { title: t('products:Types'), dataIndex: 'age_type', width: '40%', },
{
title: '',
dataIndex: 'operation',
width: '4rem',
render: (_, r) => (
<Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r.info)} okText={t('Yes')} >
<Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r)} okText={t('Yes')} >
<Button size='small' type='link' danger>
{t('Delete')}
</Button>

@ -1,276 +0,0 @@
import { useEffect, useState } from "react";
import { useParams, Link, useNavigate, useLocation } from "react-router-dom";
import { App, Button, Divider, Popconfirm, Select } from "antd";
import { ReloadOutlined } from "@ant-design/icons";
import { useProductsAuditStatesMapVal } from "@/hooks/useProductsSets";
import { useTranslation } from "react-i18next";
import useProductsStore, {
postAgencyProductsAuditAction,
postAgencyAuditAction,
} from "@/stores/Products/Index";
import { isEmpty, objectMapper } from "@/utils/commons";
import useAuthStore from "@/stores/Auth";
import RequireAuth from "@/components/RequireAuth";
import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from "@/config";
import dayjs from "dayjs";
import VendorSelector from "@/components/VendorSelector";
import AuditStateSelector from "@/components/AuditStateSelector";
import { usingStorage } from "@/hooks/usingStorage";
import AgencyPreview from "../Print/AgencyPreview";
import ExportDocxBtn from "../Print/ExportDocxBtn";
const Header = ({ refresh, ...props }) => {
const location = useLocation();
const isEditPage = location.pathname.includes("edit");
const showEditA = !location.pathname.includes("edit");
const showAuditA = !location.pathname.includes("audit");
const { travel_agency_id, use_year, audit_state } = useParams();
const { travelAgencyId } = usingStorage();
const { t } = useTranslation();
const isPermitted = useAuthStore((state) => state.isPermitted);
const [activeAgency, setActiveAgency] = useProductsStore((state) => [
state.activeAgency,
state.setActiveAgency,
]);
const [switchParams, setSwitchParams] = useProductsStore((state) => [
state.switchParams,
state.setSwitchParams,
]);
// const [activeAgencyState] = useProductsStore((state) => [state.activeAgencyState]);
const stateMapVal = useProductsAuditStatesMapVal();
const { message, notification } = App.useApp();
const navigate = useNavigate();
const yearOptions = [];
const currentYear = switchParams.use_year || dayjs().year();
const baseYear = use_year
? Number(use_year === "all" ? currentYear : use_year)
: currentYear;
for (let i = currentYear - 5; i <= baseYear + 5; i++) {
yearOptions.push({ label: i, value: i });
}
const [param, setParam] = useState({
pick_year: baseYear,
pick_agency: travel_agency_id,
});
const [pickYear, setPickYear] = useState(baseYear);
const [pickAgency, setPickAgency] = useState({
value: activeAgency.travel_agency_id,
label: activeAgency.travel_agency_name,
});
const [pickAuditState, setPickAuditState] = useState();
useEffect(() => {
const _param = objectMapper(param, {
pick_year: "use_year",
pick_agency: "travel_agency_id",
pick_state: "audit_state",
});
setSwitchParams({
..._param,
travel_agency_id: _param?.travel_agency_id || travelAgencyId,
});
refresh(param);
return () => {};
}, [param]);
const emptyPickState = { value: "", label: t("products:State") };
useEffect(() => {
const baseState = audit_state
? audit_state === "all"
? emptyPickState
: stateMapVal[`${audit_state}`]
: emptyPickState;
if (isEmpty(pickAuditState)) {
setPickAuditState(baseState);
}
return () => {};
}, [audit_state, stateMapVal]);
const handleYearChange = (value) => {
setPickYear(value);
setParam((pre) => ({ ...pre, ...{ pick_year: value } }));
};
const handleAuditStateChange = (labelValue) => {
const { value } = labelValue || emptyPickState;
setPickAuditState(labelValue || emptyPickState);
setParam((pre) => ({ ...pre, ...{ pick_state: value } }));
};
const handleAgencyChange = ({ label, value }) => {
setPickAgency({ label, value });
setActiveAgency({ travel_agency_id: value, travel_agency_name: label });
setParam((pre) => ({ ...pre, ...{ pick_agency: value } }));
};
const handleAuditAgency = (state) => {
// const s = Object.keys(agencyProducts).map((typeKey) => {
// });
postAgencyProductsAuditAction(state, {
travel_agency_id: activeAgency.travel_agency_id,
use_year: switchParams.use_year,
})
.then((json) => {
if (json.errcode === 0) {
message.success(json.errmsg);
if (typeof refresh === "function") {
refresh(param);
}
}
})
.catch((ex) => {
notification.error({
message: "Notification",
description: ex.message,
placement: "top",
duration: 4,
});
});
};
const handleSubmitForAudit = () => {
postAgencyAuditAction(activeAgency.travel_agency_id, switchParams.use_year)
.then((json) => {
if (json.errcode === 0) {
message.success(t("Success"));
if (typeof refresh === "function") {
refresh(param);
const auditPagePath = isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${switchParams.use_year}/all/audit`
: isPermitted(PERM_PRODUCTS_OFFER_PUT)
? `/products/audit`
: "";
navigate(auditPagePath);
}
}
})
.catch((ex) => {
notification.error({
message: "Notification",
description: ex.message,
placement: "top",
duration: 4,
});
});
};
return (
<div className="flex justify-end items-center gap-4 h-full">
<div className="grow">
<h2 className="m-0 leading-tight">
{isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? (
<VendorSelector
value={{
label: activeAgency.travel_agency_name,
value: activeAgency.travel_agency_id,
}}
onChange={handleAgencyChange}
allowClear={false}
mode={null}
className="w-72"
size="large"
variant={"borderless"}
/>
) : (
activeAgency.travel_agency_name
)}
<Divider type={"vertical"} />
<Select
options={yearOptions}
variant={"borderless"}
className="w-24"
size="large"
value={pickYear}
onChange={handleYearChange}
/>
<Divider type={"vertical"} />
<AuditStateSelector
variant={"borderless"}
className="w-32"
size="large"
value={pickAuditState}
onChange={handleAuditStateChange}
/>
{/* <Divider type={'vertical'} />
{(use_year || '').replace('all', '')} */}
<Button
onClick={() => refresh(param)}
type="text"
className="text-primary round-none"
icon={<ReloadOutlined />}
/>
</h2>
</div>
<AgencyPreview params={switchParams} />
<ExportDocxBtn params={switchParams} />
{/* {activeAgencyState === 0 && ( */}
<>
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
<Popconfirm
title={t("products:sureSubmitAudit")}
onConfirm={handleSubmitForAudit}
okText={t("Yes")}
placement={"bottomLeft"}
>
<Button size="small" type={"primary"}>
{t("Submit")}
{t("Audit")}
</Button>
</Popconfirm>
</RequireAuth>
</>
{/* )} */}
{showEditA && (
<Link
className="px-2"
to={
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${pickYear}/all/edit`
: `/products/edit`
}
>
{t("Edit")}
</Link>
)}
{showAuditA && (
<Link
className="px-2"
to={
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/audit`
: `/products/audit`
}
>
{t("products:AuditRes")}
</Link>
)}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button
size="small"
type={"primary"}
onClick={() => handleAuditAgency("1")}
>
{t("products:auditStateAction.Published")}
</Button>
</RequireAuth>
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditAgency('2')}>
{t('products:auditStateAction.Approved')}
</Button> */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button
size="small"
type={"primary"}
danger
ghost
onClick={() => handleAuditAgency("3")}
>
{t("products:auditStateAction.Rejected")}
</Button>
</RequireAuth>
</div>
);
};
export default Header;

@ -1,135 +0,0 @@
import { useState, useEffect } from 'react';
import { Form, Modal, Input, Button } from 'antd';
import { objectMapper } from '@/utils/commons';
import { useTranslation } from 'react-i18next';
import ProductsTypesSelector from '@/components/ProductsTypesSelector';
import useProductsStore from '@/stores/Products/Index';
import { useNewProductRecord, useProductsTypesMapVal } from '@/hooks/useProductsSets';
import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_NEW } from '@/config';
export const NewProductsForm = ({ initialValues, onFormInstanceReady, ...props }) => {
const { t } = useTranslation('products');
const [form] = Form.useForm();
useEffect(() => {
onFormInstanceReady(form);
}, []);
const [pickType, setPickType] = useState({ value: '6' });
const onValuesChange = (changeValues, allValues) => {
if ('products_type' in changeValues) {
setPickType(changeValues.products_type);
}
};
return (
<Form layout='horizontal' form={form} name='new_product_in_modal' initialValues={initialValues} onValuesChange={onValuesChange}>
<Form.Item name={`products_type`} label={t('products:ProductType')} rules={[{ required: true }]} tooltip={false}>
<ProductsTypesSelector maxTagCount={1} mode={null} placeholder={t('common:All')} />
</Form.Item>
<Form.Item name={`title`} label={t('products:Title')} rules={[{ required: true }]} tooltip={t(`FormTooltip.NewTitle.${pickType.value}`)} dependencies={['products_type']}>
{/* ${pickType.value} */}
<Input placeholder={t(`FormTooltip.NewTitle.${pickType.value}`)} />
</Form.Item>
</Form>
);
};
const formValuesMapper = (values) => {
const destinationObject = {
'products_types': {
key: 'products_types',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '-1';
},
},
};
let dest = {};
const { ...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 NewProductModal = ({ initialValues }) => {
const { t } = useTranslation();
const [formInstance, setFormInstance] = useState();
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
const [switchParams] = useProductsStore((state) => [state.switchParams]);
const [open, setOpen] = useState(false);
const [copyLoading, setCopyLoading] = useState(false);
const productsTypesMapVal = useProductsTypesMapVal();
const newProduct = useNewProductRecord();
const { language } = useDefaultLgc();
const handelAddProduct = (param) => {
const copyNewProduct = structuredClone(newProduct);
copyNewProduct.info.title = param.title;
copyNewProduct.info.product_title = param.title;
copyNewProduct.info.product_type_id = productsTypesMapVal[param.products_type.value].value;
copyNewProduct.info.product_type_name = productsTypesMapVal[param.products_type.value].label;
copyNewProduct.lgc_details[0].lgc = language;
copyNewProduct.lgc_details[0].title = param.title;
copyNewProduct.quotation[0].use_dates_start = `${switchParams.use_year}-01-01`;
copyNewProduct.quotation[0].use_dates_end = `${switchParams.use_year}-12-31`;
setEditingProduct(copyNewProduct);
// if (typeof onSubmit === 'function') {
// onSubmit();
// }
setOpen(false);
return false;
};
return (
<>
<RequireAuth subject={PERM_PRODUCTS_NEW}>
<Button size='small' type={'primary'} onClick={() => setOpen(true)}>
{t('New')}
{t('products:#')}
</Button>
</RequireAuth>
<Modal
width={600}
open={open}
title={`${t('common:New')}${t('products:#')}`}
okButtonProps={{
autoFocus: true,
}}
confirmLoading={copyLoading}
onCancel={() => {
// onCancel();
setOpen(false);
formInstance?.resetFields();
}}
destroyOnClose
onOk={async () => {
try {
const values = await formInstance?.validateFields();
// formInstance?.resetFields();
const dest = formValuesMapper(values);
handelAddProduct(dest);
} catch (error) {
console.log('Failed:', error);
}
}}>
<NewProductsForm
initialValues={initialValues}
onFormInstanceReady={(instance) => {
setFormInstance(instance);
}}
/>
</Modal>
</>
);
};
export default NewProductModal;

@ -1,109 +0,0 @@
import { useState } from 'react'
import { Input, Space } from 'antd'
const PriceCompactInput = (props) => {
const { id, value = {}, onChange } = props
const [numberStart, setNumberStart] = useState(0)
const [numberEnd, setNumberEnd] = useState(0)
const [audultPrice, setAudultPrice] = useState(0)
const [childrenPrice, setChildrenPrice] = useState(0)
const triggerChange = (changedValue) => {
onChange?.({
numberStart,
numberEnd,
audultPrice,
childrenPrice,
...value,
...changedValue,
})
}
const onNumberStartChange = (e) => {
const newNumber = parseInt(e.target.value || '0', 10)
if (Number.isNaN(newNumber)) {
return
}
if (!('numberStart' in value)) {
setNumberStart(newNumber)
}
triggerChange({
numberStart: newNumber,
})
}
const onNumberEndChange = (e) => {
const newNumber = parseInt(e.target.value || '0', 10)
if (Number.isNaN(newNumber)) {
return
}
if (!('numberEnd' in value)) {
setNumberEnd(newNumber)
}
triggerChange({
numberEnd: newNumber,
})
}
const onAudultPriceChange = (e) => {
const newNumber = parseInt(e.target.value || '0', 10)
if (Number.isNaN(newNumber)) {
return
}
if (!('audultPrice' in value)) {
setAudultPrice(newNumber)
}
triggerChange({
audultPrice: newNumber,
})
}
const onChildrenPriceChange = (e) => {
const newNumber = parseInt(e.target.value || '0', 10)
if (Number.isNaN(newNumber)) {
return
}
if (!('childrenPrice' in value)) {
setChildrenPrice(newNumber)
}
triggerChange({
childrenPrice: newNumber,
})
}
return (
<Space.Compact id={id}>
<Input
type='text'
value={value.numberStart || numberStart}
onChange={onNumberStartChange}
style={{
width: '20%',
}}
/>
<Input
type='text'
value={value.numberEnd || numberEnd}
onChange={onNumberEndChange}
style={{
width: '40%',
}}
addonBefore='~'
/>
<Input
type='text'
value={value.audultPrice || audultPrice}
onChange={onAudultPriceChange}
style={{
width: '70%',
}}
addonBefore='成人价'
/>
<Input
type='text'
value={value.childrenPrice || childrenPrice}
onChange={onChildrenPriceChange}
style={{
width: '70%',
}}
addonBefore='儿童价'
/>
</Space.Compact>
)
}
export default PriceCompactInput

@ -1,165 +0,0 @@
import { useEffect, useState } from 'react';
import { App, Breadcrumb, Divider } from 'antd';
import { useTranslation } from 'react-i18next';
import { useProductsTypesMapVal, useNewProductRecord } from '@/hooks/useProductsSets';
import useProductsStore, { postProductsSaveAction } from '@/stores/Products/Index';
import useAuthStore from '@/stores/Auth';
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT, PERM_PRODUCTS_NEW } from '@/config';
import { isEmpty, objectMapper, pick, unique } from '@/utils/commons';
import ProductInfoForm from './ProductInfoForm';
import { usingStorage } from '@/hooks/usingStorage';
import Extras from './Extras';
import NewProductModal from './NewProductModal';
const ProductInfo = ({ ...props }) => {
const { t } = useTranslation();
const { notification, message } = App.useApp();
const { userId } = usingStorage();
const isPermitted = useAuthStore((state) => state.isPermitted);
const productsTypesMapVal = useProductsTypesMapVal();
const newProductRecord = useNewProductRecord();
const [loading, setLoading, appendNewProduct] = useProductsStore((state) => [state.loading, state.setLoading, state.appendNewProduct]);
const [activeAgency, editingProduct, setEditingProduct] = useProductsStore((state) => [state.activeAgency, state.editingProduct, state.setEditingProduct]);
const [extrasVisible, setExtrasVisible] = useState(false);
const [editablePerm, setEditablePerm] = useState(false);
const [infoEditable, setInfoEditable] = useState(false);
const [priceEditable, setPriceEditable] = useState(false);
useEffect(() => {
const topPerm = isPermitted(PERM_PRODUCTS_MANAGEMENT); //
const hasHT = (editingProduct?.info?.htid || 0) > 0;
// const hasAuditPer = isPermitted(PERM_PRODUCTS_OFFER_AUDIT);
const hasEditPer = isPermitted(PERM_PRODUCTS_INFO_PUT) || isPermitted(PERM_PRODUCTS_NEW); // || isPermitted(PERM_PRODUCTS_OFFER_PUT);
setEditablePerm(topPerm || hasEditPer);
// setEditable(topPerm || (hasAuditPer ? true : (!hasHT && hasEditPer)));
// setEditable(true); // debug: 0
// console.log('editable', hasAuditPer, (notAudit && hasEditPer));
setInfoEditable(topPerm || (!hasHT && hasEditPer));
const _priceEditable = [-1, 3].includes(activeAgency?.audit_state_id) || isEmpty(editingProduct?.info?.id);
const hasPricePer = isPermitted(PERM_PRODUCTS_OFFER_PUT);
// setPriceEditable(topPerm || (_priceEditable && hasPricePer));
setPriceEditable(topPerm || (hasPricePer));
// setPriceEditable(true); // debug: 0
const showExtras = topPerm && hasHT; // !isEmpty(editingProduct) &&
setExtrasVisible(showExtras);
setLgcEdits({});
setInfoEditStatus('');
setEditKeys([]);
return () => {};
}, [activeAgency, editingProduct]);
const [infoEditStatus, setInfoEditStatus] = useState('');
const [lgcEdits, setLgcEdits] = useState({});
// const [editChanged, setEditChanged] = useState({});
const [editKeys, setEditKeys] = useState([]);
const onValuesChange = (changedValues) => {
// console.log('onValuesChange', changedValues);
const changedKeys = objectMapper(changedValues, { 'city': 'city_id', 'dept': 'dept_id', 'product_title': 'title', 'lgc_details_mapped': 'lgc_details'});
setEditKeys(prev => unique([...prev, ...Object.keys(changedKeys)]));
// const preValues = pick(editingProduct.info, editKeys);
if ('product_title' in changedValues) {
setInfoEditStatus('2');
setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}});
}
if ('lgc_details_mapped' in changedValues) {
const lgc = Object.keys(changedValues.lgc_details_mapped)[0];
setLgcEdits({...lgcEdits, [lgc]: {'edit_status': '2'}});
} else if ('quotation' in changedValues) {
// edit_status
} else {
setInfoEditStatus('2');
}
};
const onSave = async (err, values, forms) => {
values.travel_agency_id = activeAgency.travel_agency_id;
const editChanged = pick(editingProduct.info, editKeys);
(editKeys.includes('lgc_details') ? editChanged.lgc_details = editingProduct.lgc_details.map(l => l.lgc) : false);
// console.log("editKeys pre values", editKeys, editChanged, '\neditingProduct', );
const copyNewProduct = structuredClone(newProductRecord);
const poster = {
// ...(topPerm ? { } : { 'audit_state': -1 }), // :
// "create_date": dayjs().format('YYYY-MM-DD HH:mm:ss'),
// "created_by": userId,
'travel_agency_id': activeAgency.travel_agency_id,
// "travel_agency_name": "",
'lastedit_changed': editChanged, // isEmpty(editChanged) ? "" : JSON.stringify(editChanged),
'edit_status': infoEditStatus || editingProduct.info.edit_status,
};
// console.log("ready to post", poster);
const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title',
const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, title: editingProduct.info.product_title, ...values.info, ...copyFields, ...poster };
// console.log('onSave', editingProduct.info, readyToSubInfo);
/** lgc_details */
const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
const mergedLgc = { ...prevLgcDetailsMapped, ...values.lgc_details_mapped, };
for (const lgcKey in lgcEdits) {
if (Object.prototype.hasOwnProperty.call(lgcEdits, lgcKey)) {
const element = lgcEdits[lgcKey];
mergedLgc[lgcKey].edit_status = element?.edit_status || values.lgc_details_mapped[lgcKey]?.edit_status || '2';
}
}
// console.log('before save', readyToSubInfo, '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
// return false; // debug: 0
// throw new Error("Test save");
/** 提交保存 */
setLoading(true);
const { success, result } = await postProductsSaveAction({
travel_agency_id: activeAgency.travel_agency_id,
info: readyToSubInfo,
lgc_details: Object.values(mergedLgc),
quotation: values.quotation.map((q) => ({ ...q, unit: Number(q.unit || q.unit_id), unit_id: Number(q.unit_id) })), // || editingProduct.quotation, // ,
}).catch((ex) => {
setLoading(false);
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
});
});
setLoading(false);
success ? message.success(t('Success')) : message.error(t('Failed'));
//
// result.quotation = isEmpty(result.quotation) ? editingProduct.quotation : result.quotation;
result.info.htid = editingProduct?.info?.htid;
appendNewProduct(result);
setEditingProduct(result);
};
return (
<>
<Breadcrumb
items={[
{ title: productsTypesMapVal[editingProduct?.info?.product_type_id]?.label || editingProduct?.info?.product_type_name },
{ title: editingProduct?.info?.title ?? t('New') },
// { title: 'htID: ' + editingProduct?.info?.htid },
// { title: 'ID: ' + editingProduct?.info?.id },
]}
/>
<Divider className='my-1' />
{isEmpty(editingProduct) ? (
<div className=' my-2'>
<NewProductModal />
</div>
) : (
<>
<h2>{t('products:EditComponents.info')}</h2>
<ProductInfoForm {...{ editablePerm, infoEditable, priceEditable, onValuesChange }} initialValues={editingProduct?.info} onSubmit={onSave} />
<Divider className='my-1' />
{extrasVisible && <Extras productId={editingProduct?.info?.id} />}
</>
)}
</>
);
};
export default ProductInfo;

@ -1,497 +0,0 @@
import { useEffect, useState } from 'react';
import { App, Form, Input, Row, Col, Select, Button, InputNumber, Checkbox } from 'antd';
import { objectMapper, isEmpty, isNotEmpty, pick } from '@/utils/commons';
import { useTranslation } from 'react-i18next';
import { useWeekdays } from '@/hooks/useDatePresets';
import DeptSelector from '@/components/DeptSelector';
import CitySelector from '@/components/CitySelector';
import { useProductsTypesFieldsets, PackageTypes } from '@/hooks/useProductsSets';
import useProductsStore from '@/stores/Products/Index';
import ProductInfoLgc from './ProductInfoLgc';
import ProductInfoQuotation from './ProductInfoQuotation';
import { useHTLanguageSetsMapVal } from '@/hooks/useHTLanguageSets';
const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditable, priceEditable, showSubmit, confirmText, formName, ...props }) => {
const { notification } = App.useApp();
const { t } = useTranslation('products');
const HTLanguageSetsMapVal = useHTLanguageSetsMapVal();
const [loading, editingProduct] = useProductsStore((state) => [state.loading, state.editingProduct]);
const weekdays = useWeekdays();
const [form] = Form.useForm();
const { sort, hides, fieldProps, fieldComProps } = {
sort: '',
fieldProps: '',
fieldComProps: '',
hides: [],
shows: [],
...props.fieldsConfig,
};
const filedsets = useProductsTypesFieldsets(editingProduct?.info?.product_type_id);
const shows = filedsets[0];
const [pickEditedInfo, setPickEditedInfo] = useState({}); //
// const [editable, setEditable] = useState(true);
const [formEditable, setFormEditable] = useState(true);
const [showSave, setShowSave] = useState(true);
useEffect(() => {
form.resetFields();
const _formValue = serverData2Form(editingProduct);
const readyFormVal = pick(_formValue, ['quotation', 'lgc_details_mapped','city', 'city_list', 'dept', 'display_to_c', 'sub_type_D'])
// form.setFieldsValue(serverData2Form(editingProduct));
// ! setFieldsValue
for (const _key in readyFormVal) {
form.setFieldValue(_key, readyFormVal[_key])
}
setPickEditedInfo({ ...pickEditedInfo, product_title: editingProduct?.info?.product_title });
setFormEditable(infoEditable || priceEditable);
// const editable0 = isEmpty(editingProduct) ? false : editablePerm; //
setShowSave(infoEditable || priceEditable);
// setEditable(editable0);
return () => {};
}, [editingProduct, editablePerm, infoEditable, priceEditable]);
const onFinish = (values) => {
console.log('Received values of form, origin form value: \n', values);
const dest = formValuesMapper2Server(values);
console.log('form value send to onSubmit:\n', dest);
if (typeof onSubmit === 'function') {
onSubmit(null, dest, values);
}
};
const onFinishFailed = ({ values, errorFields }) => {
console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields);
notification.warning({
message: '数据未填写完整',
// description: '',
placement: 'top',
duration: 4,
})
};
const handleReset = () => {
form.setFieldsValue({
// 'DateType': undefined,
});
if (typeof onReset === 'function') {
onReset();
}
};
const onIValuesChange = (changedValues, allValues) => {
const dest = formValuesMapper2Server(allValues);
// console.log('form onValuesChange', Object.keys(changedValues), changedValues);
if ('product_title' in changedValues) {
const editTitle = (changedValues.product_title);
setPickEditedInfo({ ...pickEditedInfo, product_title: editTitle });
}
if (typeof onValuesChange === 'function') {
onValuesChange(changedValues, dest);
}
};
const onFieldsChange = (hangedFields, allFields) => {
console.log('onFieldsChange', hangedFields, allFields);
};
return (
<>
<Form
form={form}
disabled={!formEditable}
name={formName || 'product_info'}
// preserve={false}
onFinish={onFinish}
onValuesChange={onIValuesChange}
// onFieldsChange={onFieldsChange}
initialValues={{ ...(editingProduct?.info || {}), sub_type_D: editingProduct?.info?.item_type || '' }}
onFinishFailed={onFinishFailed}
scrollToFirstError>
<Row>
{getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable: infoEditable })}
{/* {showSubmit && (
<Col flex='1 0 90px' className='flex justify-end items-start'>
<Space align='center'>
<Button size={'middle'} type='primary' htmlType='submit' loading={loading}>
{confirmText || t('common:Save')}
</Button>
</Space>
</Col>
)} */}
</Row>
{/* <Divider className='my-1' /> */}
<Form.Item
className='mb-0'
name={'lgc_details_mapped'}
rules={[
() => ({
transform(value) {
return Object.values(value).filter((_v) => !isEmpty(_v));
},
validator: async (_, valueArr) => {
const invalidLgcName = valueArr
.filter((l) => isEmpty(l.title))
.map((x) => HTLanguageSetsMapVal[x.lgc].label)
.join(', ');
if (isNotEmpty(invalidLgcName)) {
// Please complete multi -language information
return Promise.reject(new Error(`请完善多语种信息: ${invalidLgcName}`));
}
return Promise.resolve();
},
}),
]}>
<ProductInfoLgc editable={infoEditable} formInstance={form} pickEditedInfo={pickEditedInfo} />
</Form.Item>
<Form.Item name='quotation'>
<ProductInfoQuotation editable={priceEditable} />
</Form.Item>
<Form.Item hidden name={'id'} label={'ID'}>
<Input />
</Form.Item>
{/* <Form.Item hidden name={'title'} label={'title'}>
<Input />
</Form.Item> */}
{showSave && (
<Form.Item>
<div className='flex justify-around'>
<Button type='primary' htmlType='submit' loading={loading}>
{t('common:Save')}
</Button>
</div>
</Form.Item>
)}
</Form>
</>
);
};
function getFields(props) {
const { fieldProps, fieldComProps, form, t, dataSets } = props;
// console.log('getFields', props.initialValue);
const styleProps = {};
const editableProps = (name) => {
// const disabled = props.ignoreEditable ? false : (isEmpty(props.initialValue?.[name]) && props.editable ? false : true)
const disabled = !props.editable;
return { disabled, className: disabled ? '!text-slate-500' : '' };
};
const bigCol = 4 * 2;
const midCol = 8;
const layoutProps = {
// gutter: { xs: 8, sm: 8, lg: 16 },
lg: { span: 6 },
md: { span: 8 },
sm: { span: 12 },
xs: { span: 24 },
};
const item = (name, sort = 0, render, col) => {
const customCol = col || midCol;
const mdCol = customCol * 2;
return {
'key': '',
sort,
name,
render,
'hide': false,
'col': { lg: { span: customCol }, md: { span: mdCol < 8 ? 10 : mdCol > 24 ? 24 : mdCol }, flex: mdCol < 8 ? '1 0' : '' },
};
};
let baseChildren = [];
baseChildren = [
item(
'product_title',
99,
<Form.Item name='product_title' label={t('Title')} {...fieldProps.product_title} rules={[{ required: true }]} tooltip={false}>
<Input allowClear {...fieldComProps.product_title} {...styleProps} {...editableProps('product_title')} />
</Form.Item>,
fieldProps?.product_title?.col || midCol
),
item(
'code',
99,
<Form.Item name='code' label={t('Code')} {...fieldProps.code} rules={[{ required: true }]} tooltip={false}>
<Input allowClear {...fieldComProps.code} {...styleProps} {...editableProps('code')} />
</Form.Item>,
fieldProps?.code?.col || midCol
),
item(
'city',
99,
<Form.Item name='city' label={t('City')} {...fieldProps.city} rules={[{ required: true }]} tooltip={t('FormTooltip.City')}>
<CitySelector {...styleProps} {...editableProps('city_id')} placeholder={t('FormTooltip.City')} />
</Form.Item>,
fieldProps?.city?.col || midCol
),
item(
'dept',
99,
<Form.Item name='dept' label={t('Dept')} {...fieldProps.dept}>
<DeptSelector labelInValue={false} isLeaf {...styleProps} {...editableProps('dept')} />
</Form.Item>,
fieldProps?.dept?.col || midCol
),
item(
'duration',
99,
<Form.Item name='duration' label={t('Duration')} {...fieldProps.duration} rules={[{ required: true, type: 'number', min: 0}]} tooltip={false}>
<InputNumber suffix={'H'} max={24} {...styleProps} {...editableProps('duration')} />
{/* <Input allowClear {...fieldComProps.duration} suffix={'H'} /> */}
</Form.Item>,
fieldProps?.duration?.col || midCol
),
item(
'city_list',
99,
<Form.Item name='city_list' label={t('多城市')} tooltip={t('把产品绑定到多个城市')}>
<CitySelector mode='multiple' maxTagCount={10} {...styleProps} {...editableProps('city_list')} />
</Form.Item>,
fieldProps?.city_list?.col || midCol
),
item(
'km',
99,
<Form.Item name='km' label={t('KM')} {...fieldProps.km} rules={[{ required: true, },]} tooltip={t('FormTooltip.KM')}>
<InputNumber suffix={'KM'} min={0.1} {...styleProps} {...editableProps('km')} placeholder={t('FormTooltip.KM')} />
</Form.Item>,
fieldProps?.km?.col || midCol
),
item(
'recommends_rate',
99,
<Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}>
{/* <Input placeholder={t('RecommendsRate')} allowClear /> */}
<InputNumber style={{width: '100%'}} {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} />
{/* <Select
{...styleProps}
{...editableProps('recommends_rate')}
style={{ width: '100%' }}
labelInValue={false}
options={[
{ value: 1, label: 'Top 1' },
{ value: 2, label: 'Top 2' },
{ value: 3, label: 'Top 3' },
{ value: 4, label: '4' },
{ value: 5, label: '5' },
]}
/> */}
</Form.Item>,
fieldProps?.recommends_rate?.col || (props.shows.includes('sort_order') ? midCol/2 : midCol)
),
item(
'sort_order',
99,
<Form.Item name='sort_order' label={t('SortOrder')} {...fieldProps.sort_order} >
<InputNumber style={{width: '100%'}} {...styleProps} {...editableProps('sort_order')} max={1000} />
</Form.Item>,
fieldProps?.sort_order?.col || midCol/2
),
item(
'display_to_c',
99,
<Form.Item
name='display_to_c'
label={t('DisplayToC')}
{...fieldProps.display_to_c}
tooltip={t('FormTooltip.DisplayToC')}
// rules={[
// () => ({
// validator(_, value) {
// if ((value || []).includes(153002) && !(value || []).includes(153001)) {
// return Promise.reject(new Error(''));
// }
// return Promise.resolve();
// },
// }),
// ]}
>
{/* <Checkbox.Group
options={[
{ value: 153001, label: '报价信不显示' },
{ value: 153002, label: '计划不显示' },
]}
/> */}
<Select
labelInValue={false}
options={[
{ value: '153001', label: '在计划显示,不在报价信显示' },
{ value: '0', label: '计划和报价信都要显示' },
{ value: '153001, 153002', label: '计划和报价信都不用显示' },
]}
{...styleProps}
{...editableProps('display_to_c')}
/>
</Form.Item>,
fieldProps?.display_to_c?.col || midCol
),
item(
'sub_type_D',
99,
<Form.Item
name='sub_type_D'
label={t('subTypeD')}
{...fieldProps.sub_type_D}
rules={[{ required: true }]}
// tooltip={t('FormTooltip.subTypeD')}
>
<Select
labelInValue={false}
options={PackageTypes}
{...styleProps}
{...editableProps('sub_type_D')}
/>
</Form.Item>,
fieldProps?.sub_type_D?.col || midCol
),
item(
'open_weekdays',
99,
<Form.Item name='open_weekdays' label={t('OpenWeekdays')} {...fieldProps.open_weekdays} tooltip={false}>
<Checkbox.Group options={dataSets.weekdays} {...styleProps} {...editableProps('open_weekdays')} />
</Form.Item>,
fieldProps?.open_weekdays?.col || 24
),
item(
'remarks',
99,
<Form.Item name='remarks' label={t('Remarks')} {...fieldProps.remarks} tooltip={t('FormTooltip.Remarks')}>
<Input.TextArea allowClear rows={2} maxLength={2000} showCount {...fieldComProps.remarks} {...styleProps} {...editableProps('remarks')} />
</Form.Item>,
fieldProps?.remarks?.col || 24
),
];
baseChildren = baseChildren
.map((x) => {
x.hide = false;
if (props.sort === undefined) {
return x;
}
const tmpSort = props.sort;
for (const key in tmpSort) {
if (Object.prototype.hasOwnProperty.call(tmpSort, key)) {
if (x.name === key) {
x.sort = tmpSort[key];
}
}
}
return x;
})
.map((x) => {
if (props.hides.length === 0 && props.shows.length === 0) {
return x;
}
if (props.hides.length === 0) {
x.hide = !props.shows.includes(x.name);
} else if (props.shows.length === 0) {
x.hide = props.hides.includes(x.name);
}
return x;
})
.filter((x) => !x.hide)
.sort((a, b) => {
return a.sort < b.sort ? -1 : 1;
});
const children = [];
const leftStyle = {}; // { borderRight: '1px solid #dedede' };
for (let i = 0; i < baseChildren.length; i++) {
let style = {}; // { padding: '0px 2px' };
style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style;
style = !baseChildren[i].hide ? { ...style, display: 'block' } : { ...style, display: 'none' };
const Item = (
<Col key={String(i)} style={style} {...baseChildren[i].col} className='px-1 shrink-0 grow'>
{baseChildren[i].render}
</Col>
);
children.push(Item);
}
return children;
}
const serverData2Form = (productItem) => {
const infoForRender = {
city: productItem?.info?.city_id ? { value: productItem?.info?.city_id, label: productItem?.info?.city_name } : undefined,
dept: { value: productItem?.info?.dept_id, label: productItem?.info?.dept_name },
display_to_c: productItem.info?.display_to_c || '0',
city_list: productItem?.info?.city_list ? productItem?.info?.city_list?.map((ele) => ({ value: ele.id, label: ele.name })) : undefined,
sub_type_D: productItem?.info?.item_type || '',
};
const lgc_details_mapped = (productItem?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
const quotation = productItem?.quotation || [];
return { ...productItem, ...(productItem?.info || {}), ...infoForRender, lgc_details_mapped };
};
const formValuesMapper2Server = (values) => {
const destinationObject = {
'city': [
{ key: 'city_id', transform: (value) => value?.value || value?.key || '' },
{ key: 'city_name', transform: (value) => value?.label || '' },
],
'city_list': [
{ key: 'city_list', transform: (value) => {
return value.map(option => {
return {
id: option?.value || option?.key || '',
name: option?.label || ''
}
})
}},
],
'dept': { key: 'dept_id', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') },
'open_weekdays': { key: 'open_weekdays', transform: (value) => (Array.isArray(value) ? value.join(',') : value) },
// 'recommends_rate': { key: 'recommends_rate', transform: (value) => ((typeof value === 'string' || typeof value === 'number') ? value : value?.value || value?.key || '') },
// 'lgc_details': [
// {
// key: 'lgc_details',
// transform: (value) => {
// const _val = value.filter((s) => s !== undefined).map((e) => ({ title: '', ...e, description: e.description || '' }));
// return _val || '';
// },
// },
// {
// key: 'lgc_details_mapped_tmp',
// transform: (value) => {
// const _val = value.filter((s) => s !== undefined);
// return _val.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
// },
// },
// ],
'lgc_details_mapped': [
{
key: 'lgc_details',
transform: (value) => {
const valueArr = Object.values(value)
.filter((_v) => !isEmpty(_v.lgc))
.map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' }));
return valueArr;
},
},
{
key: 'lgc_details_mapped',
transform: (value) => {
const valueArr = Object.values(value)
.filter((_v) => !isEmpty(_v.lgc))
.map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' }));
return valueArr.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
},
},
],
'product_title': { key: 'title' },
'sub_type_D': { key: 'item_type'},
'sort_order': { key: 'sort_order'},
};
let dest = {};
const { city, dept, product_title, sub_type_D, ...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]);
const { lgc_details, lgc_details_mapped, quotation, ...info } = dest;
return { info, lgc_details, lgc_details_mapped, quotation };
};
export default InfoForm;

@ -1,185 +0,0 @@
import { useEffect, useState } from 'react';
import { Input, Tabs, Modal, Select, Form } from 'antd';
import useProductsStore from '@/stores/Products/Index';
import { useHTLanguageSets, useHTLanguageSetsMapVal } from '@/hooks/useHTLanguageSets';
import { useTranslation } from 'react-i18next';
import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
import { cloneDeep, isEmpty, isNotEmpty } from '@/utils/commons';
const ProductInfoLgc = ({ editable, formInstance, pickEditedInfo, ...props }) => {
const { t } = useTranslation('products');
const { language: languageHT } = useDefaultLgc();
const HTLanguageSetsMapVal = useHTLanguageSetsMapVal();
const allLgcOptions = useHTLanguageSets();
const [editingProduct] = useProductsStore((state) => [state.editingProduct]);
const [activeKey, setActiveKey] = useState();
const [items, setItems] = useState([]);
useEffect(() => {
formInstance.setFieldValue(['lgc_details_mapped', '2', 'title'], pickEditedInfo.product_title);
return () => {};
}, [pickEditedInfo.product_title]);
useEffect(() => {
const existsLgc = (editingProduct?.lgc_details || []).map((ele, li) => ({
...ele,
label: HTLanguageSetsMapVal[ele.lgc]?.label || ele.lgc,
// key: `${editingProduct.info.id}-${ele.id}`,
key: ele.lgc,
closable: false, // isPermitted(PERM_PRODUCTS_MANAGEMENT) ? true : false,
forceRender: true,
children: (
<Form.Item noStyle key={`${editingProduct.info.id}-${ele.id}`}>
<Form.Item name={['lgc_details_mapped', `${ele.lgc}`, 'title']} label={t('products:Title')} initialValue={ele.title} rules={[{ required: true }]} tooltip={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)}>
<Input
className={' !text-slate-600'}
allowClear
placeholder={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)}
// onChange={(e) => handleChange('title', e.target.value)}
// disabled={ignoreEditable ? false : (!isEmpty(ele.title) || !editable)}
// disabled={ignoreEditable ? false : !editable}
disabled={!editable}
/>
</Form.Item>
<Form.Item name={['lgc_details_mapped', `${ele.lgc}`, 'descriptions']} label={t('products:Description')} initialValue={ele.descriptions} tooltip={t('FormTooltip.Description')}>
<Input.TextArea
className={'!text-slate-600'}
rows={3} maxLength={2000} showCount
allowClear
// onChange={(e) => handleChange('description', e.target.value)}
// disabled={ignoreEditable ? false : (!isEmpty(ele.descriptions) || !editable)}
// disabled={ignoreEditable ? false : !editable}
disabled={!editable}
/>
</Form.Item>
<Form.Item hidden name={['lgc_details_mapped', `${ele.lgc}`, 'lgc']} initialValue={ele.lgc}>
<Input />
</Form.Item>
<Form.Item hidden name={['lgc_details_mapped', `${ele.lgc}`, 'id']} initialValue={ele.id}>
<Input />
</Form.Item>
</Form.Item>
),
}));
setItems(existsLgc);
const pageDefaultLgcI = (editingProduct?.lgc_details || []).findIndex((ele) => ele.lgc === languageHT);
setActiveKey(existsLgc?.[pageDefaultLgcI || 0]?.key);
// formInstance.validateFields();
const filterLgcOptions = allLgcOptions.filter((ele) => !existsLgc.some((item) => `${item.lgc}` === ele.value));
setLgcOptions(filterLgcOptions);
return () => {};
}, [editingProduct]);
const onLgcTabChange = (newActiveKey) => {
setActiveKey(newActiveKey);
};
const addLgc = (lgcItem) => {
// const currentLgcOptions = structuredClone(lgcOptions);
const currentLgcOptions = cloneDeep(lgcOptions);
currentLgcOptions.splice(
currentLgcOptions.findIndex((ele) => ele.value === lgcItem.value),
1
);
setLgcOptions(currentLgcOptions);
const newActiveKey = lgcItem.value;
const newPanes = [...items];
newPanes.push({
...lgcItem,
forceRender: true,
key: lgcItem.value,
children: (
<Form.Item noStyle>
<Form.Item name={['lgc_details_mapped', `${lgcItem.value}`, 'title']} preserve={false} label={t('products:Title')} rules={[{ required: true }]} tooltip={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)}>
<Input allowClear placeholder={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)} />
</Form.Item>
<Form.Item name={['lgc_details_mapped', `${lgcItem.value}`, 'descriptions']} preserve={false} label={t('products:Description')} tooltip={t('FormTooltip.Description')}>
<Input.TextArea rows={3} maxLength={2000} showCount allowClear />
</Form.Item>
<Form.Item hidden name={['lgc_details_mapped', `${lgcItem.value}`, 'lgc']} preserve={false} initialValue={lgcItem.value}>
<Input />
</Form.Item>
<Form.Item hidden name={['lgc_details_mapped', `${lgcItem.value}`, 'id']} preserve={false} initialValue={''}>
<Input />
</Form.Item>
</Form.Item>
),
});
setItems(newPanes);
setActiveKey(newActiveKey);
setSelectNewLgc(null);
};
const remove = (targetKey) => {
let newActiveKey = activeKey;
let lastIndex = -1;
items.forEach((item, i) => {
if (item.key === targetKey) {
lastIndex = i - 1;
}
});
const newPanes = items.filter((item) => item.key !== targetKey);
if (newPanes.length && newActiveKey === targetKey) {
if (lastIndex >= 0) {
newActiveKey = newPanes[lastIndex].key;
} else {
newActiveKey = newPanes[0].key;
}
}
setItems(newPanes);
setActiveKey(newActiveKey);
setLgcOptions([...lgcOptions, ...items.filter((item) => item.key === targetKey)]);
};
const onEdit = (targetKey, action) => {
if (action === 'add') {
setNewLgcModalVisible(true);
} else {
remove(targetKey);
formInstance.validateFields(['lgc_details_mapped']);
}
};
const [newLgcModalVisible, setNewLgcModalVisible] = useState(false);
const [selectNewLgc, setSelectNewLgc] = useState();
const [lgcOptions, setLgcOptions] = useState(allLgcOptions);
const handleOk = () => {
addLgc(selectNewLgc);
setNewLgcModalVisible(false);
};
const handleCancel = () => {
setNewLgcModalVisible(false);
};
const onSelectNewLgc = (lgcItem) => {
setSelectNewLgc(lgcItem);
};
return (
<>
<Tabs
type='editable-card'
size='small'
onChange={onLgcTabChange}
activeKey={activeKey}
onEdit={onEdit}
items={items}
hideAdd={isEmpty(lgcOptions) || !editable}
tabPosition='top'
/>
<Modal title={t('LgcModal.title')} open={newLgcModalVisible} onOk={handleOk} onCancel={() => setNewLgcModalVisible(false)} destroyOnClose>
<Select
showSearch
labelInValue
style={{ width: '80%' }}
placeholder={t('LgcModal.placeholder')}
optionFilterProp='children'
options={lgcOptions}
onChange={onSelectNewLgc}
value={selectNewLgc}
/>
</Modal>
</>
);
};
export default ProductInfoLgc;

@ -1,687 +0,0 @@
import { useState, useEffect } from "react";
import {
Table,
Form,
Modal,
Button,
Radio,
Input,
Flex,
Card,
InputNumber,
Checkbox,
DatePicker,
Space,
App,
Popconfirm,
Tooltip,
} from "antd";
import { useTranslation } from "react-i18next";
import {
CloseOutlined,
StarTwoTone,
PlusOutlined,
QuestionCircleOutlined,
} from "@ant-design/icons";
import { useDatePresets } from "@/hooks/useDatePresets";
import dayjs from "dayjs";
import useProductsStore from "@/stores/Products/Index";
import PriceCompactInput from "@/views/products/Detail/PriceCompactInput";
import { formatGroupSize } from "@/hooks/useProductsSets";
const { RangePicker } = DatePicker;
const defaultPriceValue = {
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
};
const getYearRange = (year) => [
dayjs().year(year).startOf("y"),
dayjs().year(year).endOf("y"),
];
const generateDefinitionValue = (year) => ({
useDateList: [{ useDate: getYearRange(year) }],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [defaultPriceValue],
});
const ProductInfoQuotation = ({ editable, ...props }) => {
const { onChange } = props;
const { t } = useTranslation();
const [
quotationList,
newEmptyQuotation,
appendQuotationList,
saveOrUpdateQuotation,
deleteQuotation,
switchParams,
] = useProductsStore((state) => [
state.quotationList,
state.newEmptyQuotation,
state.appendQuotationList,
state.saveOrUpdateQuotation,
state.deleteQuotation,
state.switchParams,
]);
const batchSetupInitialValues = {
defList: [
//
{
useDateList: [{ useDate: getYearRange(switchParams.use_year) }],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [
{
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 3,
numberEnd: 4,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 5,
numberEnd: 6,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 7,
numberEnd: 9,
audultPrice: 0,
childrenPrice: 0,
},
},
],
},
//
{
useDateList: [
{
useDate: [
dayjs().year(switchParams.use_year).subtract(2, "M").startOf("M"),
dayjs().year(switchParams.use_year).endOf("M"),
],
},
],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [
{
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 3,
numberEnd: 4,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 5,
numberEnd: 6,
audultPrice: 0,
childrenPrice: 0,
},
},
{
priceInput: {
numberStart: 7,
numberEnd: 9,
audultPrice: 0,
childrenPrice: 0,
},
},
],
},
],
};
const [defaultUseDates, setDefaultUseDates] = useState(
getYearRange(switchParams.use_year)
);
const [defaultDefinitionValue, setDefaultDefinitionValue] = useState(
generateDefinitionValue(switchParams.use_year)
);
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false);
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false);
const [groupAllSize, setGroupAllSize] = useState(false);
const [groupMaxUnlimit, setGroupMaxUnlimit] = useState(false);
const { notification } = App.useApp();
const [quotationForm] = Form.useForm();
const [batchSetupForm] = Form.useForm();
const datePresets = useDatePresets();
useEffect(() => {
setDefaultUseDates(getYearRange(switchParams.use_year));
setDefaultDefinitionValue(generateDefinitionValue(switchParams.use_year));
}, [switchParams]);
const triggerChange = (changedValue) => {
onChange?.(changedValue);
};
const onQuotationSeleted = async (quotation) => {
// start, end RangePicker
quotation.use_dates = [
dayjs(quotation.use_dates_start),
dayjs(quotation.use_dates_end),
];
quotation.weekdayList = quotation.weekdays.split(",");
quotationForm.setFieldsValue(quotation);
setQuotationModalOpen(true);
};
const onNewQuotation = () => {
setGroupAllSize(false); //
const emptyQuotation = newEmptyQuotation(defaultUseDates);
quotationForm.setFieldsValue(emptyQuotation);
setQuotationModalOpen(true);
};
const onQuotationFinish = (values) => {
const newList = saveOrUpdateQuotation(values);
triggerChange(newList);
setQuotationModalOpen(false);
};
const onBatchSetupFinish = () => {
const defList = batchSetupForm.getFieldsValue().defList;
const newList = appendQuotationList(defList);
triggerChange(newList);
setBatchSetupModalOpen(false);
};
const quotationColumns = [
// { title: 'id', dataIndex: 'id', width: 40, className: 'italic text-gray-400' }, // test: 0
// { title: 'WPI_SN', dataIndex: 'WPI_SN', width: 40, className: 'italic text-gray-400' }, // test: 0
{ title: t("products:adultPrice"), dataIndex: "adult_cost", width: "5rem" },
{
title: t("products:childrenPrice"),
dataIndex: "child_cost",
width: "5rem",
},
{ title: t("products:currency"), dataIndex: "currency", width: "4rem" },
{
title: (
<>
{t("products:unit_name")}{" "}
<Tooltip placement="top" title={t("products:FormTooltip.PriceUnit")}>
<QuestionCircleOutlined className="text-gray-500" />
</Tooltip>
</>
),
dataIndex: "unit_id",
width: "6rem",
render: (text) => t(`products:PriceUnit.${text}`), // (text === '0' ? '' : text === '1' ? '' : text),
},
{
title: t("products:group_size"),
dataIndex: "group_size",
width: "6rem",
render: (_, record) =>
formatGroupSize(record.group_size_min, record.group_size_max),
},
{
title: (
<>
{t("products:use_dates")}{" "}
<Tooltip
placement="top"
styles={{ body: { width: "24rem" } }}
title={t("products:FormTooltip.UseDates")}
>
<QuestionCircleOutlined className="text-gray-500" />
</Tooltip>
</>
),
dataIndex: "use_dates",
render: (_, record) =>
`${record.use_dates_start}-${record.use_dates_end}`,
},
{ title: t("products:Weekdays"), dataIndex: "weekdays", width: "4rem" },
{
title: t("products:operation"),
dataIndex: "operation",
width: "10rem",
render: (_, quotation) => {
return (
<Space>
<Button
type="link"
onClick={() => onQuotationSeleted(quotation)}
>
{t("Edit")}
</Button>
<Popconfirm
placement="topRight"
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
title="请确认"
description="你要删除这条价格吗?"
onConfirm={() => {
deleteQuotation(quotation)
.then((res) => {
triggerChange(res);
})
.catch((ex) => {
notification.error({
message: "Notification",
description: ex.message,
placement: "top",
duration: 4,
});
});
}}
>
<Button
type="link"
danger
>
{t("Delete")}
</Button>
</Popconfirm>
</Space>
);
},
},
];
return (
<>
<h2>{t("products:EditComponents.Quotation")}</h2>
<Table
size="small"
bordered
dataSource={quotationList}
columns={quotationColumns}
pagination={false}
/>
{editable && (
<Space>
<Button
onClick={() => onNewQuotation()}
type="primary"
ghost
style={{ marginTop: 16 }}
>
{t("products:addQuotation")}
</Button>
<Button
onClick={() => setBatchSetupModalOpen(true)}
type="primary"
ghost
style={{ marginTop: 16, marginLeft: 16 }}
>
批量设置
</Button>
</Space>
)}
<Modal
centered
title="批量设置价格"
width={"640px"}
open={isBatchSetupModalOpen}
onOk={() => onBatchSetupFinish()}
onCancel={() => setBatchSetupModalOpen(false)}
destroyOnHidden
forceRender
>
<Form
labelCol={{ span: 3 }}
wrapperCol={{ span: 20 }}
form={batchSetupForm}
name="batchSetupForm"
autoComplete="off"
initialValues={batchSetupInitialValues}
>
<Form.List name="defList">
{(fields, { add, remove }) => (
<Flex gap="middle" vertical>
{fields.map((field, index) => (
<Card
size="small"
title={
index == 0 ? "全年" : index == 1 ? "特殊时间段" : "其他"
}
key={field.key}
extra={
index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
)
}
>
<Form.Item label="币种" name={[field.name, "currency"]}>
<Radio.Group>
<Radio value="RMB">RMB</Radio>
<Radio value="USD">USD</Radio>
<Radio value="THB">THB</Radio>
<Radio value="JPY">JPY</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="类型" name={[field.name, "unitId"]}>
<Radio.Group>
<Radio value="0">每人</Radio>
<Radio value="1">每团</Radio>
</Radio.Group>
</Form.Item>
<Form.Item label="周末" name={[field.name, "weekend"]}>
<Checkbox.Group options={["5", "6", "7"]} />
</Form.Item>
<Form.Item label="有效期">
<Form.List name={[field.name, "useDateList"]}>
{(useDateFieldList, useDateOptList) => (
<Flex gap="middle" vertical>
{useDateFieldList.map((useDateField, index) => (
<Space key={useDateField.key}>
<Form.Item
noStyle
name={[useDateField.name, "useDate"]}
>
<RangePicker
style={{ width: "100%" }}
allowClear={true}
inputReadOnly={true}
presets={datePresets}
placeholder={["From", "Thru"]}
/>
</Form.Item>
{index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() =>
useDateOptList.remove(useDateField.name)
}
/>
)}
</Space>
))}
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() =>
useDateOptList.add({ useDate: defaultUseDates })
}
block
>
新增有效期
</Button>
</Flex>
)}
</Form.List>
</Form.Item>
<Form.Item label="人等">
<Form.List name={[field.name, "priceList"]}>
{(priceFieldList, priceOptList) => (
<Flex gap="middle" vertical>
{priceFieldList.map((priceField, index) => (
<Space key={priceField.key}>
<Form.Item
noStyle
name={[priceField.name, "priceInput"]}
>
<PriceCompactInput />
</Form.Item>
{index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() =>
priceOptList.remove(priceField.name)
}
/>
)}
</Space>
))}
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() =>
priceOptList.add(defaultPriceValue)
}
block
>
新增人等
</Button>
</Flex>
)}
</Form.List>
</Form.Item>
</Card>
))}
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => add(defaultDefinitionValue)}
block
>
新增设置
</Button>
</Flex>
)}
</Form.List>
</Form>
</Modal>
<Modal
centered
okButtonProps={{
autoFocus: true,
htmlType: "submit",
}}
title={t("products:EditComponents.Quotation")}
open={isQuotationModalOpen}
onCancel={() => setQuotationModalOpen(false)}
destroyOnHidden
forceRender
modalRender={(dom) => (
<Form
name="quotationForm"
form={quotationForm}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
className="max-w-2xl"
onFinish={onQuotationFinish}
autoComplete="off"
>
{dom}
</Form>
)}
>
<Form.Item name="id" className="hidden">
<Input />
</Form.Item>
<Form.Item name="key" className="hidden">
<Input />
</Form.Item>
<Form.Item name="fresh" className="hidden">
<Input />
</Form.Item>
<Form.Item
label={t("products:adultPrice")}
name="adult_cost"
rules={[
{
required: true,
message: t("products:Validation.adultPrice"),
},
]}
>
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item
label={t("products:childrenPrice")}
name="child_cost"
rules={[
{
required: true,
message: t("products:Validation.childrenPrice"),
},
]}
>
<InputNumber style={{ width: "100%" }} />
</Form.Item>
<Form.Item
label={t("products:currency")}
name="currency"
rules={[
{
required: true,
message: t("products:Validation.currency"),
},
]}
>
<Radio.Group>
<Radio value="RMB">RMB</Radio>
<Radio value="USD">USD</Radio>
<Radio value="THB">THB</Radio>
<Radio value="JPY">JPY</Radio>
</Radio.Group>
</Form.Item>
<Form.Item
label={t("products:unit_name")}
name="unit_id"
rules={[
{
required: true,
message: t("products:Validation.unit_name"),
},
]}
>
<Radio.Group>
<Radio value="0">每人</Radio>
<Radio value="1">每团</Radio>
</Radio.Group>
</Form.Item>
<Checkbox
onChange={(e) => {
if (e.target.checked) {
quotationForm.setFieldValue("group_size_min", 1);
quotationForm.setFieldValue("group_size_max", 1000);
setGroupAllSize(true);
} else {
setGroupAllSize(false);
}
}}
>
<span className="font-bold">不分人等(1~1000)</span>
</Checkbox>
<Form.Item
label={t("products:group_size")}
name="group_size_min"
rules={[
{
required: true,
message: t("products:Validation.group_size_min"),
},
{
validator: (_, value) => {
if (value > 1000 || value < 1) {
return Promise.reject("人等必须在 1~1000 之间");
}
return Promise.resolve();
},
},
]}
>
<InputNumber disabled={groupAllSize} style={{ width: "100%" }} />
</Form.Item>
<Checkbox
disabled={groupAllSize}
onChange={(e) => {
if (e.target.checked) {
quotationForm.setFieldValue("group_size_max", 1000);
setGroupMaxUnlimit(true);
} else {
setGroupMaxUnlimit(false);
}
}}
>
<span className="font-bold">不限(1000)</span>
</Checkbox>
<Form.Item
label={t("products:group_size")}
name="group_size_max"
rules={[
{
required: true,
message: t("products:Validation.group_size_max"),
},
{
validator: (_, value) => {
if (value > 1000 || value < 1) {
return Promise.reject("人等必须在 1~1000 之间");
}
return Promise.resolve();
},
},
]}
>
<InputNumber
disabled={groupAllSize || groupMaxUnlimit}
style={{ width: "100%" }}
/>
</Form.Item>
<Form.Item
label={t("products:use_dates")}
name="use_dates"
rules={[
{
required: true,
message: t("products:Validation.use_dates"),
},
]}
>
<RangePicker presets={datePresets} style={{ width: "100%" }} />
</Form.Item>
<Form.Item label={t("products:Weekdays")} name="weekdayList">
<Checkbox.Group options={["5", "6", "7"]} />
</Form.Item>
</Modal>
</>
);
};
export default ProductInfoQuotation;

@ -1,285 +0,0 @@
import { useState, useMemo } from 'react';
import { Button, Table, Popover, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { HT_HOST } from '@/config';
import { fetchJSON } from '@/utils/request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@/utils/commons';
import { usingStorage } from '@/hooks/usingStorage';
/**
* 产品价格日志
*/
const getPPLogAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_log`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格: 已发布的
*/
const getPPRunningAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_running`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格快照
*/
const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
const parseJson = (str) => {
let result;
if (str === null || str === undefined || str === '') {
return {};
}
try {
result = typeof str === 'string' ? JSON.parse(str) : str;
return Array.isArray(result) ? result.reduce((acc, cur) => ({ ...acc, ...cur }), {}) : result;
} catch (e) {
return {};
}
};
const statesForHideEdited = [1, 2];
export const columnsSets = (t, colorize = true) => [
{
key: 'adult',
title: t('AgeType.Adult'),
width: '12rem',
render: (_, { adult_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = isNotEmpty(_changed.adult_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.adult_cost || adult_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
{
key: 'child',
title: t('AgeType.Child'),
width: '12rem',
render: (_, { child_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = isNotEmpty(_changed.child_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.child_cost || child_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
width: '6rem',
render: (_, { audit_state_id, group_size_min, group_size_max, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? (
<div className='text-muted line-through '>{`${_changed.group_size_min ?? group_size_min} - ${_changed.group_size_max ?? group_size_max}`}</div>
) : null;
const editCls = colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{formatGroupSize(group_size_min, group_size_max)}</span>
</div>
);
},
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
width: '12rem',
render: (_, { use_dates_start, use_dates_end, weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? (
<div className='text-muted'>
{isNotEmpty(_changed.use_dates_start) ? <span className=' line-through '>{_changed.use_dates_start}</span> : use_dates_start} ~{' '}
{isNotEmpty(_changed.use_dates_end) ? <span className='t line-through '>{_changed.use_dates_end}</span> : use_dates_end}
</div>
) : null;
const editCls = colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${use_dates_start} ~ ${use_dates_end}`}</span>
</div>
);
},
},
{
key: 'weekdays',
dataIndex: ['weekdays'],
title: t('Weekdays'),
width: '6rem',
render: (text, { weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = !isEmpty(_changed.weekdays);
const _weekdays = ifData
? _changed.weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`common:weekdaysShort.${w}`))
.join(', ')
: '';
const preValue = ifCompare && ifData ? <div className='text-muted line-through '>{_weekdays}</div> : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
const weekdaysTxt = weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`common:weekdaysShort.${w}`))
.join(', ');
return (
<div>
{preValue}
<span className={editCls}>{weekdaysTxt || t('Unlimited')}</span>
</div>
);
},
},
];
const useLogMethod = (method) => {
const { t } = useTranslation('products');
const methodMap = {
'history': {
title: '📑' + t('versionHistory'),
btnText: t('versionHistory'),
fetchData: async (params) => {
const data = await getPPLogAction(params);
return { data };
},
},
'published': {
title: '✅' + t('versionPublished'),
btnText: t('versionPublished'),
fetchData: async (params) => {
const { travel_agency_id, product_id, price_id, use_year } = params;
const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id, use_year });
return { data: data?.[0]?.quotation || [] };
},
},
'snapshot': {
title: '📷' + t('versionSnapshot'),
btnText: t('versionSnapshot'),
fetchData: async (params) => {
const { price_id, ..._params } = params;
const data = await getPPSnapshotAction(_params);
return data; //?.[0]?.quotation || [];
},
},
};
return methodMap[method];
};
/**
* ProductQuotationLogPopover - A popover component that displays product quotation change logs or published data
*
* This component shows a history of price changes for a specific product quotation in a popover table.
* It supports displaying different data sources (history logs or published data) and shows
* comparison between previous and current values with visual indicators.
*
* @param {Object} props - Component props
* @param {string} props.btnText - The text to display on the trigger button and in the popover header
* @param {'history' | 'published' | 'snapshot'} props.method - Determines data source - "history" for change logs or "published" for published quotations
* @param {Object} props.triggerProps - Additional props to pass to the trigger button
* @param {number} props.travel_agency_id - ID of the travel agency (used in data fetching)
* @param {number} props.product_id - ID of the product (used in data fetching)
* @param {number} props.price_id - ID of the price entry (used in data fetching)
* @param {number} props.use_year - Year to use for fetching data (used in data fetching)
* @param {Function} props.onOpenChange - Callback function to be called when the popover opens or closes
*/
const ProductQuotationLogPopover = ({ method, triggerProps = {}, onOpenChange, ...props }) => {
const { travel_agency_id, product_id, price_id, use_year } = props;
const { travelAgencyId } = usingStorage();
const { t } = useTranslation('products');
const [open, setOpen] = useState(false);
const [logData, setLogData] = useState([]);
const { title, btnText: methodBtnText, fetchData } = useLogMethod(method);
const tablePagination = useMemo(() => method === 'history' ? { pageSize: 5, position: ['bottomLeft']} : { pageSize: 10, position: ['bottomLeft']}, [method]);
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const { data } = await fetchData({ travel_agency_id: travel_agency_id || travelAgencyId, product_id, price_id, use_year });
setLogData(data);
invokeOpenChange(true);
setLoading(false);
};
const invokeOpenChange = (_open) => {
if (typeof onOpenChange === 'function') {
onOpenChange(_open);
}
};
const columns = [...columnsSets(t, false),
{ title: t('common:time'), dataIndex: 'updatetime', key: 'updatetime', width: '10rem', },
{ title: t('common:operator'), dataIndex: 'update_by', key: 'update_by' }
];
return (
<Popover
placement='bottom'
className=''
rootClassName='w-5/6'
{...props}
title={
<div className='flex justify-between mt-0 gap-4 items-center '>
<Typography.Text strong>{title}</Typography.Text>
<Button
size='small'
onClick={() => {
setOpen(false);
invokeOpenChange(false);
}}>
&times;
</Button>
</div>
}
content={
<>
<Table columns={columns} dataSource={logData} rowKey={'id'} size='small' loading={loading} pagination={tablePagination} />
</>
}
trigger={['click']}
open={open}
onOpenChange={(v) => {
setOpen(v);
invokeOpenChange(v);
}}>
<Button {...triggerProps} onClick={getData} title={title}>
{props.btnText || methodBtnText}
</Button>
</Popover>
);
};
export default ProductQuotationLogPopover;

@ -1,299 +0,0 @@
import { useState, useMemo } from 'react';
import { Button, Table, Popover, Typography, List, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import { HT_HOST } from '@/config';
import { fetchJSON } from '@/utils/request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@/utils/commons';
import { chunkBy } from '@/hooks/useProductsQuotationFormat';
/**
* 产品价格日志
*/
const getPPLogAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_log`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格: 已发布的
*/
const getPPRunningAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_running`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格快照
*/
const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
const parseJson = (str) => {
let result;
if (str === null || str === undefined || str === '') {
return {};
}
try {
result = JSON.parse(str);
return Array.isArray(result) ? result.reduce((acc, cur) => ({ ...acc, ...cur }), {}) : result;
} catch (e) {
return {};
}
};
export const columnsSets = (t, colorize = true) => [
{
key: 'adult',
title: t('AgeType.Adult'),
width: '12rem',
render: (_, { adult_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && ![-1, 1, 2].includes(audit_state_id);
const ifData = isNotEmpty(_changed.adult_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.adult_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
{
key: 'child',
title: t('AgeType.Child'),
width: '12rem',
render: (_, { child_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && ![-1, 1, 2].includes(audit_state_id);
const ifData = isNotEmpty(_changed.child_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.child_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
width: '6rem',
render: (_, { audit_state_id, group_size_min, group_size_max, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? (
<div className='text-muted line-through '>{`${_changed.group_size_min ?? group_size_min} - ${_changed.group_size_max ?? group_size_max}`}</div>
) : null;
const editCls = colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{formatGroupSize(group_size_min, group_size_max)}</span>
</div>
);
},
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
width: '12rem',
render: (_, { use_dates_start, use_dates_end, weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? (
<div className='text-muted'>
{isNotEmpty(_changed.use_dates_start) ? <span className=' line-through '>{_changed.use_dates_start}</span> : use_dates_start} ~{' '}
{isNotEmpty(_changed.use_dates_end) ? <span className='t line-through '>{_changed.use_dates_end}</span> : use_dates_end}
</div>
) : null;
const editCls = colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${use_dates_start} ~ ${use_dates_end}`}</span>
</div>
);
},
},
{
key: 'weekdays',
dataIndex: ['weekdays'],
title: t('Weekdays'),
width: '6rem',
render: (text, { weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && ![-1, 1, 2].includes(audit_state_id);
const ifData = !isEmpty((_changed.weekdayList || []).filter((s) => s));
const preValue = ifCompare && ifData ? <div className='text-muted line-through '>{_changed.weekdayList}</div> : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{text || t('Unlimited')}</span>
</div>
);
},
},
];
const useLogMethod = (method) => {
const { t } = useTranslation('products');
const methodMap = {
'history': {
title: '📑' + t('versionHistory'),
btnText: t('versionHistory'),
fetchData: async (params) => {
const data = await getPPLogAction(params);
return {data};
},
},
'published': {
title: '✅' + t('versionPublished'),
btnText: t('versionPublished'),
fetchData: async (params) => {
const { travel_agency_id, product_id, price_id, use_year } = params;
const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id, use_year });
return {data: data?.[0]?.quotation || []};
},
},
'snapshot': {
title: '📷' + t('versionSnapshot'),
btnText: t('versionSnapshot'),
subTitle: t('点击左侧价格版本查看具体价格'),
fetchData: async (params) => {
const { price_id, ..._params } = params;
const data = await getPPSnapshotAction(_params);
return {data}; //?.[0]?.quotation || [];
},
},
};
return methodMap[method];
};
/**
* ProductQuotationLogPopover - A popover component that displays product quotation change logs or published data
*
* This component shows a history of price changes for a specific product quotation in a popover table.
* It supports displaying different data sources (history logs or published data) and shows
* comparison between previous and current values with visual indicators.
*
* @param {Object} props - Component props
* @param {string} props.btnText - The text to display on the trigger button and in the popover header
* @param {'history' | 'published' | 'snapshot'} props.method - Determines data source - "history" for change logs or "published" for published quotations
* @param {Object} props.triggerProps - Additional props to pass to the trigger button
* @param {number} props.travel_agency_id - ID of the travel agency (used in data fetching)
* @param {number} props.product_id - ID of the product (used in data fetching)
* @param {number} props.price_id - ID of the price entry (used in data fetching)
* @param {number} props.use_year - Year to use for fetching data (used in data fetching)
* @param {Function} props.onOpenChange - Callback function to be called when the popover opens or closes
*/
const ProductQuotationSnapshotPopover = ({ method, triggerProps = {}, onOpenChange, ...props }) => {
const { travel_agency_id, product_id, price_id, use_year } = props;
const { t } = useTranslation('products');
const [open, setOpen] = useState(false);
const [logData, setLogData] = useState([]);
const { title, subTitle, btnText: methodBtnText, fetchData } = useLogMethod(method);
const tablePagination = useMemo(() => method === 'history' ? { pageSize: 5, position: ['bottomLeft']} : { pageSize: 10, position: ['bottomLeft']}, [method]);
const [viewSnapshotItem, setViewSnapshotItem] = useState([]);
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const { data } = await fetchData({ travel_agency_id, product_id, price_id, use_year });
setLogData(data);
invokeOpenChange(true);
setLoading(false);
};
const invokeOpenChange = (_open) => {
if (typeof onOpenChange === 'function') {
onOpenChange(_open);
}
};
const onClickSnapshotItem = (item) => {
console.log(item)
setViewSnapshotItem(item);
console.log('cc\n');
const chunk = chunkBy(2025, [{...item, quotation: item.quotation.map(q => ({...q, WPI_SN: product_id })), info: { id: product_id }}], ['quote_season', 'quote_size']);
console.log(chunk)
};
const columns = [...columnsSets(t, false), { title: '时间', dataIndex: 'updatetime', key: 'updatetime' }];
return (
<Popover
placement='bottom'
className=''
rootClassName='w-5/6'
{...props}
title={
<div className='flex mt-0 gap-4 items-center '>
<Typography.Text strong>{title}</Typography.Text>
{subTitle && <Typography.Text type='secondary'>{subTitle}</Typography.Text>}
<Button
size='small' className='ml-auto'
onClick={() => {
setOpen(false);
invokeOpenChange(false);
}}>
&times;
</Button>
</div>
}
content={
<>
<Flex direction='column' gap='small'>
<List
bordered
dataSource={logData}
loading={loading}
renderItem={(item) => (
<List.Item onClick={() => onClickSnapshotItem(item)} className={viewSnapshotItem.version === item.version ? 'active' : ''}>
{item.version}
</List.Item>
)}
pagination={{ pageSize: 5, size: 'small', showLessItems: true, simple: { readOnly: true } }}
className=' cursor-pointer basis-48 flex flex-col [&>*:first-child]:flex-1 [&_.ant-list-pagination]:m-1 [&_.ant-list-item]:py-1 [&_.ant-list-item.active]:bg-blue-100'
/>
<div className='flex-auto'>
<Table columns={columns} dataSource={viewSnapshotItem.quotation} rowKey={'id'} size='small' loading={loading} pagination={tablePagination} />
</div>
</Flex>
</>
}
trigger={['click']}
open={open}
onOpenChange={(v) => {
setOpen(v);
invokeOpenChange(v);
if (v === false) {
setLogData([]);
setViewSnapshotItem([]);
}
}}>
<Button {...triggerProps} onClick={getData} title={title}>
{props.btnText || methodBtnText}
</Button>
</Popover>
);
};
export default ProductQuotationSnapshotPopover;

@ -1,212 +0,0 @@
import { createContext, useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom';
import { Tree, Input, Divider } from 'antd';
import { CaretDownOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import useProductsStore from '@/stores/Products/Index';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import { groupBy, isEmpty, sortBy } from '@/utils/commons';
import NewProductModal from './NewProductModal';
import ContractRemarksModal from './ContractRemarksModal'
const flattenTreeFun = (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 ProductsTree = ({ onNodeSelect, ...props }) => {
const { t } = useTranslation();
const location = useLocation();
const isEditPage = location.pathname.includes('edit');
const [agencyProducts, editingProduct, setEditingProduct] = useProductsStore((state) => [state.agencyProducts, state.editingProduct, state.setEditingProduct]);
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const productsTypes = useProductsTypes();
const [treeData, setTreeData] = useState([]); // render data
const [rawTreeData, setRawTreeData] = useState([]);
const [flattenTreeData, setFlattenTreeData] = useState([]);
const [expandedKeys, setExpandedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
useEffect(() => {
setExpandedKeys(productsTypes.map((item) => item.key)); //
return () => {}
}, [productsTypes, activeAgency]);
useEffect(() => {
// ;
// const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
const hasDataTypes = Object.keys(agencyProducts);
// const cityData = groupBy(productsSortByHT, (row) => `${row.info.city_id}-${row.info.city_name}`);
const copyAgencyProducts = structuredClone(agencyProducts);
Object.keys(copyAgencyProducts).map((key) => {
const _cityProductsData = groupBy(copyAgencyProducts[key], (row) => `${row.info.city_name || '(空)'}`);
copyAgencyProducts[key] = _cityProductsData;
});
// console.log(copyAgencyProducts);
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => ({
...ele,
title: ele.label,
key: ele.value,
children: (agencyProducts[ele.value] || []).reduce((arr, product) => {
const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({ ...rlgc, [clgc.lgc]: clgc }), {});
// const combindCityList = product.info.city_list.indexOf(city => city.id === product.info.city_id) !== -1 ? product.info.city_list : [...product.info.city_list, { id: product.info.city_id, name: product.info.city_name }];
// const cityListName = product.info.city_list.reduce((acc, city) => {
// return acc.concat([city.name]);
// }, []).join(',');
const hasCityList = !isEmpty(product.info.city_list) && product.info.city_list.some(cc => cc.id !== product.info.city_id) ? `【含多城市】` : ``;
const combindCityList = [{ id: product.info.city_id, name: product.info.city_name }];
const flatCityP = combindCityList.map(city => ({
title: `${city.name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || '') + `${hasCityList}`,
// key: `${ele.value}-${product.info.id}`,
key: `${product.info.id}-${city.id}`,
_raw: product,
isLeaf: true,
}));
return arr.concat(flatCityP);
}, []),
// ``
// _children: Object.keys(copyAgencyProducts[ele.value] || []).map(city => {
// return {
// title: city,
// key: `${ele.value}-${city}`,
// children: copyAgencyProducts[ele.value][city],
// };
// }),
}));
setTreeData(_show);
setRawTreeData(_show);
setFlattenTreeData(flattenTreeFun(_show));
// setExpandedKeys(productsTypes.map((item) => item.key)); //
// setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
return () => {};
}, [productsTypes, agencyProducts]);
useEffect(() => {
if (isEmpty(editingProduct)) {
return () => {};
}
const allKeysWithCity = [...(editingProduct.info?.city_list || []), { id: editingProduct.info.city_id, name: editingProduct.info.city_name }].map(
(city) => `${editingProduct.info.id}-${city.id}`
);
setSelectedKeys(allKeysWithCity);
return () => {};
}, [editingProduct?.info?.id]);
const [searchValue, setSearchValue] = useState('');
const onSearch = ({ target: { value } }) => {
// const { value } = e.target;
if (isEmpty(value)) {
setTreeData(rawTreeData);
setSearchValue(value);
return;
}
const newExpandedKeys = flattenTreeData
.filter((item) => item.title.includes(value))
.map((item) => getParentKey(item.key, rawTreeData))
.filter((item, i, self) => item && self.indexOf(item) === i);
setExpandedKeys(newExpandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
const matchTree = rawTreeData.map(node1 => {
const _find = node1.children.filter(node2 => node2.title.includes(value));
return _find.length > 0 ? {...node1, children: _find} : null;
}).filter(node => node);
setTreeData(matchTree);
};
const handleNodeSelect = (selectedKeys, { node }) => {
if (node._raw) {
setEditingProduct(node._raw);
const allKeysWithCity = [...node._raw.info.city_list, { id: node._raw.info.city_id, name: node._raw.info.city_name }].map((city) => `${node._raw.info.id}-${city.id}`);
setSelectedKeys(allKeysWithCity);
} else {
// : /
// const isExpand = expandedKeys.includes(selectedKeys[0]);
// const _keys = isExpand ? expandedKeys.filter(k => k !== node.key) : [].concat(expandedKeys, selectedKeys);
// setExpandedKeys(_keys);
}
if (typeof onNodeSelect === 'function') {
onNodeSelect(selectedKeys, { node });
}
};
const onExpand = (keys) => {
setExpandedKeys(keys);
setAutoExpandParent(false);
};
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 = <span style={{ color: 'red' }}>{searchValue}</span>;
return index > -1 ? (
<span>
{beforeStr}
{highlighted}
{afterStr}
</span>
) : (
<span>{node.title}</span>
);
};
return (
<div className={`${props.className} relative`} style={props.style}>
<Input.Search placeholder='Search' onChange={onSearch} allowClear className='sticky top-1 z-20 mb-3' />
{/* 编辑 */}
{isEditPage && (
<NewProductModal />
)}
<Divider type='vertical' />
<ContractRemarksModal />
<Tree
blockNode
showLine defaultExpandAll expandAction={'doubleClick'}
selectedKeys={selectedKeys} multiple
switcherIcon={<CaretDownOutlined />}
onSelect={handleNodeSelect}
treeData={treeData}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={onExpand}
titleRender={titleRender}
/>
</div>
);
};
export default ProductsTree;

@ -1,19 +1,12 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { App, Space, Table, Button, Modal, Divider } from 'antd';
import { App, Space, Table, Button, Modal } 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 { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import useFormStore from '@/stores/Form';
import { objectMapper } from '@/utils/commons';
import CopyProductsFormModal from './Detail/CopyProducts';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
dayjs.extend(arraySupport);
function Index() {
const { notification, message } = App.useApp();
@ -23,10 +16,6 @@ function Index() {
const [searchValues, setSearchValues] = useProductsStore((state) => [state.searchValues, state.setSearchValues]);
const formValuesToSub = useFormStore(state => state.formValuesToSub);
const stateMapVal = useProductsAuditStatesMapVal();
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' });
@ -36,22 +25,21 @@ function Index() {
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('');
setCopyLoading(true);
const postbody = objectMapper(param, { agency: 'target_agency', });
const toID = postbody.target_agency;
const success = await copyAgencyDataAction({...postbody, source_agency: sourceAgency.travel_agency_id});
setCopyLoading(false);
success ? message.success('复制成功') : message.error('复制失败');
setCopyModalVisible(false);
navigate(`/products/${toID}/${param.target_use_year || 'all'}/all/edit`);
navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
};
const openCopyModal = (from, action) => {
const openCopyModal = (from) => {
setSourceAgency(from);
setCopyAction(action);
setCopyModalVisible(true);
};
@ -63,18 +51,10 @@ function Index() {
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',
render: (_, r) => {
const stateCls = ` text-${stateMapVal[`${r.audit_state_id}`]?.color} `;
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
},
},
{ title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by' },
{ title: t('products:CreatedBy'), key: 'created_by', dataIndex: 'created_by_name' },
{ title: t('products:CreateDate'), key: 'create_date', dataIndex: 'create_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: '',
@ -83,15 +63,7 @@ function Index() {
<Space size={'large'}>
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`}>{t('Edit')}</Link>
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/audit`}>{t('Audit')}</Link>
{/* {r.audit_state_id >= 0 ? (
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/audit`}>{t('Audit')}</Link>
) : (
<span className='text-muted'>{t('Audit')}</span>
)} */}
<Button type='link' size={'small'} onClick={() => openCopyModal(r, '#')}>
{t('Copy') + '-' + t('products:#')}
</Button>
{/* <Button type='link' size={'small'} onClick={() => openCopyModal(r, 'o')}>{t('Copy')+'-'+t('products:Offer')}</Button> */}
<Button type='link' onClick={() => openCopyModal(r)}>{t('Copy')}</Button>
</Space>
),
},
@ -100,19 +72,19 @@ function Index() {
<Space direction='vertical' style={{ width: '100%' }}>
<SearchForm
fieldsConfig={{
shows: ['agency', 'audit_state', 'dates', 'year'],
shows: ['agency', 'audit_state', 'dates', 'year', ],
fieldProps: {
agency: { col: 4 },
dates: { label: t('products:CreateDate') },
keyword: { label: t('products:Title'), col: 4 },
year: { col: 4, rules: [{ required: true }] },
year: { col: 4 },
},
sort: { agency: 1, audit_state: 2, keyword: 100 },
}}
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
audit_state: { value: '', label: t('products:State') },
year: dayjs(), // .add(1, 'year'),
year: dayjs().add(1, 'year'),
}}
onSubmit={(err, formVal, filedsVal) => {
handleSearchAgency(formVal);
@ -121,17 +93,28 @@ function Index() {
<Table bordered={true} columns={columns} dataSource={agencyList} pagination={{ defaultPageSize: 20, showTotal: showTotal }} loading={loading} rowKey={'travel_agency_id'} />
{/* 复制弹窗 */}
<CopyProductsFormModal
initialValues={{agency: {label: sourceAgency.travel_agency_name, value: sourceAgency.travel_agency_id}}}
open={copyModalVisible}
action={copyAction}
source={{ sourceAgency, sourceYear: useYear }}
onCancel={() => setCopyModalVisible(false)}
onSubmit={(formVal) => {
handleCopyAgency(formVal);
}}
{...{copyModalVisible, setCopyModalVisible}}
/>
<Modal width={600} open={copyModalVisible} title={`复制供应商产品`} footer={false} onCancel={() => setCopyModalVisible(false)} destroyOnClose>
<div className='py-2'>复制源: {sourceAgency.travel_agency_name}</div>
<SearchForm formName='copyform' loading={copyLoading}
confirmText={t('Confirm')}
fieldsConfig={{
shows: ['agency', 'products_types', 'dept'],
fieldProps: {
agency: { label: `目标${t('products:Vendor')}`, col: 24, rules: [{ required: true, message: t('products:CopyFormMsg.requiredVendor') }] },
products_types: { col: 24 },
dept: { col: 24, rules: [{ required: true, message: t('products:CopyFormMsg.requiredDept') }] },
},
fieldComProps: {
agency: { mode: null }, //
products_types: { mode: 'multiple' },
dept: { isLeaf: true },
},
}}
onSubmit={(err, formVal, filedsVal) => {
handleCopyAgency(formVal);
}}
/>
</Modal>
</Space>
);
}

@ -1,40 +0,0 @@
import { useNavigate } from "react-router-dom";
import { Row, DatePicker, Flex, Col, Typography } from "antd";
import dayjs from "dayjs";
import { usingStorage } from "@/hooks/usingStorage";
function PickYear() {
const navigate = useNavigate();
const { travelAgencyId } = usingStorage();
return (
<>
<Row justify="center">
<Col span={4}>
<Flex gap="middle" vertical>
<Typography.Title className="text-center" level={3}>
请选择产品年份
</Typography.Title>
<DatePicker
className="w-full"
size="large"
variant="underlined"
needConfirm
inputReadOnly={true}
minDate={dayjs('2022-01-01')}
maxDate={dayjs().add(2, "year")}
allowClear={false}
picker="year"
open={true}
onOk={(date) => {
const useYear = date.year();
navigate(`/products/${travelAgencyId}/${useYear}/all/edit`);
}}
/>
</Flex>
</Col>
</Row>
</>
);
}
export default PickYear;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -1,65 +0,0 @@
import { Button } from 'antd';
import { useProductsAuditStatesMapVal, useProductsTypesMapVal } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
import useProductsStore, { getAgencyAllExtrasAction } from '@/stores/Products/Index';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_OFFER_AUDIT } from '@/config';
import dayjs from 'dayjs';
import AgencyContract from '../Print/AgencyContract';
import { saveAs } from 'file-saver';
import { Packer } from 'docx';
import { isEmpty } from '@/utils/commons';
const ExportDocxBtn = ({ params = { travel_agency_id: '', use_year: '', audit_state: '' }, ...props }) => {
const { t } = useTranslation();
const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const { travel_agency_id, use_year, audit_state } = params;
const auditStatesMap = useProductsAuditStatesMapVal();
const productsTypesMapVal = useProductsTypesMapVal();
const { getRemarkList } = useProductsStore((selector) => ({
getRemarkList: selector.getRemarkList,
}));
const handleDownload = async () => {
// await refresh();
const _agencyExtras = await getAgencyAllExtrasAction(params);
const agencyExtras = Object.keys(_agencyExtras).reduce((acc, pid) => {
const pitemExtras = _agencyExtras[pid];
const _pitem = (pitemExtras || []).map(eitem => ({ ...eitem, info: { ...eitem.info, product_type_name_txt: productsTypesMapVal[eitem.info.product_type_id]?.label || eitem.info.product_type_name } } ));
return { ...acc, [pid]: _pitem };
}, {});
const remarks = await getRemarkList();
const remarksMappedByType = remarks.reduce((r, v) => ({ ...r, [v.product_type_id]: v }), {});
const documentCreator = new AgencyContract();
const doc = documentCreator.create([
params,
activeAgency,
agencyProducts,
agencyExtras,
// remarks,
remarksMappedByType,
]);
const _d = dayjs().format('YYYYMMDD_HH.mm.ss.SSS'); // Date.now().toString(32)
// console.log(params);
const _state = isEmpty(audit_state) ? '' : auditStatesMap[audit_state].label;
Packer.toBlob(doc).then((blob) => {
saveAs(blob, `${activeAgency.travel_agency_name}${use_year}年地接合同-${_state}-${_d}.docx`);
});
};
return (
<>
{/* todo: export, 审核完成之后才能导出 */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' onClick={handleDownload}>
{t('Export')} .docx
</Button>
{/* <PrintContractPDF /> */}
</RequireAuth>
</>
);
};
export default ExportDocxBtn;

@ -1,19 +1,19 @@
import { useParams, useNavigate } 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, Flex } from 'antd'
import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App } from 'antd'
import {
FileOutlined, ArrowLeftOutlined
} from '@ant-design/icons'
FileOutlined
} from '@ant-design/icons';
import { usingStorage } from '@/hooks/usingStorage'
import useReservationStore from '@/stores/Reservation'
import { useTranslation } from 'react-i18next'
import {ImageUploader} from '@/components/ImageUploader'
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 = [
{
@ -42,38 +42,38 @@ function Detail() {
},
];
function detailTextRender(_, confirm) {
const formattedText = confirm.PCI_ConfirmText;
function detailTextRender(text, confirm) {
const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, "\n\n");
return (
<div className='whitespace-pre-line'>
<div style={{whiteSpace: 'pre-line'}}>
{formattedText}
</div>
);
}
function attachmentRender(_, confirm) {
const attachmentKey = `GHH/${travelAgencyId}/${reservationId}/PCISN${confirm.key}`;
function attachmentRender(text, confirm) {
return (
<>
<ImageUploader osskey={attachmentKey} ignore_case={false} deletable={false} />
{confirm.attachmentList.map(attch => {
return (
<Tag bordered={false} icon={<FileOutlined />}><a href={attch.file_url} target="_blank">{attch.file_name}</a></Tag>
)}
)}
</>
);
}
function confirmRender(_, confirm) {
function confirmRender(text, confirm) {
return (
<Button type='link' onClick={() => showConfirmModal(confirm)}>{t('Confirm')}</Button>
<Button type="link" onClick={() => showConfirmModal(confirm)}>{t('Confirm')}</Button>
);
}
const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [confirmText, setConfirmText] = useState('');
const [newConfirmText, setNewConfirmText] = useState('');
const [dataLoading, setDataLoading] = useState(false);
const [reservationPreviewUrl, setReservationPreviewUrl] = useState('');
const [nameCardPreviewUrl, setNameCardPreviewUrl] = useState('');
const { notification } = App.useApp();
const { reservationId } = useParams();
@ -82,18 +82,21 @@ function Detail() {
const [getReservationDetail, reservationDetail, confirmationList, selectConfirmation, submitConfirmation] =
useReservationStore((state) =>
[state.getReservationDetail, state.reservationDetail, state.confirmationList, state.selectConfirmation, state.submitConfirmation])
const randomString = new Date().getTime()
const officeWebViewerUrl =
'https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src=';
// https://www.chinahighlights.com/public/reservationW220420009.doc
const reservationUrl =
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=1&v=${randomString}`
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=1`;
const nameCardUrl =
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=2&v=${randomString}`
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=2`;
const reservationPreviewUrl = officeWebViewerUrl + encodeURIComponent(reservationUrl);
const nameCardPreviewUrl = officeWebViewerUrl + encodeURIComponent(nameCardUrl);
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);
};
@ -113,10 +116,6 @@ function Detail() {
useEffect(() => {
setDataLoading(true);
setReservationPreviewUrl(officeWebViewerUrl + encodeURIComponent(reservationUrl))
setNameCardPreviewUrl(officeWebViewerUrl + encodeURIComponent(nameCardUrl))
getReservationDetail(reservationId)
.catch(ex => {
notification.error({
@ -141,7 +140,7 @@ function Detail() {
<Title level={4}>{t('group:ConfirmationDetails')}</Title>
<Paragraph>
<blockquote>
<div className='whitespace-pre-line'>
<div style={{whiteSpace: 'pre-line'}}>
{confirmText}
</div>
</blockquote>
@ -156,22 +155,27 @@ function Detail() {
}}
/>
</Modal>
<Space direction='vertical' className='w-full'>
<Flex horizontal="true" align="flex-start" gap="middle"><Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)} /><Title level={4}> {t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate};</Title></Flex>
<Space direction="vertical" style={{ width: '100%' }}>
<Row gutter={{ md: 24 }}>
<Col span={20}>
<Title level={4}>{t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate};</Title>
</Col>
<Col span={4}>
<BackBtn to={'/reservation/newest?back'} />
</Col>
</Row>
<Row gutter={{ md: 24 }}>
<Col span={12} className='w-full'>
<iframe id='msdoc-iframe-reservation' title='msdoc-iframe-reservation'
src={reservationPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Col span={12} style={{height: '100%'}} >
<iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl} frameBorder="0" style={{ width: '100%', height: '600px' }}></iframe>
<Button type='link' target='_blank' href={reservationUrl}>{t('Download')} Itinerary</Button>
</Col>
<Col span={12} className='w-full'>
<iframe id='msdoc-iframe-name-card' title='msdoc-iframe-name-card'
src={nameCardPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Col span={12} style={{height: '100%'}} >
<iframe id="msdoc-iframe-name-card" title="msdoc-iframe-name-card" src={nameCardPreviewUrl} frameBorder="0" style={{ width: '100%', height: '600px' }}></iframe>
<Button type='link' target='_blank' href={nameCardUrl}>{t('Download')} Name Card</Button>
</Col>
</Row>
<Row>
<Col span={24}><Space direction='vertical' className='w-full'>
<Col span={24}><Space direction="vertical" style={{ width: '100%' }}>
<Table
bordered
loading={dataLoading}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save