Compare commits

..

No commits in common. 'main' and 'dev/i18n' have entirely different histories.

@ -1,15 +0,0 @@
module.exports = {
root: true,
env: { browser: true, es2020: true },
extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:react/jsx-runtime', 'plugin:react-hooks/recommended'],
ignorePatterns: ['dist', '.eslintrc.cjs'],
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
settings: { react: { version: '18.2' } },
plugins: ['react-refresh'],
rules: {
'react-refresh/only-export-components': ['warn', { allowConstantExport: true }],
'no-unused-vars': ['warn', { args: 'after-used', vars: 'all' }],
'react/prop-types': 'off',
'react-hooks/rules-of-hooks': 'warn',
},
};

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
@ -39,47 +24,7 @@ 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 开发管理 - 机票和价格管理
---
gitGraph TB:
commit
commit
branch release
branch feature/i18n
checkout main
commit id: "release" tag: "1.0.5.31"
branch bug/x1
commit id: "fix(...): ..."
checkout feature/i18n
commit
commit
checkout main
merge feature/i18n id: "机票开始"
branch feature/price_manager
checkout main
commit
commit
commit id: "账户体系"
checkout feature/price_manager
commit
commit
commit
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
```
反馈表测试链接
http://202.103.68.111:5173/feedback/330948

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

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

@ -1,92 +0,0 @@
CREATE TABLE auth_role
(
[role_id] [int] IDENTITY(1,1) NOT NULL,
[role_name] [nvarchar](255) NOT NULL,
[created_on] [datetime] NOT NULL,
CONSTRAINT [PK_auth_role] PRIMARY KEY CLUSTERED
(
[role_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
ALTER TABLE auth_role ADD CONSTRAINT [DF_auth_role_created_on] DEFAULT (getdate()) FOR [created_on]
CREATE TABLE auth_permission
(
[role_id] [int] NOT NULL,
[res_id] [int] NOT NULL
) ON [PRIMARY]
CREATE TABLE auth_resource
(
[res_id] [int] IDENTITY(1,1) NOT NULL,
[res_name] [nvarchar](255) NOT NULL,
[res_pattern] [nvarchar](255) NOT NULL,
[res_category] [nvarchar](255) NOT NULL,
CONSTRAINT [PK_auth_resource] PRIMARY KEY CLUSTERED
(
[res_id] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
INSERT INTO [dbo].[auth_role] ([role_name])
VALUES ('系统管理员')
INSERT INTO [dbo].[auth_role] ([role_name])
VALUES ('国内供应商')
INSERT INTO [dbo].[auth_role] ([role_name])
VALUES ('海外供应商')
INSERT INTO [dbo].[auth_role] ([role_name])
VALUES ('客服组')
INSERT INTO [dbo].[auth_role] ([role_name])
VALUES ('产品组')
INSERT INTO [dbo].[auth_role] ([role_name])
VALUES ('技术研发部')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有权限', '*', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('管理账号', '/account/management', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('新增账号', '/account/new', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('禁用账号', '/account/disable', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('重置密码', '/account/reset-password', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('管理角色', '/account/role-new', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有海外功能', '/oversea/all', 'oversea')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有国内功能', '/domestic/all', 'domestic')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有机票功能', '/air-ticket/all', 'air-ticket')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有火车票功能', '/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')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('审核价格', '/products/offer/audit', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('录入价格', '/products/offer/put', 'products')
-- 默认页面
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('最新计划', 'route=/reservation/newest', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('机票订票', 'route=/airticket', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(客服)', 'route=/products', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(供应商)', 'route=/products/edit', 'page')
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1)

Binary file not shown.

@ -14,6 +14,13 @@
100%{-webkit-transform:translate(150px)}
}
</style>
<script async src="https://www.googletagmanager.com/gtag/js?id=G-7JN1HT1DY4"></script>
<script>
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config', 'G-7JN1HT1DY4');
</script>
</head>
<body>
<div id="root">

@ -1,49 +1,32 @@
{
"name": "global-highlights-hub",
"name": "global.highlights.hub",
"private": true,
"version": "2.0.20",
"version": "0.1.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.17.2",
"dayjs": "^1.11.13",
"docx": "^8.5.0",
"file-saver": "^2.0.5",
"antd": "^5.4.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",
"mobx": "^6.9.0",
"mobx-react": "^7.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-i18next": "^14.1.2",
"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.2"
"react-to-pdf": "^1.0.1"
},
"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",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "^4.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-windicss": "^1.9.3",
"windicss": "^3.5.6"
"vite": "^4.2.0"
}
}

@ -1,6 +0,0 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}

@ -1,42 +0,0 @@
{
"CurrentPassword": "Current password",
"NewPassword": "New password",
"ReenterPassword": "Reenter password",
"Validation": {
"Success": "Your password has been successfully updated.",
"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"
}

@ -10,9 +10,7 @@
"Confirm": "Confirm",
"Close": "Close",
"Save": "Save",
"New": "New",
"Edit": "Edit",
"Audit": "Audit",
"Delete": "Delete",
"Add": "Add",
"View": "View",
@ -20,89 +18,37 @@
"Download": "Download",
"Upload": "Upload",
"preview": "Preview",
"Total": "Total",
"Action": "Action",
"Import": "Import",
"Export": "Export",
"Copy": "Copy",
"sureCancel": "Are you sure to cancel?",
"sureDelete": "Are you sure to delete?",
"sureSubmit": "Are you sure to submit?",
"Yes": "Yes",
"No": "No",
"Success": "Success",
"Failed": "Failed",
"All": "All",
"Table": {
"Total": "Total {{total}} items"
},
"Login": "Login",
"Username": "Username",
"Realname": "Realname",
"Password": "Password",
"ChangePassword": "Change password",
"Profile": "Profile",
"Logout": "Logout",
"ChangeVendor": "Change Vendor",
"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": {
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Sunday"
},
"weekdaysShort": {
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat",
"7": "Sun"
},
"menu": {
"Reservation": "Reservation",
"Invoice": "Invoice",
"Feedback": "Feedback",
"Notice": "Notice",
"Report": "Report",
"Airticket": "AirTicket",
"Trainticket": "TrainTicket",
"Products": "Products"
},
"Validation": {
"Title": "Notification",
"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"
"Notice": "Notice"
}
}

@ -1,7 +1,6 @@
{
"ArrivalDate": "Arrival Date",
"RefNo": "Reference number",
"unconfirmed": "Unconfirmed",
"Pax": "Pax",
"Status": "Status",
"City": "City",
@ -11,7 +10,6 @@
"Attachments": "Attachments",
"ConfirmationDate": "Confirmation Date",
"ConfirmationDetails": "Confirmation Details",
"PNR": "PASSAGER NAME RECORD",
"#": "#"
}

@ -1,147 +0,0 @@
{
"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": "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",
"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"
},
"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"
}

@ -1,5 +0,0 @@
{
"report": {
"GetReport": "Get Report"
}
}

@ -1,42 +0,0 @@
{
"CurrentPassword": "当前密码",
"NewPassword": "新密码",
"ReenterPassword": "重复输入密码",
"Validation": {
"Success": "密码更新成功",
"Fail": "密码更新失败",
"CurrentPassword": "请输入密码。",
"NewPassword": "请输入新密码。",
"ReenterPassword": "请重复输入密码。",
"username": "请重复输入用户名。",
"realname": "请重复输入真实姓名。",
"email": "请重复输入邮箱。",
"travelAgency": "请重复输供应商。",
"role": "请重复输入角色。",
"roleName": "请重复输入角色名称。"
},
"createdOn": "创建时间",
"action": "操作",
"action.edit": "编辑",
"action.enable": "启用",
"action.disable": "禁用",
"action.enable.title": "确定启用该账号吗?",
"action.disable.title": "确定禁用该账号吗?",
"action.resetPassword": "重置密码",
"action.resetPassword.tile": "确定重置账号密码吗?",
"accountList": "管理账号",
"newAccount": "新增账号",
"detail": "详细信息",
"username": "用户名",
"realname": "姓名",
"travelAgency": "供应商",
"travelAgencyName": "供应商名称",
"email": "邮箱地址",
"lastLogin": "最后登陆时间",
"roleList": "管理角色",
"newRole": "新增角色",
"roleName": "角色名称",
"permission": "权限"
}

@ -10,9 +10,7 @@
"Confirm": "确认",
"Close": "关闭",
"Save": "保存",
"New": "新增",
"Edit": "编辑",
"Audit": "审核",
"Delete": "删除",
"Add": "添加",
"View": "查看",
@ -20,89 +18,37 @@
"Download": "下载",
"Upload": "上传",
"preview": "预览",
"Total": "总数",
"Action": "操作",
"Import": "导入",
"Export": "导出",
"Copy": "复制",
"sureCancel": "确定取消?",
"sureDelete": "确定删除?",
"sureSubmit": "确定提交?",
"Yes": "是",
"No": "否",
"Success": "成功",
"Failed": "失败",
"All": "所有",
"Table": {
"Total": "共 {{total}} 条"
},
"Login": "登录",
"Username": "账号",
"Realname": "姓名",
"Username": "账户名",
"Password": "密码",
"ChangePassword": "修改密码",
"Profile": "账户中心",
"Logout": "退出",
"ChangeVendor": "切换账户",
"LoginTimeout": "登录超时",
"LoginTimeoutTip": "请输入密码",
"userProfile": "账号信息",
"Telephone": "联系电话",
"Email": "电子邮箱",
"Address": "公司地址",
"Company": "公司名称",
"Department": "部门",
"datetime": {
"thisWeek": "本周",
"lastWeek": "上周",
"thisMonth": "本月",
"lastMonth": "上月",
"nextMonth": "下月",
"lastThreeMonth": "前三个月",
"nextThreeMonth": "后三个月",
"firstHalfYear": "上半年",
"latterHalfYear": "下半年",
"thisYear": "今年"
},
"weekdays": {
"1": "周一",
"2": "周二",
"3": "周三",
"4": "周四",
"5": "周五",
"6": "周六",
"7": "周日"
},
"weekdaysShort": {
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "日"
},
"menu": {
"Reservation": "团预订",
"Invoice": "账单",
"Feedback": "反馈表",
"Notice": "通知",
"Report": "质量评分",
"Airticket": "机票订票",
"Trainticket": "火车订票",
"Products": "产品管理"
},
"Validation": {
"Title": "温馨提示",
"LoginFailed": "密码错误,登陆失败。",
"UsernameIsEmpty": "请输入账号",
"PasswordIsEmpty": "请输入密码"
},
"invoiceStatus": {
"Status": "审核状态",
"Not_submitted": "待提交",
"Submitted": "待审核",
"Travel_advisor_approved": "顾问已审核",
"Finance_Dept_arrproved": "财务已审核",
"Paid": "已打款"
"Notice": "通知"
}
}

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

@ -1,127 +0,0 @@
{
"ProductType": "项目类型",
"ContractRemarks": "合同备注",
"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": "报价信显示",
"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": "请选择所属小组"
},
"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": "选择语种"
},
"#": "产品"
}

@ -1,5 +0,0 @@
{
"report": {
"GetReport": "获取报告"
}
}

@ -1,6 +1,19 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
.ant-table-wrapper.border-collapse table {
border-collapse: collapse;
.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%;
}

@ -1,12 +0,0 @@
import { Select } from 'antd';
import { useProductsAuditStates } from '@/hooks/useProductsSets';
const AuditStateSelector = ({ ...props }) => {
const states = useProductsAuditStates();
return (
<>
<Select labelInValue allowClear options={states} {...props}/>
</>
);
};
export default AuditStateSelector;

@ -1,16 +0,0 @@
import { createContext, useContext, useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { Button } from 'antd';
import { isNotEmpty } from '@/utils/commons';
const BackBtn = ({to, ...props}) => {
const { t } = useTranslation();
const navigate = useNavigate();
return (
<>
{isNotEmpty(to) ? <Link to={to} className='px-4'>{t('Back')}</Link> : <Button type='link' onClick={() => navigate(-1)}>{t('Back')}</Button>}
</>
);
};
export default BackBtn;

@ -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>
);
};

@ -1,60 +0,0 @@
import { Component } from 'react';
import { Select } from 'antd';
// import { groups, leafGroup } from '../../libs/ht';
/**
* 小组
*/
export const groups = [
{ value: '1,2,28,7,33', key: '1,2,28,7,33', label: 'GH事业部', code: 'GH', children: [1, 2, 28, 7, 33] },
{ value: '8,9,11,12,20,21', key: '8,9,11,12,20,21', label: '国际事业部', code: 'INT', children: [8, 9, 11, 12, 20, 21] },
{ value: '10,18,16,30', key: '10,18,16,30', label: '孵化学院', code: '', children: [10, 18, 16, 30] },
{ value: '1', key: '1', label: 'CH直销', code: '', children: [] },
{ value: '2', key: '2', label: 'CH大客户', code: '', children: [] },
{ value: '28', key: '28', label: 'AH亚洲项目组', code: 'AH', children: [] },
{ value: '33', key: '33', label: 'GH项目组', code: '', children: [] },
{ value: '7', key: '7', label: '市场推广', code: '', children: [] },
{ value: '8', key: '8', label: '德语', code: '', children: [] },
{ value: '9', key: '9', label: '日语', code: '', children: [] },
{ value: '11', key: '11', label: '法语', code: '', children: [] },
{ value: '12', key: '12', label: '西语', code: '', children: [] },
{ value: '20', key: '20', label: '俄语', code: '', children: [] },
{ value: '21', key: '21', label: '意语', code: '', children: [] },
{ value: '10', key: '10', label: '商旅', code: '', children: [] },
{ value: '18', key: '18', label: 'CT', code: 'CT', children: [] },
{ value: '16', key: '16', label: 'APP', code: 'APP', children: [] },
{ value: '30', key: '30', label: 'Trippest', code: 'TP', children: [] },
{ value: '31', key: '31', label: '花梨鹰', code: '', children: [] },
];
export const groupsMappedByCode = groups.reduce((a, c) => ({ ...a, [String(c.code || c.key)]: c }), {});
export const groupsMappedByKey = groups.reduce((a, c) => ({ ...a, [String(c.key)]: c }), {});
export const leafGroup = groups.slice(3);
export const overviewGroup = groups.slice(0, 3); // todo: APP Trippest
export const DeptSelector = ({show_all, isLeaf,...props}) => {
const _show_all = ['tags', 'multiple'].includes(props.mode) ? false : show_all;
const options = isLeaf===true ? leafGroup : groups;
return (
<div>
<Select
mode={props.mode}
placeholder="选择小组"
labelInValue
maxTagCount={1}
allowClear={props.mode != null}
{...props}
options={options}
/>
{/* {_show_all ? (
<Select.Option key="ALL" value="ALL">
所有小组
</Select.Option>
) : (
''
)} */}
</div>
);
};
export default DeptSelector;

@ -1,33 +0,0 @@
import React, { PureComponent } from 'react'
import { Result } from 'antd'
//
// https://zh-hans.react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary
// https://juejin.cn/post/7168720873006825503
// https://github.com/bvaughn/react-error-boundary/tree/master
class ErrorBoundary extends PureComponent {
constructor(props) {
super(props);
this.state = { hasError: false, info: '' }
}
componentDidCatch(error, info) {
console.error('Sorry, Something went wrong.')
console.error(error)
this.setState({ hasError: true, info: error.message })
}
render() {
if (this.state.hasError) {
return <Result
status='500'
title='Sorry, Something went wrong.'
subTitle={this.state.info}
/>
}
return this.props.children
}
}
export default ErrorBoundary

@ -1,13 +0,0 @@
import { useRouteError } from 'react-router-dom'
import { Result } from 'antd'
export default function ErrorPage() {
const errorResponse = useRouteError()
return (
<Result
status='404'
title='Sorry, an unexpected error has occurred.'
subTitle={errorResponse?.message || errorResponse.error?.message}
/>
)
}

@ -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;

@ -1,22 +0,0 @@
import { Result } from 'antd'
import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from '@/stores/Auth'
export default function RequireAuth({ children, ...props }) {
const [isPermitted, currentUser] = useAuthStore(state => [state.isPermitted, state.currentUser])
const { userId } = usingStorage()
if (isPermitted(props.subject)) {
// if (props.subject === '/account/management1') {
return children
} else if (props.result) {
return (
<Result
status='403'
title='403'
subTitle={`抱歉,你(${currentUser.username})没有权限使用该功能(${props.subject})。`}
/>
)
}
}

@ -1,399 +0,0 @@
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 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";
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 };
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;
};
useEffect(() => {
setFormValues(readValues);
const dest = formValuesMapper(readValues);
setFormValuesToSub(dest);
if (typeof onMounted === "function") {
onMounted(dest);
}
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 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 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}>
重置
</Button> */}
</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 {...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;
}
export default SearchForm;

@ -1,49 +0,0 @@
import React, { useMemo, useRef, useState } from 'react';
import { Select, Spin } from 'antd';
import { debounce, objectMapper } from '@/utils/commons';
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState([]);
const fetchRef = useRef(0);
const debounceFetcher = useMemo(() => {
const loadOptions = (value) => {
fetchRef.current += 1;
const fetchId = fetchRef.current;
setOptions([]);
setFetching(true);
fetchOptions(value).then((newOptions) => {
const mapperOptions = newOptions.map(ele => objectMapper(ele, props.map));
if (fetchId !== fetchRef.current) {
// for fetch callback order
return;
}
setOptions(mapperOptions);
setFetching(false);
});
};
return debounce(loadOptions, debounceTimeout);
}, [fetchOptions, debounceTimeout]);
return (
<Select
labelInValue
filterOption={false}
showSearch
allowClear
maxTagCount={1}
dropdownStyle={{width: '20rem'}}
{...props}
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size='small' /> : null}
optionFilterProp='label'
>
{options.map((d) => (
<Select.Option key={d.value} title={d.label}>
{d.label}
</Select.Option>
))}
</Select>
);
}
export default DebounceSelect;

@ -1,31 +0,0 @@
import { Outlet, useNavigate } from 'react-router-dom';
import { Layout, Flex, theme, Spin, Divider } from 'antd';
import BackBtn from './BackBtn';
const { Content, Header } = Layout;
const HeaderWrapper = ({ children, header, loading, backTo, ...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'>
<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} />}
</Flex>
</Header>
<Divider className='my-2' />
<Content className='overflow-auto' style={{ backgroundColor: colorBgContainer }}>
{children || <Outlet />}
</Content>
</Layout>
</Spin>
</>
);
};
export default HeaderWrapper;

@ -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;

@ -1,47 +1,40 @@
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 OVERSEA_HOST = 'https://ht20-p9axztuwd7x8a7.mycht.cn'
import React from "react";
import dayjs from "dayjs";
export const HT_HOST = process.env.NODE_ENV == "production" ? "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, '')
export const BUILD_VERSION = import.meta.env.PROD ? __BUILD_VERSION__ : import.meta.env.MODE;
// 权限常量定义
// 账号、权限管理
// category: system
export const PERM_ACCOUNT_MANAGEMENT = '/account/management'
export const PERM_ACCOUNT_NEW = '/account/new'
export const PERM_ACCOUNT_DISABLE = '/account/disable'
export const PERM_ACCOUNT_RESET_PASSWORD = '/account/reset-password'
export const PERM_ROLE_NEW = '/account/role-new'
// 海外供应商
// category: oversea
export const PERM_OVERSEA = '/oversea/all'
// 国内供应商
// category: domestic
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'; // 信息.审核
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'; // 价格.录入
export const DATE_PRESETS = [
{
label: "This Week",
value: [dayjs().startOf("w"), dayjs().endOf("w")],
},
{
label: "Last Week",
value: [dayjs().startOf("w").subtract(7, "days"), dayjs().endOf("w").subtract(7, "days")],
},
{
label: "This Month",
value: [dayjs().startOf("M"), dayjs().endOf("M")],
},
{
label: "Last Month",
value: [dayjs().subtract(1, "M").startOf("M"), dayjs().subtract(1, "M").endOf("M")],
},
{
label: "Last Three Month",
value: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
},
{
label: "This Year",
value: [dayjs().startOf("y"), dayjs().endOf("y")],
},
// 本月: [dayjs().startOf("month"), moment().endOf("month")],
// 上个月: [dayjs().subtract(1, "months").startOf("month"), moment(new Date()).subtract(1, "months").endOf("month")],
// 近7天: [dayjs().add(-7, "days"), dayjs()],
// 近30天: [dayjs().subtract(30, "days"), moment()],
// 近三个月: [dayjs().subtract(2, "month").startOf("month"), moment().endOf("month")],
// 今年: [dayjs().startOf("year"), moment().endOf("year")],
// 去年: [dayjs().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
];
export const arrManager = ["testzp","testzac","testycc","testlyj","testqqs","testjjh","testhn"];//特定账号加修改所属供应商的菜单 zp

@ -1,76 +0,0 @@
import { useEffect, useState } from 'react';
import dayjs from "dayjs";
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
export const useDatePresets = () => {
const [presets, setPresets] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const newPresets = [
{
label: t("datetime.thisWeek"),
value: [dayjs().startOf("w"), dayjs().endOf("w")],
},
{
label: t("datetime.lastWeek"),
value: [dayjs().startOf("w").subtract(7, "days"), dayjs().endOf("w").subtract(7, "days")],
},
{
label: t("datetime.thisMonth"),
value: [dayjs().startOf("M"), dayjs().endOf("M")],
},
{
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")],
},
];
setPresets(newPresets);
}, [i18n.language]);
return presets;
}
export const useWeekdays = () => {
const [data, setData] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const newData = [
{ value: '1', label: t('weekdays.1') },
{ value: '2', label: t('weekdays.2') },
{ value: '3', label: t('weekdays.3') },
{ value: '4', label: t('weekdays.4') },
{ value: '5', label: t('weekdays.5') },
{ value: '6', label: t('weekdays.6') },
{ value: '7', label: t('weekdays.7') },
];
setData(newData);
return () => {};
}, [i18n.language]);
return data;
};

@ -1,20 +0,0 @@
export const useHTLanguageSets = () => {
const newData = [
{ key: '1', value: '1', label: 'English' },
{ key: '2', value: '2', label: 'Chinese (中文)' },
{ key: '3', value: '3', label: 'Japanese (日本語)' },
{ key: '4', value: '4', label: 'German (Deutsch)' },
{ key: '5', value: '5', label: 'French (Français)' },
{ key: '6', value: '6', label: 'Spanish (Español)' },
{ key: '7', value: '7', label: 'Russian (Русский)' },
{ key: '8', value: '8', label: 'Italian (Italiano)' },
];
return newData;
};
export const useHTLanguageSetsMapVal = () => {
const stateSets = useHTLanguageSets();
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
return stateMapVal;
};

@ -0,0 +1,42 @@
import { useEffect, useState } from 'react';
import dayjs from "dayjs";
import { useTranslation } from 'react-i18next';
const usePresets = () => {
const [presets, setPresets] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const newPresets = [
{
label: t("datetime.thisWeek"),
value: [dayjs().startOf("w"), dayjs().endOf("w")],
},
{
label: t("datetime.lastWeek"),
value: [dayjs().startOf("w").subtract(7, "days"), dayjs().endOf("w").subtract(7, "days")],
},
{
label: t("datetime.thisMonth"),
value: [dayjs().startOf("M"), dayjs().endOf("M")],
},
{
label: t("datetime.lastMonth"),
value: [dayjs().subtract(1, "M").startOf("M"), dayjs().subtract(1, "M").endOf("M")],
},
{
label: t("datetime.lastThreeMonth"),
value: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
},
{
label: t("datetime.thisYear"),
value: [dayjs().startOf("y"), dayjs().endOf("y")],
},
];
setPresets(newPresets);
}, [i18n.language]);
return presets;
}
export default usePresets;

@ -1,184 +0,0 @@
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';
/**
* 产品管理 相关的预设数据
* 项目类型
* * 酒店预定 1
* * 火车 2
* * 飞机票务 3
* * 游船 4
* * 快巴 5
* * 旅行社(综费) 6
* * 景点 7
* * 特殊项目 8
* * 其他 9
* * 酒店 A
* * 超公里 B
* * 餐费 C
* * 小包价 D // 包价线路
* * X
* * 购物 S
* * R (餐厅)
* * 娱乐 E
* * 精华线路 T
* * 客人testimonial F
* * 线路订单 O
* * P
* * 信息 I
* * 国家 G
* * 城市 K
* * 图片 H
* * 地图 M
* * 包价线路 L (已废弃)
* * 节日节庆 V
* * 火车站 N
* * 手机租赁 Z
* * ---- webht 类型, 20240624 新增HT类型 ----
* * 导游 Q
* * 车费 J
*/
export const useProductsTypes = (showAll = false) => {
const [types, setTypes] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const allItem = [{ label: t('All'), value: '', key: '' }];
const newData = [
{ label: t('products:type.Experience'), value: '6', key: '6' },
{ label: t('products:type.UltraService'), value: 'B', key: 'B' },
{ label: t('products:type.Car'), value: 'J', key: 'J' },
{ label: t('products:type.Guide'), value: 'Q', key: 'Q' },
{ label: t('products:type.Attractions'), value: '7', key: '7' }, // landscape
{ label: t('products:type.Meals'), value: 'R', key: 'R' },
{ label: t('products:type.Extras'), value: '8', key: '8' },
{ label: t('products:type.Package'), value: 'D', key: 'D' },
];
const res = showAll ? [...allItem, ...newData] : newData;
setTypes(res);
}, [i18n.language]);
return types;
};
export const useProductsTypesMapVal = (value) => {
const stateSets = useProductsTypes();
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
return stateMapVal;
};
export const useProductsAuditStates = () => {
const [types, setTypes] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const newData = [
{ key: '-1', value: '-1', label: t('products:auditState.New'), color: 'muted' },
{ key: '0', value: '0', label: t('products:auditState.Pending'), color: '' },
{ key: '2', value: '2', label: t('products:auditState.Approved'), color: 'primary' },
{ key: '3', value: '3', label: t('products:auditState.Rejected'), color: 'danger' },
{ key: '1', value: '1', label: t('products:auditState.Published'), color: 'primary' },
// ELSE 未知
];
setTypes(newData);
}, [i18n.language]);
return types;
};
export const useProductsAuditStatesMapVal = (value) => {
const stateSets = useProductsAuditStates();
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
return stateMapVal;
};
/**
* @ignore
*/
export const useProductsTypesFieldsets = (type) => {
const [isPermitted] = useAuthStore((state) => [state.isPermitted]);
const infoDefault = [['city'], ['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 infoTypesMap = {
'6': [[...infoDisplay], []],
'B': [['km', ...infoDisplay], []],
'J': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
'D': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
'7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']],
'R': [[...infoDisplay], ['description']],
'8': [[...infoDisplay], []],
};
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]],
];
};
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,
},
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',
},
],
};
};

@ -1,86 +0,0 @@
const persistObject = {}
/**
* G-INT:USER_ID -> userId = 456
* G-STR:LOGIN_TOKEN -> loginToken = 'E6779386E7D64DF0ADD0F97767E00D8B'
* G-JSON:LOGIN_USER -> loginUser = { username: 'test-username' }
*/
export function usingStorage() {
const getStorage = () => {
if (import.meta.env.DEV && window.localStorage) {
return window.localStorage
} else if (window.sessionStorage) {
return window.sessionStorage
} else {
console.error('browser not support localStorage and sessionStorage.')
}
}
const setProperty = (key, value) => {
const webStorage = getStorage()
const typeAndKey = key.split(':')
if (typeAndKey.length === 2) {
const propName = camelCasedWords(typeAndKey[1])
persistObject[propName] = value
if (typeAndKey[0] === 'G-JSON') {
webStorage.setItem(key, JSON.stringify(value))
} else {
webStorage.setItem(key, value)
}
}
}
// USER_ID -> userId
const camelCasedWords = (string) => {
if (typeof string !== 'string' || string.length === 0) {
return string;
}
return string.split('_').map((word, index) => {
if (index === 0) {
return word.toLowerCase()
} else {
return word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()
}
}).join('')
}
if (Object.keys(persistObject).length == 0) {
const webStorage = getStorage()
for (let i = 0; i < webStorage.length; i++) {
const key = webStorage.key(i)
const typeAndKey = key.split(':')
if (typeAndKey.length === 2) {
const value = webStorage.getItem(key)
const propName = camelCasedWords(typeAndKey[1])
if (typeAndKey[0] === 'G-INT') {
persistObject[propName] = parseInt(value, 10)
} else if (typeAndKey[0] === 'G-JSON') {
try {
persistObject[propName] = JSON.parse(value)
} catch (e) {
// 如果解析失败,保留原始字符串值
persistObject[propName] = value
console.error('解析 JSON 失败。')
}
} else {
persistObject[propName] = value
}
}
}
}
return {
...persistObject,
setStorage: (key, value) => {
setProperty(key, value)
},
clearStorage: () => {
getStorage().clear()
Object.assign(persistObject, {})
}
}
}

@ -1,30 +1,13 @@
import { useState, useEffect } from 'react';
import { Dropdown } from 'antd';
import React, { useState } from 'react';
import { Dropdown, Menu } from 'antd';
import { useTranslation } from 'react-i18next';
import { appendRequestParams } from '@/utils/request';
const i18n_to_htcode = {
'zh': 2,
'en': 1,
};
export const useDefaultLgc = () => {
const { i18n } = useTranslation();
return { language: i18n_to_htcode[i18n.language], };
};
/**
* 语言选择组件
*/
const Language = () => {
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation();
const [selectedKeys, setSelectedKeys] = useState([i18n.language]);
useEffect(() => {
appendRequestParams('lgc', i18n_to_htcode[i18n.language]);
return () => {};
}, [i18n.language]);
//
const handleChangeLanguage = ({ key }) => {
setSelectedKeys([key]);
@ -36,7 +19,7 @@ const { t, i18n } = useTranslation();
/* 🌏🌐 */
return (
<Dropdown menu={{ items: langSupports, onClick: handleChangeLanguage, style: { width: 100 }, selectedKeys: selectedKeys }}>
<div className='icon text-primary'>🌐<span>{t(`lang.${i18n.language}`)}</span></div>
<div className='icon'>🌐<span style={{color: '#00b96b'}}>{t(`lang.${i18n.language}`)}</span></div>
</Dropdown>
);
};

@ -17,16 +17,12 @@ i18n
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'group', 'vendor', 'account', 'products'],
ns: ['common', 'group'],
defaultNS: 'common',
detection: {
// convertDetectedLanguage: 'Iso15897',
convertDetectedLanguage: (lng) => {
const langPart = lng.split('-')[0];
return langPart;
// return lng.replace('-', '_');
},
},
// detection: {
// convertDetectedLanguage: 'Iso15897',
// convertDetectedLanguage: (lng) => lng.replace('-', '_')
// },
supportedLngs: ['en', 'zh'],
// resources: {
// en: { translation: en },

@ -1,132 +1,86 @@
import { createRoot } from 'react-dom/client'
import React from "react";
import { configure } from "mobx";
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'
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 { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from './stores/Auth'
import { isNotEmpty } from '@/utils/commons'
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
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>},
]
},
{
element: <Standlone />,
children: [
{ path: '/login', element: <Login /> },
{ path: '/logout', element: <Logout /> },
]
}
])
}
const initAppliction = async () => {
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await useAuthStore.getState().initAuth()
} from "react-router-dom";
import RootStore from "@/stores/Root";
import { StoreContext } from '@/stores/StoreContext';
import "@/assets/global.css";
import App from "@/views/App";
import Standlone from "@/views/Standlone";
import Login from "@/views/Login";
import Index from "@/views/index";
import ErrorPage from "@/views/error-page";
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 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 ChangeVendor from "@/views/account/ChangeVendor";
import './i18n';
configure({
useProxies: "ifavailable",
enforceActions: "observed",
computedRequiresReaction: true,
observableRequiresReaction: false,
reactionRequiresObservable: true,
disableErrorBoundaries: process.env.NODE_ENV == "production"
});
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Index /> },
{ path: "reservation/newest", element: <ReservationNewest />},
{ path: "reservation/:reservationId", element: <ReservationDetail />},
{ path: "account/change-password", element: <ChangePassword />},
{ path: "account/profile", element: <AccountProfile />},
{ path: "feedback", element: <FeedbackIndex />},
{ path: "feedback/:GRI_SN/:RefNo", element: <FeedbackDetail />},
{ path: "feedback/:GRI_SN/:CII_SN/:RefNo", element: <FeedbackCustomerDetail />},
{ path: "report", element: <ReportIndex />},
{ path: "notice", element: <NoticeIndex />},
{ path: "notice/:CCP_BLID", element: <NoticeDetail />},
{ path: "invoice",element:<InvoiceIndex />},
{ path: "invoice/detail/:GMDSN/:GSN",element:<InvoiceDetail />},
{ path: "invoice/paid",element:<InvoicePaid />},
{ path: "invoice/paid/detail/:flid",element:<InvoicePaidDetail />},
{ path: "account/change-vendor",element:<ChangeVendor />},
]
},
{
element: <Standlone />,
children: [
{ path: "/login", element: <Login /> },
]
}
]);
const router = await initRouter()
const root = document.getElementById('root')
if (!root) throw new Error('No root element found')
const rootStore = new RootStore();
createRoot(root).render(
//<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
ReactDOM.createRoot(document.getElementById("root")).render(
//<React.StrictMode>
<StoreContext.Provider value={rootStore}>
<RouterProvider
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</ThemeContext.Provider>
//</React.StrictMode>
)
}
initAppliction()
</StoreContext.Provider>
//</React.StrictMode>
);

@ -1,51 +0,0 @@
import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } 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}`,
];
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);
});
};
export const uploadPageSpyLog = async () => {
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
await window.$harbor.upload() // { clearCache: true, remark: '' }
alert('Success')
} else {
alert('Failure')
}
}
export const PageSpyLog = () => {
return (
<>
{window.$pageSpy && (
<a
className='text-primary'
onClick={() => {
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
}}>
上传Debug日志 ({window.$pageSpy.address.substring(0, 4)})
</a>
)}
</>
);
};

@ -1,191 +0,0 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons'
import { HT_HOST } from "@/config"
import { usingStorage } from '@/hooks/usingStorage'
export const postAccountStatus = async (formData) => {
const { errcode, result } = await postForm(
`${HT_HOST}/service-CooperateSOA/set_account_status`, formData)
return errcode !== 0 ? {} : result
}
export const postAccountPassword = async (formData) => {
const { errcode, result } = await postForm(
`${HT_HOST}/service-CooperateSOA/reset_account_password`, formData)
return errcode !== 0 ? {} : result
}
export const fetchAccountList = async (params) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/search_account`, params)
return errcode !== 0 ? {} : result
}
export const postAccountForm = async (formData) => {
const { errcode, result } = await postForm(
`${HT_HOST}/service-CooperateSOA/new_or_update_account`, formData)
return errcode !== 0 ? {} : result
}
export const postRoleForm = async (formData) => {
const { errcode, result } = await postForm(
`${HT_HOST}/service-CooperateSOA/new_or_update_role`, formData)
return errcode !== 0 ? {} : result
}
export const fetchRoleList = async () => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_role_list`)
return errcode !== 0 ? {} : result
}
export const fetchPermissionList = async () => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_all_permission_list`)
return errcode !== 0 ? {} : result
}
export const fetchPermissionListByRoleId = async (params) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_role_permission_list`, params)
return errcode !== 0 ? {} : result
}
export const fetchTravelAgencyByName = async (name) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/Service_BaseInfoWeb/VendorList`, {q: name})
return errcode !== 0 ? {} : result
}
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) => ({
accountList: [],
toggleAccountStatus: async (userId, status) => {
const statusValue = status ? 'enable' : 'disable'
const formData = new FormData()
formData.append('lmi_sn', userId)
formData.append('account_status', statusValue)
return postAccountStatus(formData)
},
resetAccountPassword: async (userId, password) => {
const formData = new FormData()
formData.append('lmi_sn', userId)
formData.append('newPassword', password)
return postAccountPassword(formData)
},
newEmptyRole: () => ({
role_id: null,
role_name: '',
role_ids: ''
}),
newEmptyAccount: () => {
return {
accountId: null,
userId: null,
lmi2_sn: null,
username: '',
realname: '',
email: '',
travelAgencyId: null,
roleId: ''
}
},
saveOrUpdateRole: async (formValues) => {
const formData = new FormData()
formData.append('role_id', formValues.role_id)
formData.append('role_name', formValues.role_name)
formData.append('res_ids', formValues.res_array.join(','))
return postRoleForm(formData)
},
saveOrUpdateAccount: async (formValues) => {
const { userId } = usingStorage()
const formData = new FormData()
formData.append('wu_id', formValues.accountId)
formData.append('lmi_sn', formValues.userId)
formData.append('lmi2_sn', formValues.lmi2_sn)
formData.append('user_name', formValues.username)
formData.append('real_name', formValues.realname)
formData.append('email', formValues.email)
formData.append('travel_agency_id', formValues.travelAgencyId)
formData.append('roles', formValues.roleId)
formData.append('opi_sn', userId)
return postAccountForm(formData)
},
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,
lgc: 2
}
const resultArray = await fetchAccountList(searchParams)
const mapAccoutList = resultArray.map((r) => {
return {
accountId: r.wu_id,
userId: r.lmi_sn,
lmi2_sn: r.lmi2_sn,
username: r.user_name,
realname: r.real_name,
email: r.email,
lastLogin: r.wu_lastlogindate,
travelAgencyName: r.travel_agency_name,
travelAgencyId: r.travel_agency_id,
disabled: r.wu_limitsign,
// 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。
roleId: isEmpty(r.roles) ? 0 : parseInt(r.roles),
role: r.roles_name,
}
})
set(() => ({
accountList: mapAccoutList
}))
},
}), { name: 'accountStore' }))
export default useAccountStore

@ -1,226 +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 airTicketStore = 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 }),
setAirPortList: airPortList => set({ airPortList }),
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/GetFlightPlan`, 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/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,
};
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,180 +1,185 @@
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'
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 WILDCARD_TOKEN = '*'
export const fetchLoginToken = async (username, password) => {
const formData = new FormData()
formData.append('username', username)
formData.append('Password', password)
const { errcode, Result } = await postForm(
`${HT_HOST}/service-CooperateSOA/Login`,
formData)
return errcode !== 0 ? {} : Result
}
export const fetchUserDetail = async (loginToken) => {
const { errcode, Result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/GetLinkManInfo`, { token: loginToken})
return errcode !== 0 ? {} : Result
}
export const fetchPermissionListByUserId = async (userId) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_account_permission_list`, { lmi_sn: userId})
return errcode !== 0 ? {} : result
}
// 取消令牌时间过期检测,待删除
async function fetchLastRequet() {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`)
return errcode !== 0 ? {} : result
}
const initialState = {
tokenInterval: null,
loginStatus: 0,
defaltRoute: '',
currentUser: {
username: '',
realname: '',
rolesName: '',
emailAddress: '',
travelAgencyName: '',
},
permissionList: []
}
const useAuthStore = create(devtools((set, get) => ({
...initialState,
initAuth: async () => {
const { loadUserPermission } = get()
const { setStorage, loginToken } = usingStorage()
// Dev 模式使用 localStorage会有 token 失效情况,需要手动删除
// Prod 环境没有该问题
const userJson = await fetchUserDetail(loginToken)
import { makeAutoObservable, runInAction } from "mobx";
import { fetchJSON, postForm } from '@/utils/request';
import { HT_HOST } from "@/config";
import { isNotEmpty, prepareUrl } from '@/utils/commons';
const KEY_LOGIN_TOKEN = 'KEY_LOGIN_TOKEN';
const KEY_TRAVEL_AGENCY_ID = 'KEY_TRAVEL_AGENCY_ID';
const KEY_USER_ID = 'KEY_USER_ID';
class Auth {
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
this.login.token = root.getSession(KEY_LOGIN_TOKEN);
this.login.userId = root.getSession(KEY_USER_ID);
this.login.travelAgencyId = root.getSession(KEY_TRAVEL_AGENCY_ID);
if (isNotEmpty(this.login.token)) {
this.fetchUserDetail();
}
}
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userJson.LMI_SN)
valdateUserPassword(usr, pwd) {
const formData = new FormData();
formData.append('username', usr);
formData.append('Password', pwd);
const postUrl = HT_HOST + '/service-CooperateSOA/Login';
setStorage(KEY_USER_ID, userJson.LMI_SN)
setStorage(KEY_TRAVEL_AGENCY_ID, userJson.LMI_VEI_SN)
return postForm(postUrl, formData)
.then(json => {
if (json.errcode == 0) {
runInAction(() => {
this.login.token = json.Result.token;
this.login.timeout = false;
});
this.root.putSession(KEY_LOGIN_TOKEN, json.Result.token);
return json.Result.WU_LMI_SN;
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
}
await loadUserPermission(userJson.LMI_SN)
fetchUserDetail() {
const fetchUrl = prepareUrl(HT_HOST + '/service-CooperateSOA/GetLinkManInfo')
.append('token', this.login.token)
.build();
set(() => ({
currentUser: {
username: userJson.LoginName,
realname: userJson.real_name,
rolesName: userJson.roles_name,
emailAddress: userJson.LMI_listmail,
travelAgencyName: userJson.VName,
return fetchJSON(fetchUrl)
.then(json => {
if (json.errcode == 0) {
runInAction(() => {
this.login.userId = json.Result.LMI_SN;
this.login.username = json.Result.LoginName;
this.login.travelAgencyId = json.Result.LMI_VEI_SN;
this.login.travelAgencyName = json.Result.VName;
this.login.telephone = json.Result.LkPhone;
this.login.emailAddress = json.Result.LMI_listmail;
this.login.cityId = json.Result.citysn;
this.root.putSession(KEY_TRAVEL_AGENCY_ID, this.login.travelAgencyId);
this.root.putSession(KEY_USER_ID, this.login.userId);
});
this.startTokenInterval();
return this.login;
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
}
startTokenInterval() {
const authStore = this;
async function fetchLastRequet() {
const fetchUrl = prepareUrl(HT_HOST + '/service-CooperateSOA/GetLastReqDate')
.append('token', authStore.login.token)
.build();
const json = await fetchJSON(fetchUrl)
if (json.errcode == 0 && isNotEmpty(json.result)) {
return json.result.LastReqDate;
} else {
return 0;
}
}))
loadPageSpy(`${userJson.real_name}-${userJson.VName}`)
},
authenticate: async (usr, pwd) => {
const { initAuth } = get()
const { setStorage } = usingStorage()
}
const { token: loginToken } = await fetchLoginToken(usr, pwd)
async function checkTokenTimeout() {
const lastRequest = await fetchLastRequet();
const lastReqDate = new Date(lastRequest);
const now = new Date();
const diffTime = now.getTime() - lastReqDate.getTime();
const diffHours = diffTime/1000/60/60;
if (diffHours > 4) {
authStore.logout();
}
}
setStorage(KEY_LOGIN_TOKEN, loginToken)
this.tokenInterval = setInterval(() => checkTokenTimeout(), 1000*60*20);
}
await initAuth()
logout() {
this.root.clearSession();
runInAction(() => {
this.login.timeout = true;
});
}
set(() => ({
loginStatus: 302
}))
},
tokenInterval = null;
loadUserPermission: async(userId) => {
let deaultPage = '/'
const permissionResult = await fetchPermissionListByUserId(userId)
const pageList = permissionResult.filter(p => {
return p.res_category === 'page'
})
if (pageList.length > 0) {
const resPattern = pageList[0].res_pattern
const splitResult = resPattern.split('=')
if (splitResult.length > 1)
deaultPage = splitResult[1]
}
set(() => ({
defaultRoute: deaultPage,
permissionList: permissionResult.map(p => p.res_pattern)
}))
},
logout: () => {
const { tokenInterval, currentUser } = get()
const { clearStorage } = usingStorage()
clearStorage()
clearInterval(tokenInterval)
set(() => ({
...initialState,
currentUser: {
username: currentUser.username
}
}))
},
// 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'
changeUserPassword(password, newPassword) {
const formData = new FormData();
formData.append('UserID', this.login.userId);
formData.append('Password', password);
formData.append('NewPassword', newPassword);
formData.append('token', this.login.token);
const postUrl = HT_HOST + '/service-CooperateSOA/SetPassword';
return postForm(postUrl, formData)
.then(json => {
if (json.errcode == 0) {
return json
return json;
} else {
throw new Error(json.errmsg + ': ' + json.errcode)
throw new Error(json.errmsg + ': ' + json.errcode);
}
})
},
isPermitted: (perm) => {
const { permissionList } = get()
// 测试权限使用:
// if (perm === '/account/management') return false
// if (perm === '/account/role/new') return false
// return true
// 以上是 Hardcode 判断
// 以下是权限列表从数据库读取后使用的方法
return permissionList.some((value) => {
if (value.indexOf(WILDCARD_TOKEN) == 0) {
return true
}
if (value === perm) {
return true
});
}
//供应商列表
fetchVendorList(){
const authStore = this;
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetHWVendorList')
.append('token', authStore.login.token)
.build();
return fetchJSON(fetchUrl)
.then(json => {
//console.log(json);
if (json.errcode == 0) {
runInAction(() => {
this.VendorList = json.Result;
});
}
});
}
//切换供应商
changeVendor(VEISN){
// const formData = new FormData();
// formData.append('VEI_SN',VEISN);
// formData.append('LMI_SN',this.login.userId);
// formData.append('token', this.login.token);
const fetchUrl = prepareUrl(HT_HOST+'/service-cusservice/PTChangeVendor')
.append('LMI_SN',this.login.userId)
.append('VEI_SN',VEISN)
.append('token', this.login.token)
.build();
//console.log(fetchUrl);
// return false;
return fetchJSON(fetchUrl)
.then(()=>{
let json = {"errcode": 0, "errmsg": "ok", "Result":[]};
if (json.errcode==0){
//console.log(json);
return json;
}else{
throw new Error(json.errmsg+":"+json.errcode);
}
return false
})
},
});
}
VendorList = [];//海外供应商列表
}), { name: 'authStore' }))
login = {
token: '',
userId: 0, // LMI_SN
username: '0',
travelAgencyId: 0, // VEI_SN
travelAgencyName: '',
telephone: '',
emailAddress: '',
cityId: 0,
timeout: false
}
}
export default useAuthStore
export default Auth;

@ -1,122 +1,201 @@
import { fetchJSON, postForm } from '@/utils/request';
import { groupBy } from '@/utils/commons';
import * as config from '@/config';
import dayjs from 'dayjs';
import { makeAutoObservable, runInAction } from "mobx";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, groupBy } from "@/utils/commons";
import * as config from "@/config";
import dayjs from "dayjs";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
class Feedback {
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
}
const { HT_HOST } = config;
loading = false;
search_date_start = dayjs().subtract(2, "M").startOf("M");
search_date_end = dayjs().endOf("M");
feedbackList = []; //反馈列表
feedbackImages = []; //图片列表
feedbackRate = []; //反馈评分
feedbackReview = []; //站外好评
feedbackInfo = []; //地接社反馈的信息
feedbackServiceRate = {}; // 反馈评分, 含细项的版本
/*
onDateRangeChange = dates => {
console.log(dates);
this.search_date_start = dates[0];
this.search_date_end = dates[1];
};
/*
地接社sn
团名
离团时间开始结束
*/
searchFeedbackList(veisn, EOI_Group_Name, TimeStart, TimeEnd) {
this.loading = true;
let url = `/service-Cooperate/Cooperate/SearchFeedbackList`;
url += `?PageSize=2000&PageIndex=1&PageTotal=0&veisn=${veisn}&GruopNo=${EOI_Group_Name}&TimeStart=${TimeStart}&TimeEnd=${TimeEnd}`;
url += `&token=${this.root.authStore.login.token}`;
return fetchJSON(config.HT_HOST + url).then(json => {
// 反馈表, 有新版就用新版
const allGroup = groupBy(json.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_AsJOSN.map(rg => `${rg.GuideCity}: ${rg.GuideName}`).join(' ; ')}));
return r.concat(v2.length > 0 ? v2 : withAllGuide);
}, [])
runInAction(() => {
this.feedbackList = filterV;
this.loading = false;
});
if (json.errcode !== 0) {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
}
/*
GRI_SN 团SN
VEI_SN 供应商SN
*/
export const getFeedbackDetail = async (VEI_SN, GRI_SN) => {
const { errcode, Result, Result1 } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/getFeedbackDetail`, { VEI_SN, GRI_SN });
return errcode !== 0 ? {} : { feedbackRate: Result, feedbackReview: Result1 };
};
getFeedbackDetail(VEI_SN, GRI_SN) {
let url = `/service-Cooperate/Cooperate/getFeedbackDetail`;
url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}`;
url += `&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.feedbackRate = json.Result;
this.feedbackReview = json.Result1;
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
/**
* 客人填写的反馈
* @param {number} VEI_SN 供应商
* @param {number} GRI_SN
* @author LYT
* 2024-01-04
*/
export const getCustomerFeedbackDetail = async (VEI_SN, GRI_SN, CII_SN) => {
const json = await fetchJSON(`${HT_HOST}/service-CooperateSOA/get_feedback_service_item`, { VEI_SN, GRI_SN, city_sn: CII_SN, lgc: 1 });
const itemGroup = groupBy(json.feedbackItemList, 'type');
const serviceItem = {
HWO_Guide: itemGroup?.W || [],
HWO_Driver: itemGroup?.Y || [],
HWO_Activity: [...(itemGroup['7'] || []), ...(itemGroup.G || []), ...(itemGroup.C || []), ...(itemGroup.A || []).map((ele) => ({ ...ele, Describe: ele.name }))],
};
const OtherThoughts = json.feedbackEvaluation[0]?.otherComments || '';
const PhotoPermission = json.feedbackEvaluation[0]?.usePhotos || '';
const signatureData = json.feedbackEvaluation[0]?.signatureDataUrl || '';
const cityName = json.group[0]?.cityName || '';
return { ...serviceItem, OtherThoughts, PhotoPermission, signatureData, cityName }; // feedbackServiceRate
};
export const getFeedbackImages = async (VEI_SN, GRI_SN) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-fileServer/ListFile`, { VEI_SN, GRI_SN });
return errcode !== 0
? {}
: result.map((data, index) => {
return {
uid: -index, //用负数,防止添加删除的时候错误
name: data.file_name,
status: 'done',
url: data.file_url,
/**
* 客人填写的反馈
* @param {number} VEI_SN 供应商
* @param {number} GRI_SN
* @author LYT
* 2024-01-04
*/
getCustomerFeedbackDetail(VEI_SN, GRI_SN, CII_SN) {
let url = `/service-CooperateSOA/get_feedback_service_item`;
url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}&city_sn=${CII_SN}&lgc=1`;
url += `&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then((response) => response.json())
.then((json) => {
const itemGroup = groupBy(json.feedbackItemList, 'type');
const serviceItem = {
HWO_Guide: itemGroup?.W || [],
HWO_Driver: itemGroup?.Y || [],
HWO_Activity: [
...(itemGroup['7'] || []),
...(itemGroup.G || []),
...(itemGroup.C || []),
...(itemGroup.A || []).map((ele) => ({ ...ele, Describe: ele.name })),
],
};
});
};
const OtherThoughts = json.feedbackEvaluation[0]?.otherComments || '';
const PhotoPermission = json.feedbackEvaluation[0]?.usePhotos || '';
const signatureData = json.feedbackEvaluation[0]?.signatureDataUrl || '';
const cityName = json.group[0]?.cityName || '';
export const removeFeedbackImages = async (fileurl) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-fileServer/FileDelete`, { fileurl });
return errcode !== 0 ? {} : Result;
};
//获取供应商反馈信息
export const getFeedbackInfo = async (VEI_SN, GRI_SN) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/getVEIFeedbackInfo`, { VEI_SN, GRI_SN });
return errcode !== 0 ? {} : Result;
};
export const postFeedbackInfo = async (VEI_SN, GRI_SN, EOI_SN, info_content) => {
const postbody = { VEI_SN, GRI_SN, EOI_SN, FeedbackInfo: info_content };
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const { errcode, Result } = await postForm(`${HT_HOST}/service-CooperateSOA/FeedbackInfo`, formData);
return errcode !== 0 ? {} : Result;
};
const initialState = {
loading: false,
feedbackList: [], //反馈列表
};
runInAction(() => {
this.feedbackServiceRate = { ...serviceItem, OtherThoughts, PhotoPermission, signatureData, cityName };
});
})
.catch((error) => {
console.log('fetch data failed', error);
});
}
const useFeedbackStore = create(
devtools((set, get) => ({
...initialState,
reset: () => set(initialState),
//获取供应商提交的图片
getFeedbackImages(VEI_SN, GRI_SN) {
let url = `/service-fileServer/ListFile`;
url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}`;
url += `&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.feedbackImages = json.result.map((data, index) => {
return {
uid: -index, //用负数,防止添加删除的时候错误
name: data.file_name,
status: "done",
url: data.file_url,
};
});
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
setLoading: (loading) => set({ loading }),
setFeedbackList: (feedbackList) => set({ feedbackList }),
//获取供应商反馈信息
getFeedbackInfo(VEI_SN, GRI_SN) {
let url = `/service-Cooperate/Cooperate/getVEIFeedbackInfo`;
url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}`;
url += `&token=${this.root.authStore.login.token}`;
return fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.feedbackInfo = json.Result;
});
return json.Result;
})
.catch(error => {
console.log("fetch data failed", error);
});
}
//删除照片
removeFeedbackImages(fileurl) {
let url = `/service-fileServer/FileDelete`;
url += `?fileurl=${fileurl}`;
url += `&token=${this.root.authStore.login.token}`;
return fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
return json.Result;
})
.catch(error => {
console.log("fetch data failed", error);
});
}
/*
地接社sn
团名
离团时间开始结束
*/
async fetchFeedbackList(veisn, EOI_Group_Name, TimeStart, TimeEnd) {
const { setLoading, setFeedbackList } = get();
setLoading(true);
const searchParams = {
PageSize: 2000,
PageIndex: 1,
PageTotal: 0,
veisn: veisn,
GruopNo: EOI_Group_Name,
TimeStart,
TimeEnd,
};
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/SearchFeedbackList`, searchParams);
const _result = errcode !== 0 ? [] : Result;
// 反馈表, 有新版就用新版
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_AsJOSN.map((rg) => `${rg.GuideCity}: ${rg.GuideName}`).join(' ; ') }));
return r.concat(v2.length > 0 ? v2 : withAllGuide);
}, []);
setFeedbackList(filterV);
setLoading(false);
},
}), { name: 'feedbackStore'})
);
//提交供应商反馈信息
postFeedbackInfo(VEI_SN, GRI_SN, EOI_SN, info_content) {
let url = `/service-CooperateSOA/FeedbackInfo`;
let formData = new FormData();
formData.append("VEI_SN", VEI_SN);
formData.append("GRI_SN", GRI_SN);
formData.append("EOI_SN", EOI_SN);
formData.append("FeedbackInfo", info_content);
formData.append("token", this.root.authStore.login.token);
return fetch(config.HT_HOST + url, {
method: "POST",
body: formData,
})
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
}
export default useFeedbackStore;
export default Feedback;

@ -1,12 +0,0 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const useFormStore = create(
devtools((set, get) => ({
formValues: {},
setFormValues: (values) => set((state) => ({ formValues: { ...state.formValues, ...values } })),
formValuesToSub: {},
setFormValuesToSub: (values) => set((state) => ({ formValuesToSub: { ...state.formValuesToSub, ...values } })),
}), { name: 'formStore' })
);
export default useFormStore;

@ -1,135 +1,370 @@
import { makeAutoObservable, runInAction } from "mobx";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST } from "@/config";
import { json } from "react-router-dom";
import * as config from "@/config";
import dayjs from "dayjs";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
export const fetchInvoiceDetail = async (VEI_SN, GMD_SN, GRI_SN) => {
const { errcode, ...json } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetZDDetail`, { VEI_SN, GMD_SN, GRI_SN, LGC: 1, Bill: 1 });
return errcode !== 0 ? {} : { invoiceGroupInfo: json.GroupInfo[0], invoiceProductList: json.ProductList, invoiceCurrencyList: json.CurrencyList, invoiceZDDetail: json.ZDDetail };
};
export const getInvoicekImages = async (VEI_SN, GRI_SN) => {
const { errcode, result, ...json } = await fetchJSON(`${HT_HOST}/service-fileServer/ListFile`, { VEI_SN, GRI_SN, FilePathName: 'invoice' });
return errcode !== 0 ? [] : result.map((data, index) => {
return {
uid: -index, //用负数,防止添加删除的时候错误
name: data.file_name,
status: "done",
url: data.file_url,
};
});
};
export const postEditInvoiceDetail = async (LMI_SN, GMD_SN, Currency, Cost, PayDate, Pic, Memo) => {
const postbody = { LMI_SN, GMD_SN, Currency, Cost, PayDate, Pic, Memo };
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const res = await postForm(`${HT_HOST}/service-cusservice/EditSupplierFK`, formData);
return res;
};
export const postAddInvoice = async (LMI_SN, VEI_SN, GRI_SN, Currency, Cost, PayDate, Pic, Memo) => {
const postbody = { LMI_SN, VEI_SN, GRI_SN, Currency, Cost, PayDate, Pic, Memo };
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const res = await postForm(`${HT_HOST}/service-cusservice/AddSupplierFK`, formData);
return res;
};
export const getInvoicePaid = async (VEI_SN, GroupNo, DateStart, DateEnd) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/GetInvoicePaid`, {VEI_SN, GroupNo, DateStart, DateEnd});
return errcode !== 0 ? [] : (Result || []).map((data, index) => ({ ...data, key: data.fl_id, }));
};
export const fetchInvoicePaidDetail = async (VEI_SN, FLID) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/GetInvoicePaidDetail`, { VEI_SN, fl_id: FLID });
return errcode !== 0 ? [] : (Result || []).map((data, index) => ({ ...data, key: data.fl2_id, }));
};
const initialState = {
invoiceList: [], //账单列表
invoicekImages: [], //图片列表
invoiceGroupInfo: {}, //账单详细
invoiceProductList: [], //账单细项
invoiceZDDetail: [], //报账信息
invoiceCurrencyList: [], //币种
invoicePicList: [], //多账单图片列表数组
invoiceFormData: { info_money: 0, info_Currency: '', info_date: '' }, //存储form数据
invoicePaid: [], //支付账单列表
invoicePaidDetail: [], //每期账单详细
loading: false,
};
const useInvoiceStore = create(
devtools((set, get) => ({
// 初始化状态
...initialState,
reset: () => set(initialState),
setLoading: (loading) => set(() => ({ loading })),
setInvoiceList: (invoiceList) => set(() => ({ invoiceList })),
setInvoicePaid: (invoicePaid) => set(() => ({ invoicePaid })),
async fetchInvoiceList(VEI_SN, GroupNo, DateStart, DateEnd,OrderType){
const { setLoading, setInvoiceList } = get();
setLoading(true);
const searchParams = {
VEI_SN,
GroupNo,
DateStart,
DateEnd,
OrderType: 0,
TimeType: 0,
limitmarket: '',
mddgroup: '',
SecuryGroup: '',
TotalNum: 0,
PageSize: 2000,
PageIndex: 1,
PayState: OrderType,
};
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTSearchGMBPageList`, searchParams);
const _result = errcode !== 0 ? [] : (Result || []).map((data, index) => {
return {
key: data.GMDSN,
gmd_gri_sn: data.GMD_GRI_SN,
gmd_vei_sn: data.GMD_VEI_SN,
GetGDate: data.GetGDate,
GMD_FillWorkers_SN: data.GMD_FillWorkers_SN,
GMD_FWks_LastEditTime: data.GMD_FWks_LastEditTime,
GMD_VerifyUser_SN: data.GMD_VerifyUser_SN,
GMD_Dealed: data.GMD_Dealed,
GMD_VRequestVerify: data.GMD_VRequestVerify,
LeftGDate: data.LeftGDate,
GMD_FillWorkers_Name: data.GMD_FillWorkers_Name,
GroupName: data.GroupName,
AllMoney: data.AllMoney,
PersonNum: data.PersonNum,
GMD_Currency: data.GMD_Currency,
VName: data.VName,
FKState: data.FKState,
};
});
setInvoiceList(_result);
setLoading(false);
},
async fetchInvoicePaid(VEI_SN, GroupNo, DateStart, DateEnd){
const { setLoading, setInvoicePaid } = get();
setLoading(true);
const res = await getInvoicePaid(VEI_SN, GroupNo, DateStart, DateEnd);
setInvoicePaid(res);
setLoading(false);
},
}), { name: 'invoiceStore'})
);
export default useInvoiceStore;
class Invoice {
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
}
invoiceList = []; //账单列表
invoicekImages = []; //图片列表
invoiceGroupInfo = {}; //账单详细
invoiceProductList = []; //账单细项
invoiceZDDetail = []; //报账信息
invoiceCurrencyList = []; //币种
invoicePicList = []; //多账单图片列表数组
invoiceFormData = { info_money: 0, info_Currency: "", info_date: "" }; //存储form数据
invoicePaid = [] ; //支付账单列表
invoicePaidDetail = []; //每期账单详细
loading = false;
search_date_start = dayjs().subtract(2, "M").startOf("M");
search_date_end = dayjs().endOf("M");
onDateRangeChange = dates => {
console.log(dates);
this.search_date_start = dates==null? null: dates[0];
this.search_date_end = dates==null? null: dates[1];
};
fetchInvoiceList(VEI_SN, GroupNo, DateStart, DateEnd,OrderType) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTSearchGMBPageList")
.append("VEI_SN", VEI_SN)
.append("OrderType", 0)
.append("GroupNo", GroupNo.trim())
.append("DateStart", DateStart)
.append("DateEnd", DateEnd)
.append("Orderbytype", 1)
.append("TimeType", 0)
.append("limitmarket", "")
.append("mddgroup", "")
.append("SecuryGroup", "")
.append("TotalNum", 0)
.append("PageSize", 2000)
.append("PageIndex", 1)
.append("PayState",OrderType)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json.Result)) {
this.invoiceList = json.Result.map((data, index) => {
return {
key: data.GMDSN,
gmd_gri_sn: data.GMD_GRI_SN,
gmd_vei_sn: data.GMD_VEI_SN,
GetGDate: data.GetGDate,
GMD_FillWorkers_SN: data.GMD_FillWorkers_SN,
GMD_FWks_LastEditTime: data.GMD_FWks_LastEditTime,
GMD_VerifyUser_SN: data.GMD_VerifyUser_SN,
GMD_Dealed: data.GMD_Dealed,
GMD_VRequestVerify: data.GMD_VRequestVerify,
LeftGDate: data.LeftGDate,
GMD_FillWorkers_Name: data.GMD_FillWorkers_Name,
GroupName: data.GroupName,
AllMoney: data.AllMoney,
PersonNum: data.PersonNum,
GMD_Currency: data.GMD_Currency,
VName: data.VName,
FKState: data.FKState,
};
});
} else {
this.invoiceList = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
fetchInvoiceDetail(GMDSN, GSN) {
const fetchUrl = prepareUrl(HT_HOST + "/service-cusservice/PTGetZDDetail")
.append("VEI_SN", this.root.authStore.login.travelAgencyId)
.append("GRI_SN", GSN)
.append("GMD_SN", GMDSN)
.append("LGC", 1)
.append("Bill", 1)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
if (json.errcode == 0) {
this.invoiceGroupInfo = json.GroupInfo[0];
this.invoiceProductList = json.ProductList;
this.invoiceCurrencyList = json.CurrencyList;
this.invoiceZDDetail = json.ZDDetail;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
return json;
});
}
//获取供应商提交的图片
getInvoicekImages(VEI_SN, GRI_SN) {
let url = `/service-fileServer/ListFile`;
url += `?GRI_SN=${GRI_SN}&VEI_SN=${VEI_SN}&FilePathName=invoice`;
url += `&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.invoicekImages = json.result.map((data, index) => {
return {
uid: -index, //用负数,防止添加删除的时候错误
name: data.file_name,
status: "done",
url: data.file_url,
};
});
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
//从数据库获取图片列表
getInvoicekImages_fromData(jsonData) {
let arrLen = jsonData.length;
let arrPicList = jsonData.map((data, index) => {
const GMD_Pic = data.GMD_Pic;
let picList = [];
if (isNotEmpty(GMD_Pic)) {
let js_Pic = JSON.parse(GMD_Pic);
picList = js_Pic.map((picData, pic_Index) => {
return {
uid: -pic_Index, //用负数,防止添加删除的时候错误
name: "",
status: "done",
url: picData.url,
};
});
}
if (data.GMD_Dealed == false && arrLen == index + 1) {
this.invoicekImages = picList;
}
return picList;
});
runInAction(() => {
this.invoicePicList = arrPicList;
});
}
//获取数据库的表单默认数据回填。
getFormData(jsonData) {
let arrLen = jsonData.length;
return jsonData.map((data, index) => {
if (data.GMD_Dealed == false && arrLen == index + 1) {
//只有最后一条账单未审核通过才显示
runInAction(() => {
this.invoiceFormData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "" };
});
}
});
}
removeFeedbackImages(fileurl) {
let url = `/service-fileServer/FileDelete`;
url += `?fileurl=${fileurl}`;
url += `&token=${this.root.authStore.login.token}`;
return fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
return json.Result;
})
.catch(error => {
console.log("fetch data failed", error);
});
}
postEditInvoiceDetail(GMD_SN, Currency, Cost, PayDate, Pic, Memo) {
let postUrl = HT_HOST + "/service-cusservice/EditSupplierFK";
let formData = new FormData();
formData.append("LMI_SN", this.root.authStore.login.userId);
formData.append("GMD_SN", GMD_SN);
formData.append("Currency", Currency);
formData.append("Cost", Cost);
formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : "");
formData.append("Pic", Pic);
formData.append("Memo", Memo);
formData.append("token",this.root.authStore.login.token);
return postForm(postUrl, formData).then(json => {
console.info(json);
return json;
});
}
postAddInvoice(GRI_SN, Currency, Cost, PayDate, Pic, Memo) {
let postUrl = HT_HOST + "/service-cusservice/AddSupplierFK";
let formData = new FormData();
formData.append("LMI_SN", this.root.authStore.login.userId);
formData.append("VEI_SN", this.root.authStore.login.travelAgencyId);
formData.append("GRI_SN", GRI_SN);
formData.append("Currency", Currency);
formData.append("Cost", Cost);
formData.append("PayDate", isNotEmpty(PayDate) ? PayDate : "");
formData.append("Pic", Pic);
formData.append("Memo", Memo);
formData.append("token",this.root.authStore.login.token);
return postForm(postUrl, formData).then(json => {
console.info(json);
return json;
});
}
//账单状态
invoiceStatus(FKState) {
switch (FKState - 1) {
case 1:
return "Submitted";
break;
case 2:
return "Travel Advisor";
break;
case 3:
return "Finance Dept";
break;
case 4:
return "Paid";
break;
default:
return "";
break;
}
}
fetchInvoicePaid(VEI_SN, GroupNo, DateStart, DateEnd) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaid")
.append("VEI_SN", VEI_SN)
.append("GroupNo", GroupNo)
.append("DateStart", DateStart)
.append("DateEnd", DateEnd)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json.Result)) {
this.invoicePaid = json.Result.map((data, index) => {
return {
key: data.fl_id,
fl_finaceNo: data.fl_finaceNo,
fl_vei_sn: data.fl_vei_sn,
fl_year: data.fl_year,
fl_month: data.fl_month,
fl_memo: data.fl_memo,
fl_adddate: data.fl_adddate,
fl_addUserSn: data.fl_addUserSn,
fl_updateUserSn: data.fl_updateUserSn,
fl_updatetime: data.fl_updatetime,
fl_state: data.fl_state,
fl_paid: data.fl_paid,
fl_pic: data.fl_pic,
fcount: data.fcount,
pSum: data.pSum,
};
});
} else {
this.invoicePaid = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
fetchInvoicePaidDetail(VEI_SN,FLID){
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST + "/service-Cooperate/Cooperate/GetInvoicePaidDetail")
.append("VEI_SN", VEI_SN)
.append("fl_id", FLID)
.append("token",this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json.Result)) {
this.invoicePaidDetail = json.Result.map((data, index) => {
return {
key: data.fl2_id,
fl2_fl_id: data.fl2_fl_id,
fl2_GroupName: data.fl2_GroupName,
fl2_gri_sn: data.fl2_gri_sn,
fl2_gmd_sn: data.fl2_gmd_sn,
fl2_wl: data.fl2_wl,
fl2_ArriveDate: data.fl2_ArriveDate,
fl2_price: data.fl2_price,
fl2_state: data.fl2_state,
fl2_updatetime: data.fl2_updatetime,
fl2_updateUserSn: data.fl2_updateUserSn,
fl2_memo: data.fl2_memo,
fl2_memo2: data.fl2_memo2,
fl2_paid: data.fl2_paid,
fl2_pic: data.fl2_pic,
};
});
} else {
this.invoicePaidDetail = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
/* 测试数据 */
//账单列表范例数据
testData = [
{
GSMSN: 449865,
gmd_gri_sn: 334233,
gmd_vei_sn: 628,
GetDate: "2023-04-2 00:33:33",
GMD_FillWorkers_SN: 8617,
GMD_FWks_LastEditTime: "2023-04-26 12:33:33",
GMD_VerifyUser_SN: 8928,
GMD_Dealed: 1,
GMD_VRequestVerify: 1,
TotalCount: 22,
LeftGDate: "2023-03-30 00:00:00",
GMD_FillWorkers_Name: "",
GroupName: " 中华游230501-CA230402033",
AllMoney: 3539,
FKState: 1,
GMD_Currency: "",
PersonNum: "1大1小",
VName: "",
},
];
}
export default Invoice;

@ -1,53 +1,73 @@
import { makeAutoObservable, runInAction } from "mobx";
import * as config from "@/config";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
class Notice {
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
}
import { fetchJSON, } from '@/utils/request';
const { HT_HOST } = config;
noticeList = []; //公告列表
noticeUnRead = 0; //未读公告数量
noticeInfo = { CCP_BLID: 0, CCP_BLTitle: "", CCP_BLContent: "", CCP_LastEditTime: "" }; //公告详情
/**
* Notice 相关的请求
*/
/*
LMI_SN 登录用户SN用户sn用来判断是否已读公告
*/
getBulletinList(LMI_SN) {
let url = `/service-Cooperate/Cooperate/GetBulletinList`;
url += `?LMI_SN=${LMI_SN}`;
url+=`&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
runInAction(() => {
this.noticeList = json.Result;
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
export const fetchBulletinList = async (LMI_SN) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/GetBulletinList`, { LMI_SN });
return errcode !== 0 ? [] : Result;
};
/*
LMI_SN 登录用户sn 用户sn用来设置已读公告请求过一次详情页表示已读
CCP_BLID 公告sn
*/
getNoticeDetail(LMI_SN, CCP_BLID) {
let url = `/service-Cooperate/Cooperate/GetBulletinDetail`;
url += `?LMI_SN=${LMI_SN}&CCP_BLID=${CCP_BLID}`;
url+=`&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.noticeInfo = json.Result;
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
export const fetchBulletinUnReadCount = async (LMI_SN) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/GetBulletinUnReadCount`, { LMI_SN });
return errcode !== 0 ? 0 : Result.CCP_BulletinCount;
//检查是否有未读公告
getBulletinUnReadCount(LMI_SN) {
let url = `/service-Cooperate/Cooperate/GetBulletinUnReadCount`;
url += `?LMI_SN=${LMI_SN}`;
url+=`&token=${this.root.authStore.login.token}`;
fetch(config.HT_HOST + url)
.then(response => response.json())
.then(json => {
console.log(json);
runInAction(() => {
this.noticeUnRead = json.Result.CCP_BulletinCount;
});
})
.catch(error => {
console.log("fetch data failed", error);
});
}
}
export const fetchNoticeDetail = async (LMI_SN, CCP_BLID) => {
const { errcode, Result } = await fetchJSON(`${HT_HOST}/service-Cooperate/Cooperate/GetBulletinDetail`, { LMI_SN, CCP_BLID });
return errcode !== 0 ? {} : Result;
}
/**
* Notice Store
*/
const initialState = {
noticeUnRead: 0, //未读公告数量
};
export const useNoticeStore = create(
devtools((set, get) => ({
// 初始化状态
...initialState,
// state actions
setNoticeUnRead: (noticeUnRead) => set(() => ({ noticeUnRead })),
reset: () => set(initialState),
// side effects
getBulletinUnReadCount: async (LMI_SN) => {
const { setNoticeUnRead } = get();
const noticeUnRead = await fetchBulletinUnReadCount(LMI_SN);
setNoticeUnRead(noticeUnRead);
},
}), { name: 'noticeStore' })
);
export default useNoticeStore;
export default Notice;

@ -1,453 +0,0 @@
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';
export const searchAgencyAction = async (param) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param);
return errcode !== 0 ? [] : result;
};
/**
* 搜索所有产品, 返回产品列表
* ! 只有审核通过, 已发布的
* @param {object} params { keyword, use_year, product_types, travel_agency_id, city }
*/
export const searchPublishedProductsAction = async (param) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/web_products_search`, param);
return errcode !== 0 ? [] : result;
};
export const copyAgencyDataAction = async (postbody) => {
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const { errcode, result } = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_products_copy`, formData);
return errcode === 0 ? true : false;
};
export const getAgencyProductsAction = async (param) => {
const _param = { ...param, use_year: String(param.use_year || '').replace('all', ''), audit_state: String(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;
};
/**
*
*/
export const delProductExtrasAction = async (body) => {
// return true; // test: 先不更新到HT
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_del`, body);
return errcode === 0 ? true : false;
};
/**
* 获取指定产品的附加项目
* @param {object} param { id, travel_agency_id, use_year }
*/
export const getAgencyProductExtrasAction = async (param) => {
const _param = { ...param, use_year: String(param.use_year || '').replace('all', '') };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param);
return errcode !== 0 ? [] : result;
};
/**
* 审核一条价格
*/
export const postProductsQuoteAuditAction = async (auditState, quoteRow) => {
const postbody = {
audit_state: auditState,
id: quoteRow.id,
travel_agency_id: quoteRow.travel_agency_id,
};
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/quotation_audit`, formData);
return json;
// return errcode !== 0 ? {} : result;
};
export const postAgencyProductsAuditAction = async (auditState, agency) => {
const postbody = {
audit_state: auditState,
travel_agency_id: agency.travel_agency_id,
use_year: agency.use_year,
};
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);
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 }
}
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: {}, // 头部切换参数
}
export const useProductsStore = create(
devtools((set, get) => ({
// 初始化状态
...initialState,
// state actions
setLoading: (loading) => set({ loading }),
setSearchValues: (searchValues) => set({ searchValues }),
setAgencyList: (agencyList) => set({ agencyList }),
setActiveAgency: (activeAgency) => set({ activeAgency }),
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 },
});
},
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: () => ({
id: null,
adult_cost: 0,
child_cost: 0,
currency: 'RMB',
unit_id: '0',
group_size_min: 1,
group_size_max: 10,
use_dates: [
dayjs().startOf('M'),
dayjs().endOf('M')
],
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 changedList = []
for (const [key, value] of Object.entries(formValues)) {
if (key === 'use_dates' || key === 'id' || key === 'key') continue
const preValue = prevQuotation[key]
const hasChanged = preValue !== value
if (hasChanged) {
changedList.push({
[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: JSON.stringify(changedList, null, 2)
}
} 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
})
let promiseDelete = Promise.resolve(newQuotationList)
if (isNotEmpty(quotationId)) {
const { result, success } = await deleteQuotationAction(quotationId)
if (!success) {
promiseDelete = Promise.reject(result)
}
}
return promiseDelete
},
// side effects
searchAgency: async (param) => {
const { setLoading, setAgencyList } = get();
setLoading(true);
const res = await searchAgencyAction(param);
setAgencyList(res);
setLoading(false);
},
getAgencyProducts: async (param) => {
const { setLoading, setActiveAgency, setActiveAgencyState, setAgencyProducts, editingProduct, setEditingProduct } = 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);
},
getAgencyProductExtras: async (param) => {
const res = await getAgencyProductExtrasAction(param);
// todo:
},
}), { name: 'productStore' })
);
export default useProductsStore;

@ -1,69 +1,113 @@
import { fetchJSON } from "@/utils/request";
import { makeAutoObservable, runInAction } from "mobx";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST } from "@/config";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { json } from "react-router-dom";
import * as config from "@/config";
import dayjs from "dayjs";
const initialState = {
loading: false,
vendorScoresData: [], //地接统计数据集,合计数据,每月数据,地接考核分数
productScoresData: [], //产品体验分析 常用酒店分析, 导游接待情况
commendScoresData: [], //表扬情况, 投诉情况, 评建议
};
export const useReportStore = create(
devtools((set, get) => ({
...initialState,
reset: () => set(initialState),
class Report {
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
}
setLoading: (loading) => set({ loading }),
setVendorScoresData: (vendorScoresData) => set({ vendorScoresData }),
setProductScoresData: (productScoresData) => set({ productScoresData }),
setCommendScoresData: (commendScoresData) => set({ commendScoresData }),
vendorScoresData = []; //地接统计数据集,合计数据,每月数据,地接考核分数
productScoresData = []; //产品体验分析 常用酒店分析, 导游接待情况
commendScoresData = []; //表扬情况, 投诉情况, 评建议
async getHWVendorScores(VEI_SN, StartDate, EndDate) {
const { setLoading, setVendorScoresData } = get();
setLoading(true);
const searchParams = {
VEI_SN,
StartDate,
EndDate,
StrDEI_SN: '(,-1,)',
OrderType: '-1',
GroupType: '-1',
};
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWVendorScores`, searchParams);
setVendorScoresData(errcode === 0 ? Result : {});
// setLoading(false);
},
async getHWProductScores(VEI_SN, StartDate, EndDate) {
const { setLoading, setProductScoresData } = get();
setLoading(true);
const searchParams = {
VEI_SN,
StartDate,
EndDate,
StrDEI_SN: '(,-1,)',
OrderType: '-1',
GroupType: '-1',
};
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWProductScores`, searchParams);
setProductScoresData(errcode === 0 ? Result : {});
setLoading(false);
},
async getHWCommendScores(VEI_SN, StartDate, EndDate) {
const { setLoading, setCommendScoresData } = get();
setLoading(true);
const searchParams = {
VEI_SN,
StartDate,
EndDate,
StrDEI_SN: '(,-1,)',
OrderType: '-1',
GroupType: '-1',
};
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWCommendScores`, searchParams);
setCommendScoresData(errcode === 0 ? Result : {});
// setLoading(false);
},
}), { name: 'reportStore'})
);
export default useReportStore;
loading = false;
search_date_start = dayjs().month(0).startOf("month");
search_date_end = dayjs().month(11).endOf("month");
onDateRangeChange = dates => {
this.search_date_start = dates == null ? null : dates[0].startOf("month");
this.search_date_end = dates == null ? null : dates[1].endOf("month");
};
getHWVendorScores(VEI_SN, StartDate, EndDate) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST +"/service-cusservice/PTGetHWVendorScores")
.append("VEI_SN", VEI_SN)
.append("StartDate", StartDate)
.append("EndDate", EndDate)
.append("StrDEI_SN", "(,-1,)")
.append("OrderType", "-1")
.append("GroupType", "-1")
.append("token", this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json)) {
this.vendorScoresData = json;
} else {
this.vendorScoresData = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
getHWProductScores(VEI_SN, StartDate, EndDate) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST +"/service-cusservice/PTGetHWProductScores")
.append("VEI_SN", VEI_SN)
.append("StartDate", StartDate)
.append("EndDate", EndDate)
.append("StrDEI_SN", "(,-1,)")
.append("OrderType", "-1")
.append("GroupType", "-1")
.append("token", this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json)) {
this.productScoresData = json;
} else {
this.productScoresData = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
getHWCommendScores(VEI_SN, StartDate, EndDate) {
this.loading = true;
const fetchUrl = prepareUrl(HT_HOST +"/service-cusservice/PTGetHWCommendScores")
.append("VEI_SN", VEI_SN)
.append("StartDate", StartDate)
.append("EndDate", EndDate)
.append("StrDEI_SN", "(,-1,)")
.append("OrderType", "-1")
.append("GroupType", "-1")
.append("token", this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl).then(json => {
runInAction(() => {
this.loading = false;
if (json.errcode == 0) {
if (isNotEmpty(json)) {
this.commendScoresData = json;
} else {
this.commendScoresData = [];
}
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
});
}
}
export default Report;

@ -1,271 +1,271 @@
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'
import { usingStorage } from '@/hooks/usingStorage'
import { makeAutoObservable, runInAction } from "mobx";
import { fetchJSON, postForm } from '@/utils/request';
import { HT_HOST } from "@/config";
import { prepareUrl } from '@/utils/commons';
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
}
export const fetchPlanDetail = async (travelAgencyId, reservationId) => {
const json = await fetchJSON(
`${HT_HOST}/service-cusservice/GetPlanInfo`,
{ VEI_SN: travelAgencyId, GRI_SN: reservationId })
class Reservation {
if (json.errcode == 0) {
return {
planDetail: json.PlanDetail == null ? {} : json.PlanDetail[0],
planChangeList: json.PlanChange??[]
}
} else {
return {}
constructor(root) {
makeAutoObservable(this, { rootStore: false });
this.root = root;
}
}
export const fetchAttachList = async (reservationId) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-fileServer/PlanChangeFileList`,
{ GRI_SN: reservationId })
return errcode !== 0 ? {} : result
}
const useReservationStore = create(devtools((set, get) => ({
cityList: [],
selectedReservation: null,
selectedConfirmation: null,
arrivalDateRange: [],
referenceNo: '',
reservationList: [],
reservationDetail: {
referenceNumber: '', arrivalDate: '', tourGuide: ''
},
reservationPage: {
current: 1,
size: 10,
total: 0
},
fetchReservationList(current, status=null) {
const fromDate = this.arrivalDateRange.length == 0 ? null : this.arrivalDateRange[0].format('YYYY-MM-DD');
const thruDate = this.arrivalDateRange.length == 0 ? null : this.arrivalDateRange[1].format('YYYY-MM-DD');
this.reservationPage.current = current;
// 设置为 0后端会重新计算总数当跳转第 X 页时可用原来的总数。
const totalNum = current == 1 ? 0 : this.reservationPage.total;
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/GetPlanSearchList')
.append('VEI_SN', this.root.authStore.login.travelAgencyId)
.append('GroupNo', this.referenceNo)
.append('DateStart', fromDate)
.append('DateEnd', thruDate)
.append('NotConfirm', status)
.append('TotalNum', totalNum)
.append('PageSize', this.reservationPage.size)
.append('PageIndex', this.reservationPage.current)
.append("token", this.root.authStore.login.token)
.build();
confirmationList: [
],
return fetchJSON(fetchUrl)
.then(json => {
if (json.errcode == 0) {
runInAction(() => {
this.reservationList = (json?.Result??[]).map((data, index) => {
return {
key: data.vas_gri_sn,
reservationId: data.vas_gri_sn,
referenceNumber: data.GriName,
arrivalDate: data.GetGDate,
pax: data.PersonNum,
status: data.GState,
reservationDate: data.SendDate,
guide: data.Guide
}
});
this.reservationPage.total = (json?.Result??[{RsTotal: 0}])[0].RsTotal;
});
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
}
getCityListByReservationId: async (reservationId) => {
const { travelAgencyId } = usingStorage()
set(() => ({
cityList: []
}))
fetchReservation(reservationId) {
const fetchDetailUrl = prepareUrl(HT_HOST + '/service-cusservice/GetPlanInfo')
.append('VEI_SN', this.root.authStore.login.travelAgencyId)
.append('GRI_SN', reservationId)
.append("token", this.root.authStore.login.token)
.build();
// https://p9axztuwd7x8a7.mycht.cn/service-fileServer/PlanChangeFileList
const fetchAttachmentUrl = prepareUrl(HT_HOST + '/service-fileServer/PlanChangeFileList')
.append('GRI_SN', reservationId)
.append("token", this.root.authStore.login.token)
.build();
const attachmentPromise = fetchJSON(fetchAttachmentUrl)
.then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
const cityListJson = await fetchCityList(travelAgencyId, reservationId)
const mapCityList = cityListJson.map((data) => {
return {
key: data.CII_SN,
cityId: data.CII_SN,
cityName: data.CityName,
tourGuideId: data.TGI_SN,
tourGuide: data.GuideName
}
})
set(() => ({
cityList: mapCityList
}))
},
const detailPromise = fetchJSON(fetchDetailUrl)
.then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
return Promise.all([attachmentPromise, detailPromise])
.then(results => {
const attachList = results[0].result;
const planDetail = results[1].PlanDetail[0];
const planChange = results[1]?.PlanChange??[];
runInAction(() => {
this.reservationDetail = {
referenceNumber: planDetail.GRI_Name,
tourGuide: planDetail.Guide,
arrivalDate: planDetail.eoi_getdate,
reservationId: reservationId
};
this.confirmationList = planChange.map((data, index) => {
const filterAttchList = attachList.filter(attch => {
return attch.PCI_SN === data.PCI_SN;
});
return {
key: data.PCI_SN,
PCI_Changetext: data.PCI_Changetext,
PCI_SendDate: data.PCI_SendDate,
ConfirmPerson: data.ConfirmPerson,
PCI_ConfirmText: data.PCI_ConfirmText,
PCI_ConfirmDate: data.PCI_ConfirmDate,
VAS_SN: data.PCI_VAS_SN,
attachmentList: filterAttchList
}
});
});
});
}
selectReservation: (reservation) => {
set(() => ({
selectedReservation: reservation
}))
},
fetchCityList(reservationId) {
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetCityGuide')
.append('VEI_SN', this.root.authStore.login.travelAgencyId)
.append('GRI_SN', reservationId)
.append('LGC', 1)
.append("token", this.root.authStore.login.token)
.build();
selectConfirmation: (confirmation) => {
set(() => ({
selectedConfirmation: confirmation
}))
},
runInAction(() => {
this.cityList = [];
});
return fetchJSON(fetchUrl)
.then(json => {
runInAction(() => {
if (json.errcode == 0) {
this.cityList = (json?.Result??[]).map((data, index) => {
return {
key: data.CII_SN,
cityId: data.CII_SN,
cityName: data.CityName,
tourGuideId: data.TGI_SN,
tourGuide: data.GuideName
}
});
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
});
}
fetchReservationList: (formValues, current=1) => {
const { travelAgencyId } = usingStorage()
const { reservationPage } = get()
// 设置为 0后端会重新计算总数当跳转第 X 页时可用原来的总数。
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('TotalNum', totalNum)
.append('PageSize', reservationPage.size)
.append('PageIndex', current)
.build()
updateReservationGuide() {
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetCityGuide')
.append('VEI_SN', this.root.authStore.login.travelAgencyId)
.append('GRI_SN', this.selectedReservation.reservationId)
.append('LGC', 1)
.append("token", this.root.authStore.login.token)
.build();
return fetchJSON(fetchUrl)
.then(json => {
if (json.errcode == 0) {
const mapReservationList = (json?.Result??[]).map((data) => {
return {
key: data.vas_gri_sn,
reservationId: data.vas_gri_sn,
referenceNumber: data.GriName,
arrivalDate: data.GetGDate,
pax: data.PersonNum,
status: data.GState,
reservationDate: data.SendDate,
guide: data.Guide
}
})
set((state) => ({
reservationList: mapReservationList,
reservationPage: {
...
state.reservationPage,
current: current,
total: (json?.Result??[{RsTotal: 0}])[0].RsTotal
}
}))
const reservationGuide = (json?.Result??[]).filter((data) => {
return data.TGI_SN != 0;
}).map((data) => {
return data.GuideName;
}).join(',');
runInAction(() => {
this.selectedReservation.guide = reservationGuide;
});
return reservationGuide;
} else {
throw new Error(json.errmsg + ': ' + json.errcode)
throw new Error(json.errmsg + ': ' + json.errcode);
}
})
},
});
}
fetchAllGuideList: () => {
const { travelAgencyId } = usingStorage()
fetchAllGuideList() {
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetGuideList')
.append('VEI_SN', travelAgencyId)
.build()
.append('VEI_SN', this.root.authStore.login.travelAgencyId)
.append("token", this.root.authStore.login.token)
.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) => {
const { travelAgencyId } = usingStorage()
const { planDetail, planChangeList } = await fetchPlanDetail(travelAgencyId, reservationId)
const attachListJson = await fetchAttachList(reservationId)
});
}
const mapConfirmationList = planChangeList.map((data) => {
const filterAttchList = attachListJson.filter(attch => {
return attch.PCI_SN === data.PCI_SN
})
return {
key: data.PCI_SN,
PCI_Changetext: data.PCI_Changetext,
PCI_SendDate: data.PCI_SendDate,
ConfirmPerson: data.ConfirmPerson,
PCI_ConfirmText: data.PCI_ConfirmText,
PCI_ConfirmDate: data.PCI_ConfirmDate,
VAS_SN: data.PCI_VAS_SN,
attachmentList: filterAttchList
}
})
setupCityGuide(cityId, guideId) {
let formData = new FormData();
formData.append('GRI_SN', this.selectedReservation.reservationId);
formData.append('VEI_SN', this.root.authStore.login.travelAgencyId);
formData.append('TGI_SN', guideId);
formData.append('CII_SN', cityId);
formData.append('GetDate', this.selectedReservation.reservationDate);
formData.append('LMI_SN', this.root.authStore.login.userId);
formData.append("token", this.root.authStore.login.token);
const postUrl = HT_HOST + '/service-cusservice/PTAddGuide';
set(() => ({
reservationDetail: {
referenceNumber: planDetail.GRI_Name,
tourGuide: planDetail.Guide,
arrivalDate: planDetail.eoi_getdate,
reservationId: reservationId
},
confirmationList: mapConfirmationList
}))
},
return postForm(postUrl, formData)
.then(json => {
if (json.errcode != 0) {
throw new Error(json.errmsg + ': ' + json.errcode);
}
});
}
submitConfirmation: (confirmText) => {
const { userId, travelAgencyId } = usingStorage()
const { selectedConfirmation, getReservationDetail, reservationDetail } = get()
const formData = new FormData()
formData.append('PCI_SN', selectedConfirmation.key)
formData.append('OPSN', userId)
formData.append('ConfirmText', confirmText)
formData.append('VAS_SN', selectedConfirmation.VAS_SN)
submitConfirmation(confirmText) {
let formData = new FormData();
formData.append('PCI_SN', this.selectedConfirmation.key);
formData.append('OPSN', this.root.authStore.login.userId);
formData.append('ConfirmText', confirmText);
formData.append('VAS_SN', this.selectedConfirmation.VAS_SN);
formData.append("token", this.root.authStore.login.token);
const postUrl = HT_HOST + '/service-cusservice/PTConfirmPlanChange'
const postUrl = HT_HOST + '/service-cusservice/PTConfirmPlanChange';
return postForm(postUrl, formData)
.then(json => {
if (json.errcode == 0 && json.Result.length == 1) {
getReservationDetail(travelAgencyId, reservationDetail.reservationId)
return json
this.fetchReservation(this.reservationDetail.reservationId);
return json;
}
})
},
});
}
setupCityGuide: (cityId, guideId) => {
const { selectedReservation } = get()
const { userId, travelAgencyId } = usingStorage()
const formData = new FormData()
formData.append('GRI_SN', selectedReservation.reservationId)
formData.append('VEI_SN', travelAgencyId)
formData.append('TGI_SN', guideId)
formData.append('CII_SN', cityId)
formData.append('GetDate', selectedReservation.reservationDate)
formData.append('LMI_SN', userId)
const postUrl = HT_HOST + '/service-cusservice/PTAddGuide'
editReservation(reservation) {
this.selectedReservation = reservation;
}
return postForm(postUrl, formData)
.then(json => {
if (json.errcode != 0) {
throw new Error(json.errmsg + ': ' + json.errcode)
}
})
},
editConfirmation(confirmation) {
this.selectedConfirmation = confirmation;
}
updatePropertyValue(name, value) {
runInAction(() => {
this[name] = value;
});
}
cityList = [];
updateReservationGuide: () => {
const { selectedReservation } = get()
const { travelAgencyId } = usingStorage()
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetCityGuide')
.append('VEI_SN', travelAgencyId)
.append('GRI_SN', selectedReservation.reservationId)
.append('LGC', 1)
.build()
selectedReservation = null;
selectedConfirmation = null;
arrivalDateRange = [];
referenceNo = '';
return fetchJSON(fetchUrl)
.then(json => {
if (json.errcode == 0) {
const reservationGuide = (json?.Result??[]).filter((data) => {
return data.TGI_SN != 0
}).map((data) => {
return data.GuideName
}).join(',')
reservationList = [];
set((state) => ({
selectedReservation: {
...state.selectedReservation,
guide: reservationGuide,
},
}))
reservationDetail = {
referenceNumber: '', arrivalDate: '', tourGuide: ''
};
return reservationGuide
} else {
throw new Error(json.errmsg + ': ' + json.errcode)
}
})
reservationPage = {
current: 1,
size: 10,
total: 0
}
}), { name: 'reservationStore' }))
export default useReservationStore
confirmationList = [
];
}
export default Reservation;

@ -0,0 +1,49 @@
import { makeAutoObservable } from "mobx";
import Reservation from "./Reservation";
import Feedback from "./Feedback";
import Notice from "./Notice";
import Auth from "./Auth";
import Invoice from "./Invoice";
import Report from "./Report";
class Root {
constructor() {
this.reservationStore = new Reservation(this);
this.feedbackStore = new Feedback(this);
this.noticeStore = new Notice(this);
this.authStore = new Auth(this);
this.invoiceStore = new Invoice(this);
this.reportStore = new Report(this);
makeAutoObservable(this);
}
clearSession() {
if (window.sessionStorage) {
const sessionStorage = window.sessionStorage;
sessionStorage.clear();
} else {
console.error('browser not support sessionStorage!');
}
}
getSession(key) {
if (window.sessionStorage) {
const sessionStorage = window.sessionStorage;
return sessionStorage.getItem(key);
} else {
console.error('browser not support sessionStorage!');
return null;
}
}
putSession(key, value) {
if (window.sessionStorage) {
const sessionStorage = window.sessionStorage;
return sessionStorage.setItem(key, value);
} else {
console.error('browser not support sessionStorage!');
}
}
}
export default Root;

@ -0,0 +1,7 @@
import { createContext, useContext } from "react";
export const StoreContext = createContext();
export function useStore() {
return useContext(StoreContext);
}

@ -1,7 +0,0 @@
import { createContext, useContext } from 'react'
export const ThemeContext = createContext({})
export function useThemeContext() {
return useContext(ThemeContext)
}

@ -1,209 +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,
};
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) {
const searchParams = {
CLF_SN: CLF_SN,
OPI_SN: OPI_SN,
LMI_SN: LMI_SN,
ServiceType: 2,
FlightMemo_messages: FlightMemo_messages,
};
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();
@ -114,523 +129,26 @@ export function escape2Html(str) {
var output = temp.innerText || temp.textContent;
temp = null;
return output;
}
export function formatPrice(price) {
return Math.ceil(price).toLocaleString();
}
export function formatPercent(number) {
return Math.round(number * 100) + "%";
}
/**
* ! 不支持计算 Set Map
* @param {*} val
* @example
* true if: 0, [], {}, null, '', undefined
* false if: 'false', 'undefined'
*/
export function isEmpty(val) {
// return val === undefined || val === null || val === "";
return [Object, Array].includes((val || {}).constructor) && !Object.entries(val || {}).length;
}
/**
* 数组排序
*/
export const sortBy = key => {
return (a, b) => (getNestedValue(a, key) > getNestedValue(b, key) ? 1 : getNestedValue(b, key) > getNestedValue(a, key) ? -1 : 0);
};
/**
* Object排序keys
*/
export const sortKeys = obj =>
Object.keys(obj)
.sort()
.reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {});
/**
* 数组排序, 给定排序数组
* @param {array} items 需要排序的数组
* @param {array} keyName 排序的key
* @param {array} keyOrder 给定排序
* @returns
*/
export const sortArrayByOrder = (items, keyName, keyOrder) => {
return items.sort((a, b) => {
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, 递归地
*/
export function merge(...objects) {
const isDeep = objects.some(obj => obj !== null && typeof obj === "object");
const result = objects[0] || (isDeep ? {} : objects[0]);
for (let i = 1; i < objects.length; i++) {
const obj = objects[i];
if (!obj) continue;
Object.keys(obj).forEach(key => {
const val = obj[key];
if (isDeep) {
if (Array.isArray(val)) {
result[key] = [].concat(Array.isArray(result[key]) ? result[key] : [result[key]], val);
} else if (typeof val === "object") {
result[key] = merge(result[key], val);
} else {
result[key] = val;
}
} else {
result[key] = typeof val === "boolean" ? val : result[key];
}
});
}
return result;
}
/**
* 数组分组
* - 相当于 lodash _.groupBy
* @see https://www.lodashjs.com/docs/lodash.groupBy#_groupbycollection-iteratee_identity
*/
export function groupBy(array = [], callback) {
return array.reduce((groups, item) => {
const key = typeof callback === "function" ? callback(item) : item[callback];
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
}
/**
* 创建一个从 object 中选中的属性的对象
* @param {*} object
* @param {array} keys
*/
export function pick(object, keys) {
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
obj[key] = object[key];
}
return obj;
}, {});
}
/**
* 返回对象的副本经过筛选以省略指定的键
* @param {*} object
* @param {string[]} keysToOmit
* @returns
*/
export function omit(object, keysToOmit) {
return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key)));
}
/**
* 深拷贝
*/
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);
}
export function groupBy(array, callback) {
return array.reduce((groups, item) => {
const key = typeof callback === 'function' ? callback(item) : item[callback];
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
// 处理普通属性
result[key] = cloneDeep(value[key], visited);
if (!groups[key]) {
groups[key] = [];
}
}
return result;
groups[key].push(item);
return groups;
}, {});
}
/**
* 向零四舍五入, 固定精度设置
*/
function curriedFix(precision = 0) {
return function (number) {
// Shift number by precision places
const shift = Math.pow(10, precision);
const shiftedNumber = number * shift;
// Round to nearest integer
const roundedNumber = Math.round(shiftedNumber);
// Shift back decimal place
return roundedNumber / shift;
};
}
/**
* 向零四舍五入, 保留2位小数
*/
export const fixTo2Decimals = curriedFix(2);
/**
* 向零四舍五入, 保留4位小数
*/
export const fixTo4Decimals = curriedFix(4);
export const fixTo1Decimals = curriedFix(1);
export const fixToInt = curriedFix(0);
/**
* 映射
* @example
* const keyMap = {
a: [{key: 'a1'}, {key: 'a2', transform: v => v * 2}],
b: {key: 'b1'}
};
const result = objectMapper({a: 1, b: 3}, keyMap);
// result = {a1: 1, a2: 2, b1: 3}
*
*/
export function objectMapper(input, keyMap) {
// Loop through array mapping
if (Array.isArray(input)) {
return input.map(obj => objectMapper(obj, keyMap));
}
if (typeof input === "object") {
const mappedObj = {};
Object.keys(input).forEach(key => {
// Keep original keys not in keyMap
if (!keyMap[key]) {
mappedObj[key] = input[key];
}
// Handle array of maps
if (Array.isArray(keyMap[key])) {
keyMap[key].forEach(map => {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key] = value;
});
// Handle single map
} else {
const map = keyMap[key];
if (map) {
let value = input[key];
if (map.transform) value = map.transform(value);
mappedObj[map.key || map] = value;
}
}
});
return mappedObj;
}
return input;
export function formatPrice(price) {
return Math.ceil(price).toLocaleString();
}
/**
* 创建一个对应于对象路径的值数组
*/
export function at(obj, path) {
let result;
if (Array.isArray(obj)) {
// array case
const indexes = path.split(".").map(i => parseInt(i));
result = [];
for (let i = 0; i < indexes.length; i++) {
result.push(obj[indexes[i]]);
}
} else {
// object case
const indexes = path.split(".").map(i => i);
result = [obj];
for (let i = 0; i < indexes.length; i++) {
result = [result[0]?.[indexes[i]] || undefined];
}
}
return result;
}
/**
* 删除 null/undefined
*/
export function flush(collection) {
let result, len, i;
if (!collection) {
return undefined;
}
if (Array.isArray(collection)) {
result = [];
len = collection.length;
for (i = 0; i < len; i++) {
const elem = collection[i];
if (elem != null) {
result.push(elem);
}
}
return result;
}
if (typeof collection === "object") {
result = {};
const keys = Object.keys(collection);
len = keys.length;
for (i = 0; i < len; i++) {
const key = keys[i];
const value = collection[key];
if (value != null) {
result[key] = value;
}
}
return result;
}
return undefined;
export function formatPercent(number) {
return Math.round(number * 100) + "%";
}
/**
* 千分位 格式化数字
*/
export const numberFormatter = number => {
return new Intl.NumberFormat().format(number);
};
/**
* @example
* const obj = { a: { b: 'c' } };
* const keyArr = ['a', 'b'];
* getNestedValue(obj, keyArr); // Returns: 'c'
*/
export const getNestedValue = (obj, keyArr) => {
return keyArr.reduce((acc, curr) => {
return acc && Object.prototype.hasOwnProperty.call(acc, curr) ? acc[curr] : undefined;
// return acc && acc[curr];
}, obj);
};
/**
* 计算笛卡尔积
*/
export const cartesianProductArray = (arr, sep = "_", index = 0, prefix = "") => {
let result = [];
if (index === arr.length) {
return [prefix];
}
arr[index].forEach(item => {
result = result.concat(cartesianProductArray(arr, sep, index + 1, prefix ? `${prefix}${sep}${item}` : `${item}`));
});
return result;
};
export const stringToColour = str => {
var hash = 0;
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash);
}
var colour = "#";
for (let i = 0; i < 3; i++) {
var value = (hash >> (i * 8)) & 0xff;
value = (value % 150) + 50;
colour += ("00" + value.toString(16)).substr(-2);
}
return colour;
};
export const debounce = (func, wait, immediate) => {
var timeout;
return function () {
var context = this,
args = arguments;
clearTimeout(timeout);
if (immediate && !timeout) func.apply(context, args);
timeout = setTimeout(function () {
timeout = null;
if (!immediate) func.apply(context, args);
}, wait);
};
};
export const removeFormattingChars = str => {
const regex = /[\r\n\t\v\f]/g;
str = str.replace(regex, " ");
// Replace more than four consecutive spaces with a single space
str = str.replace(/\s{4,}/g, " ");
return str;
};
export const olog = (text, ...args) => {
console.log(`%c ${text} `, "background:#fb923c ; padding: 1px; border-radius: 3px; color: #fff", ...args);
};
export const sanitizeFilename = str => {
// Remove whitespace and replace with hyphens
str = str.replace(/\s+/g, "-");
// Remove invalid characters and replace with hyphens
str = str.replace(/[^a-zA-Z0-9.-]/g, "-");
// Replace consecutive hyphens with a single hyphen
str = str.replace(/-+/g, "-");
// Trim leading and trailing hyphens
str = str.replace(/^-+|-+$/g, "");
return str;
};
export const formatBytes = (bytes, decimals = 2) => {
if (bytes === 0) return "";
const k = 1024;
const dm = decimals < 0 ? 0 : decimals;
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + " " + sizes[i];
};
export const calcCacheSizes = async () => {
try {
let swCacheSize = 0;
let diskCacheSize = 0;
let indexedDBSize = 0;
// 1. Get the service worker cache size
if ("caches" in window) {
const cacheNames = await caches.keys();
for (const name of cacheNames) {
const cache = await caches.open(name);
const requests = await cache.keys();
for (const request of requests) {
const response = await cache.match(request);
swCacheSize += Number(response.headers.get("Content-Length")) || 0;
}
}
}
// 2. Get the disk cache size
// const diskCacheName = 'disk-cache';
// const diskCache = await caches.open(diskCacheName);
// const diskCacheKeys = await diskCache.keys();
// for (const request of diskCacheKeys) {
// const response = await diskCache.match(request);
// diskCacheSize += Number(response.headers.get('Content-Length')) || 0;
// }
// 3. Get the IndexedDB cache size
// const indexedDBNames = await window.indexedDB.databases();
// for (const dbName of indexedDBNames) {
// const db = await window.indexedDB.open(dbName.name);
// const objectStoreNames = db.objectStoreNames;
// if (objectStoreNames !== undefined) {
// const objectStores = Array.from(objectStoreNames).map((storeName) => db.transaction([storeName], 'readonly').objectStore(storeName));
// for (const objectStore of objectStores) {
// const request = objectStore.count();
// request.onsuccess = () => {
// indexedDBSize += request.result;
// };
// }
// }
// }
return { swCacheSize, diskCacheSize, indexedDBSize, totalSize: Number(swCacheSize) + Number(diskCacheSize) + indexedDBSize };
} catch (error) {
console.error("Error getting cache sizes:", error);
}
};
export const clearAllCaches = async cb => {
try {
// 1. Clear the service worker cache
if ("caches" in window) {
// if (navigator.serviceWorker) {
const cacheNames = await caches.keys();
await Promise.all(cacheNames.map(name => caches.delete(name)));
}
// 2. Clear the disk cache (HTTP cache)
// const diskCacheName = 'disk-cache';
// await window.caches.delete(diskCacheName);
// const diskCache = await window.caches.open(diskCacheName);
// const diskCacheKeys = await diskCache.keys();
// await Promise.all(diskCacheKeys.map((request) => diskCache.delete(request)));
// 3. Clear the IndexedDB cache
const indexedDBNames = await window.indexedDB.databases();
await Promise.all(indexedDBNames.map(dbName => window.indexedDB.deleteDatabase(dbName.name)));
// Unregister the service worker
const registration = await navigator.serviceWorker.getRegistration();
if (registration) {
await registration.unregister();
console.log("Service worker unregistered");
} else {
console.log("No service worker registered");
}
if (typeof cb === "function") {
cb();
}
} catch (error) {
console.error("Error clearing caches or unregistering service worker:", error);
}
};
export const loadScript = src => {
return new Promise((resolve, reject) => {
const script = document.createElement("script");
script.type = "text/javascript";
script.onload = resolve;
script.onerror = reject;
script.crossOrigin = "anonymous";
script.src = src;
if (document.head.append) {
document.head.append(script);
} else {
document.getElementsByTagName("head")[0].appendChild(script);
}
});
};
//格式化为冒号时间2010转为20:10
export const formatColonTime = text => {
const hours = text.substring(0, 2);
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)
)

@ -1,45 +0,0 @@
const initListener = []
const authListener = []
export const addInitLinstener = (fn) => {
initListener.push(fn)
}
export const addAuthLinstener = (fn) => {
authListener.push(fn)
}
export const notifyInit = async () => {
for (const listener of initListener) {
await listener()
}
}
export const notifyAuth = async (obj) => {
for (const listener of authListener) {
await listener(obj)
}
}
// Zustand 中间件,用于订阅前端应用的生命周期,实验阶段。
// 失败,无法同步调用异步方法!
export const lifecycleware = (fn) => (set, get, store) => {
addInitLinstener(() => {
if (store.getState().hasOwnProperty('onInit')) {
store.getState().onInit()
} else {
console.info('store has no function: onInit.')
}
})
addAuthLinstener(() => {
if (store.getState().hasOwnProperty('onAuth')) {
store.getState().onAuth()
} else {
console.info('store has no function: onAuth.')
}
})
return fn(set, get, store)
}

@ -1,154 +1,69 @@
import { BUILD_VERSION } from '@/config'
const customHeaders = []
// 添加 HTTP Reuqest 自定义头部
export function appendRequestHeader(n, v) {
customHeaders.push({
name: n,
value: v
})
}
function getRequestHeader() {
return customHeaders.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {});
}
const initParams = [];
export function appendRequestParams(n, v) {
initParams.push({
name: n,
value: v
})
}
function getRequestInitParams() {
return initParams.reduce((acc, item) => {
acc[item.name] = item.value;
return acc;
}, {});
}
function checkStatus(response) {
if (response.status >= 200 && response.status < 300) {
return response
return response;
} else {
const message =
'Fetch error: ' + response.url + ' ' + response.status + ' (' +
response.statusText + ')'
const error = new Error(message)
error.response = response
throw error
}
}
function checkBizCode(responseJson) {
if (responseJson.errcode === 0) {
return responseJson;
} else {
throw new Error(responseJson.errmsg + ': ' + responseJson.errcode);
response.statusText + ')';
const error = new Error(message);
error.response = response;
throw error;
}
}
export function fetchText(url) {
const headerObj = getRequestHeader()
return fetch(url, {
method: 'GET',
headers: {
'X-Web-Version': BUILD_VERSION,
...headerObj
}
}).then(checkStatus)
return fetch(url)
.then(checkStatus)
.then(response => response.text())
.catch(error => {
throw error
})
throw error;
});
}
export function fetchJSON(url, data = {}) {
const initParams = getRequestInitParams();
const params4get = Object.assign({}, initParams, data);
const params = params4get ? new URLSearchParams(params4get).toString() : '';
const ifp = url.includes('?') ? '&' : '?';
const headerObj = getRequestHeader();
const fUrl = params !== '' ? `${url}${ifp}${params}` : url;
return fetch(fUrl, {
method: 'GET',
headers: {
'X-Web-Version': BUILD_VERSION,
...headerObj
}
}).then(checkStatus)
export function fetchJSON(url) {
return fetch(url)
.then(checkStatus)
.then(response => response.json())
.then(checkBizCode)
.catch(error => {
throw error;
});
}
export function postForm(url, data) {
const initParams = getRequestInitParams();
Object.keys(initParams).forEach(key => {
if (! data.has(key)) {
data.append(key, initParams[key]);
}
});
const headerObj = getRequestHeader()
return fetch(url, {
method: 'POST',
body: data,
headers: {
'X-Web-Version': BUILD_VERSION,
...headerObj
}
body: data
}).then(checkStatus)
.then(response => response.json())
.then(checkBizCode)
.catch(error => {
throw error
})
throw error;
});
}
export function postJSON(url, obj) {
const initParams = getRequestInitParams();
const params4get = Object.assign({}, initParams);
const params = new URLSearchParams(params4get).toString();
const ifp = url.includes('?') ? '&' : '?';
const fUrl = params !== '' ? `${url}${ifp}${params}` : url;
const headerObj = getRequestHeader()
return fetch(fUrl, {
return fetch(url, {
method: 'POST',
body: JSON.stringify(obj),
headers: {
'Content-type': 'application/json; charset=UTF-8',
'X-Web-Version': BUILD_VERSION,
...headerObj
'Content-type': 'application/json; charset=UTF-8'
}
}).then(checkStatus)
.then(response => response.json())
.then(checkBizCode)
.catch(error => {
throw error
})
throw error;
});
}
export function postStream(url, obj) {
const headerObj = getRequestHeader()
return fetch(url, {
method: 'POST',
body: JSON.stringify(obj),
headers: {
'Content-type': 'application/octet-stream',
'X-Web-Version': BUILD_VERSION,
...headerObj
'Content-type': 'application/octet-stream'
}
}).then(checkStatus)
.then(response => response.json())
.catch(error => {
throw error
})
throw error;
});
}

@ -1,173 +1,229 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, 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 } 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 { BugOutlined } from "@ant-design/icons"
import ErrorBoundary from '@/components/ErrorBoundary'
import { BUILD_VERSION, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT } from '@/config'
import useNoticeStore from '@/stores/Notice'
import useAuthStore from '@/stores/Auth'
import { useThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import { useDefaultLgc } from '@/i18n/LanguageSwitcher'
import { appendRequestParams } from '@/utils/request'
import { uploadPageSpyLog } from '@/pageSpy';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET } from '@/config'
const { Header, Content, Footer } = Layout
import { Outlet, Link, useHref, useNavigate, useLocation, NavLink } from "react-router-dom";
import { useEffect, useState } from 'react';
import { observer } from "mobx-react";
import { toJS } from "mobx";
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 } from "@/utils/commons";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
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';
const { Header, Content, Footer } = Layout;
const { Title } = Typography;
let items = [];
const useItemDefault = () => {
const [data, setData] = useState([]);
const { t, i18n } = useTranslation();
function App() {
const { t, i18n } = useTranslation()
const { colorPrimary } = useThemeContext()
const [isPermitted, currentUser] = useAuthStore(
(state) => [state.isPermitted, state.currentUser])
const { loginToken } = usingStorage()
useEffect(() => {
const newData = [
{ label: <Link to='/account/change-password'>{t('ChangePassword')}</Link>, key: '0' },
{ label: <Link to='/account/profile'>{t('Profile')}</Link>, key: '1' },
{ type: 'divider' },
{ label: <Link to='/login?out'>{t('Logout')}</Link>, key: '3' },
];
setData(newData);
}, [i18n.language]);
return data;
};
const useItemManager = () => {
const items_default = useItemDefault();
const [data, setData] = useState(items_default);
const { t, i18n } = useTranslation();
const [messageApi, contextHolder] = message.useMessage()
useEffect(() => {
const newData = [
{ label: <Link to='/account/change-password'>{t('ChangePassword')}</Link>, key: '0' },
{ label: <Link to='/account/profile'>{t('Profile')}</Link>, key: '1' },
{ type: 'divider' },
{ label: <Link to='/login?out'>{t('Logout')}</Link>, key: '3' },
{ label: <Link to='/account/change-vendor'>{t('ChangeVendor')}</Link>, key: '4' },
];
setData(newData);
}, [i18n.language]);
return data;
};
const noticeUnRead = useNoticeStore((state) => state.noticeUnRead)
const href = useHref()
const navigate = useNavigate()
function App() {
const { t, i18n } = useTranslation();
const items_default = useItemDefault();
const item_manager = useItemManager();
const [password, setPassword] = useState('');
const { authStore, noticeStore } = useStore();
const { notification } = AntApp.useApp();
const login = toJS(authStore.login);
const { noticeUnRead } = noticeStore;
const href = useHref();
const loginToken = login.token;
const navigate = useNavigate();
const location = useLocation();
const arrManager = config.arrManager; // ["testzp","testzac","testycc","testlyj","testqqs","testjjh","testhn"];// zp
if (arrManager.includes(authStore.login.username)){
items = item_manager;
}else{
items = items_default;
}
const needToLogin = href !== '/login' && isEmpty(loginToken)
useEffect(() => {
if (needToLogin) {
navigate('/login')
if (href !== '/login' && isEmpty(loginToken)) {
navigate('/login');
}
}, [href])
}, [href]);
const splitPath = href.split('/')
let defaultPath = 'notice'
useEffect(() => {
window.gtag('event', 'page_view', { page_location: window.location.href });
}, [location]);
const onSubmit = () => {
authStore.valdateUserPassword(login.username, password)
.then(() => {
authStore.fetchUserDetail()
.catch(ex => {
notification.error({
message: `Notification`,
description: 'Failed to get user information.',
placement: 'top',
duration: 4,
});
});
})
.catch(ex => {
notification.error({
message: `Notification`,
description: ex.message,
placement: 'top',
duration: 4,
});
});
setPassword('');
};
const splitPath = href.split("/");
let defaultPath = "reservation";
if (splitPath.length > 1) {
defaultPath = splitPath[1]
defaultPath = splitPath[1];
}
const { language } = useDefaultLgc()
const [antdLng, setAntdLng] = useState(enLocale)
useEffect(() => {
setAntdLng(i18n.language === 'en' ? enLocale : zhLocale)
appendRequestParams('lgc', language)
}, [i18n.language])
const uploadLog = () => {
if (window.$pageSpy) {
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload')
messageApi.info('Success')
} else {
messageApi.error('Failure')
}
}
//
const isProductPermitted = isPermitted(PERM_PRODUCTS_MANAGEMENT) || isPermitted(PERM_PRODUCTS_INFO_PUT)
const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/edit'
const {
token: { colorBgContainer },
} = theme.useToken();
const [antdLng, setAntdLng] = useState(enLocale);
useEffect(() => {
setAntdLng(i18n.language === 'en' ? enLocale : zhLocale);
}, [i18n.language]);
return (
<ConfigProvider locale={antdLng}
theme={{
token: {
colorPrimary: colorPrimary,
// "sizeStep": 3,
// "sizeUnit": 3,
colorPrimary: "#00b96b",
},
algorithm: theme.defaultAlgorithm,
}}>
<AntApp>
<FloatButton.Group
shape='square'
style={{
insetInlineEnd: 94,
}}
<Modal
centered
closable={false}
maskClosable={false}
footer={null}
open={login.timeout}
>
<FloatButton icon={<BugOutlined />} onClick={() => uploadPageSpyLog()} />
<FloatButton.BackTop />
</FloatButton.Group>
{contextHolder}
<ErrorBoundary>
<Layout className='min-h-screen h-dvh'>
<Header className='sticky top-0 z-10 w-full'>
<Row gutter={{ md: 24 }} justify='end' align='middle'>
<Col span={15}>
<NavLink to='/'>
<img src={AppLogo} className='float-left h-9 my-4 mr-6 ml-0 bg-white/30' alt='App logo' />
<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={login.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={16}>
<NavLink to="/">
<img src={AppLogo} className="logo" alt="App logo" />
</NavLink>
<Menu
theme='dark'
mode='horizontal'
theme="dark"
mode="horizontal"
selectedKeys={[defaultPath]}
items={[
isPermitted(PERM_OVERSEA) ? { key: 'reservation', label: <Link to='/reservation/newest'>{t('menu.Reservation')}</Link> } : null,
isPermitted(PERM_OVERSEA) ? { key: 'invoice', label: <Link to='/invoice'>{t('menu.Invoice')}</Link> } : null,
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: "reservation", label: <Link to="/reservation/newest">{t('menu.Reservation')}</Link> },
{ key: "invoice", label: <Link to="/invoice">{t('menu.Invoice')}</Link> },
{ key: "feedback", label: <Link to="/feedback">{t('menu.Feedback')}</Link> },
// { key: "report", label: <Link to="/report">Report</Link> },
{
key: 'notice',
key: "notice",
label: (
<Link to='/notice'>
<Link to="/notice">
{t('menu.Notice')}
{noticeUnRead ? <Badge dot /> : ''}
{noticeUnRead ? <Badge dot /> : ""}
</Link>
),
},
]}
/>
</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" }}>
{authStore.login.travelAgencyName}
</Title>
</Col>
<Col span={2}>
<Dropdown
menu={{
items: [...[
{ label: <Link to='/account/change-password'>{t('ChangePassword')}</Link>, key: '0' },
{ label: <Link to='/account/profile'>{t('Profile')}</Link>, key: '1' },
isPermitted(PERM_ACCOUNT_MANAGEMENT) ? { label: <Link to='/account/management'>{t('account:accountList')}</Link>, key: '3' } : null,
isPermitted(PERM_ROLE_NEW) ? { label: <Link to='/account/role-list'>{t('account:roleList')}</Link>, key: '4' } : null,
{ type: 'divider' },
{ label: <Link to='/logout'>{t('Logout')}</Link>, key: '99' },
]
],
items,
}}
trigger={['click']}
>
<a onClick={e => e.preventDefault()}>
<Space>
<div className='line-clamp-1'>{currentUser?.realname}</div>
{authStore.login.username}
<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'>
{needToLogin ? <>login...</> : <Outlet />}
<Content
style={{
padding: 24,
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}>
<Outlet />
</Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
<Footer></Footer>
</Layout>
</ErrorBoundary>
</AntApp>
</ConfigProvider>
)
);
}
export default App
export default observer(App);

@ -0,0 +1,13 @@
export default function Index() {
return (
<p id="zero-state">
Global Highlights Hub
<br />
Check out{" "}
<a href="https://www.chinahighlights.com">
the docs at chinahighlights.com
</a>
.
</p>
);
}

@ -1,109 +1,120 @@
import { useNavigate } from 'react-router-dom'
import { useEffect } from 'react'
import { Button, Form, Input, Row, Radio, App } 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 { useStore } from '@/stores/StoreContext.js';
import { useTranslation } from 'react-i18next';
function Login() {
const [authenticate, loginStatus, defaultRoute] =
useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute])
const { t, i18n } = useTranslation();
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)
const { authStore, noticeStore } = useStore();
const { notification } = App.useApp();
const navigate = useNavigate();
const location = useLocation();
const [form] = Form.useForm();
useEffect (() => {
if (loginStatus === 302) {
navigate(defaultRoute)
if (location.search === '?out') {
authStore.logout();
navigate('/login');
}
}, [loginStatus])
return () => {
// unmount...
};
}, []);
const onFinish = (values) => {
authenticate(values.username, values.password)
authStore.valdateUserPassword(values.username, values.password)
.then((userId) => {
noticeStore.getBulletinUnReadCount(userId);
authStore.fetchUserDetail()
.then((user) => {
// navigate(-1) is equivalent to hitting the back button.
navigate("/reservation/newest");
})
.catch(ex => {
notification.error({
message: `Notification`,
description: 'Failed to get user information.',
placement: 'top',
duration: 4,
});
});
})
.catch(ex => {
console.error(ex)
notification.error({
message: t('Validation.Title'),
description: t('Validation.LoginFailed'),
message: `Notification`,
description: 'Login failed. Incorrect username or password.',
placement: 'top',
duration: 4,
})
})
}
});
});
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo);
}
};
return (
<Row justify='center' align='middle' className='min-h-96'>
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
<Form
name='login'
layout='vertical'
name="basic"
// layout="vertical"
form={form}
size='large'
size="large"
labelCol={{
span: 8,
}}
wrapperCol={{
span: 24,
span: 16,
}}
style={{
maxWidth: 600,
}}
className='max-w-xl'
initialValues={{
language: defaultLng,
remember: true,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete='off'
autoComplete="off"
>
<Form.Item
label={t('Username')}
name='username'
label={t("Username")}
name="username"
rules={[
{
required: true,
message: t('Validation.UsernameIsEmpty'),
message: 'Please input your username!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('Password')}
name='password'
label={t("Password")}
name="password"
rules={[
{
required: true,
message: t('Validation.PasswordIsEmpty'),
message: 'Please input your password!',
},
]}
>
<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'>
<Form.Item
wrapperCol={{
offset: 8,
span: 16,
}}
>
<Button type="primary" htmlType="submit" style={{width: "100%"}}>
{t('Login')}
</Button>
</Form.Item>
</Form>
</Row>
)
);
}
export default Login
export default Login;

@ -1,31 +0,0 @@
import { Flex, Result, Spin } from 'antd'
import { useEffect } from 'react'
import { useNavigate } from 'react-router-dom'
import useAuthStore from '@/stores/Auth'
function Logout() {
const navigate = useNavigate()
const logout = useAuthStore(state => state.logout)
useEffect(() => {
logout()
navigate('/login')
}, [])
return (
<Flex justify='center' align='center' gap='middle' vertical>
<Result
status='success'
title='退出成功'
subTitle='正在跳转登陆页面'
extra={[
<Spin key='small-span' size='small' />
]}
/>
</Flex>
)
}
export default Logout

@ -1,37 +1,61 @@
import { Outlet } from "react-router-dom";
import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from "antd";
import { Outlet, Link, useHref, useLocation } from "react-router-dom";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { Layout, Menu, ConfigProvider, theme, Typography, Space, Row, Col, Alert, App as AntApp } from "antd";
import { DownOutlined } from "@ant-design/icons";
import "antd/dist/reset.css";
import AppLogo from "@/assets/logo-gh.png";
import { useThemeContext } from "@/stores/ThemeContext";
import { BUILD_VERSION } from "@/config";
import { useStore } from "@/stores/StoreContext.js";
import Language from "../i18n/LanguageSwitcher";
const { Title } = Typography;
const { Header, Content, Footer } = Layout;
function Standlone() {
const { colorPrimary } = useThemeContext();
const { authStore } = useStore();
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">
<img src={AppLogo} className="float-left h-9 my-4 mr-6 ml-0 bg-white/30" alt="App logo" />
<p className="text-white text-center">Global Highlights Hub</p>
<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}</Footer>
<Footer></Footer>
</Layout>
</AntApp>
</ConfigProvider>
);
}
export default Standlone;
export default observer(Standlone);

@ -1,89 +1,88 @@
import { useNavigate } from "react-router-dom"
import { Button, Space, Form, Input, Row, Typography, App } from 'antd'
import { useTranslation } from 'react-i18next'
import useAuthStore from '@/stores/Auth'
import { useNavigate } from "react-router-dom";
import { Button, Space, Form, Input, Row, Typography, App } from 'antd';
import { useStore } from '@/stores/StoreContext.js';
const { Title } = Typography
const { Title } = Typography;
function ChangePassword() {
const { t } = useTranslation()
const navigate = useNavigate()
const changeUserPassword = useAuthStore((state) => state.changeUserPassword)
const { notification } = App.useApp()
const [form] = Form.useForm()
const navigate = useNavigate();
const { authStore } = useStore();
const { notification } = App.useApp();
const [form] = Form.useForm();
const onFinish = (values) => {
changeUserPassword(values.currentPassword, values.newPassword)
authStore.changeUserPassword(values.currentPassword, values.newPassword)
.then(() => {
notification.success({
message: t('Validation.Title'),
description: t('account:Validation.Success'),
message: `Notification`,
description: 'Your password has been successfully updated.',
placement: 'top',
duration: 4,
})
form.resetFields()
});
})
.catch(() => {
notification.error({
message: t('Validation.Title'),
description: t('account:Validation.Fail'),
message: `Notification`,
description: 'Failed to change password. Please try again.',
placement: 'top',
duration: 4,
})
})
}
});
});
};
const onFinishFailed = (errorInfo) => {
console.log('Failed:', errorInfo)
// form.resetFields()
}
console.log('Failed:', errorInfo);
// form.resetFields();
};
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"
>
<Form.Item><Title level={2}>{t('ChangePassword')}</Title></Form.Item>
<Form.Item><Title level={2}>Change your password</Title></Form.Item>
<Form.Item
label={t('account:CurrentPassword')}
label="Current password"
name="currentPassword"
rules={[
{
required: true,
message: t('account:Validation.CurrentPassword'),
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item
label={t('account:NewPassword')}
label="New password"
name="newPassword"
rules={[
{
required: true,
message: t('account:Validation.NewPassword'),
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item
label={t('account:ReenterPassword')}
label="Reenter password"
name="reenterPassword"
rules={[
{
required: true,
message: t('account:Validation.ReenterPassword'),
message: 'Please reenter your password!',
},
]}
>
@ -92,17 +91,17 @@ function ChangePassword() {
<Form.Item>
<Space size="middle">
<Button type="primary" htmlType="submit">
{t('Submit')}
Save
</Button>
<Button onClick={() => navigate('/reservation/newest')}>
{t('Cancel')}
Cancel
</Button>
</Space>
</Form.Item>
</Form>
</Row>
</>
)
);
}
export default ChangePassword
export default ChangePassword;

@ -0,0 +1,113 @@
import { useNavigate } from "react-router-dom";
import { useEffect, useState, useRef } from "react";
import { observer } from "mobx-react";
import { Button, Space, Form, Input, Row, Typography, App,Select } from "antd";
import { useStore } from "@/stores/StoreContext.js";
const { Title } = Typography;
function ChangeVendor() {
const navigate = useNavigate();
const { authStore,VendorList } = useStore();
const { notification } = App.useApp();
const [form] = Form.useForm();
const { formVeiSn, onVeiSnChange } = useState();
useEffect(() => {
authStore.fetchVendorList();
},[]);
//
function bindVendor() {
let arr=[];
arr = authStore.VendorList.map((data,index) =>{
return {
value: data.VEI_SN,
label: data.VEI2_CompanyBN,
}
})
return arr;
}
const onFinish = (values) => {
if (values.VEISN == authStore.login.travelAgencyId){
notification.error({
message: `Notification`,
description: "切换的供应商是当前供应商.",
placement: "top",
duration: 4,
});
return ;
}
// console.log(values);
// console.log(authStore.login.travelAgencyId);
authStore.changeVendor(values.VEISN)
.then(()=>{
authStore.logout();
})
.catch(()=>{
//console.log(json);
notification.error({
message: `Notification`,
description: "切换的供应商错误,请重试!",
placement: "top",
duration: 4,
});
})
};
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
// form.resetFields();
};
return (
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
<Form
name="basic"
form={form}
layout="vertical"
size="large"
style={{
maxWidth: 600,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item>
<Title level={2}>Change your password</Title>
</Form.Item>
<Form.Item
name="VEISN"
label="供应商"
rules={[
{
required: true,
message: "请选择需要切换的供应商!",
},
]}
>
<Select
placeholder="请选择"
onChange={onVeiSnChange}
options={bindVendor()}
></Select>
</Form.Item>
<Form.Item>
<Space size="middle">
<Button type="primary" htmlType="submit">
</Button>
</Space>
</Form.Item>
</Form>
</Row>
);
}
export default observer(ChangeVendor);

@ -1,373 +0,0 @@
import SearchForm from '@/components/SearchForm'
import useAccountStore, { fetchRoleList, fetchTravelAgencyByName, genRandomPassword } from '@/stores/Account'
import useFormStore from '@/stores/Form'
import { isEmpty, debounce } 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'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
function Management() {
const { t } = useTranslation()
const accountListColumns = [
{
title: t('account:username'),
dataIndex: 'username',
render: accountRender
},
{
title: t('account:realname'),
dataIndex: 'realname',
},
{
title: t('account:email'),
dataIndex: 'email',
},
{
title: t('account:travelAgency'),
dataIndex: 'travelAgencyName',
},
{
title: t('account:roleName'),
dataIndex: 'role'
},
{
title: t('account:lastLogin'),
dataIndex: 'lastLogin',
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss'))
},
{
title: t('account:action'),
dataIndex: 'account:action',
render: actionRender
},
]
function accountRender(text, account) {
return (
<Button type='link' onClick={() => onAccountSeleted(account)}>{text}</Button>
)
}
function actionRender(_, account) {
return (
<Space key='actionRenderSpace' size='middle'>
<Switch checkedChildren={t('account:action.enable')} unCheckedChildren={t('account:action.disable')} checked={account.disabled==0} onChange={(checked) => {
showDisableConfirm(account, checked)
}} />
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
</Space>
)
}
const [isAccountModalOpen, setAccountModalOpen] = useState(false)
const [dataLoading, setDataLoading] = useState(false)
const [roleAllList, setRoleAllList] = useState([])
const [travelAgencyList, setTravelAgencyList] = useState([])
const [currentTravelAgency, setCurrentTravelAgency] = useState(null)
const [accountForm] = Form.useForm()
const [searchAccountByCriteria, accountList, toggleAccountStatus, saveOrUpdateAccount, resetAccountPassword, newEmptyAccount] =
useAccountStore((state) =>
[state.searchAccountByCriteria, state.accountList, state.toggleAccountStatus, state.saveOrUpdateAccount, state.resetAccountPassword, state.newEmptyAccount])
const formValues = useFormStore(state => state.formValues)
const { notification, modal } = App.useApp()
useEffect(() => {
fetchRoleList()
.then((roleList) => {
const roleListMap = roleList.map(r => {
return {
value: r.role_id,
label: r.role_name,
disabled: r.role_id === 1
}
})
roleListMap.unshift({ value: 0, label: '未设置', disabled: true });
setRoleAllList(roleListMap)
})
}, [])
const handelAccountSearch = () => {
setDataLoading(true)
searchAccountByCriteria(formValues)
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
.finally(() => {
setDataLoading(false)
})
}
const onAccountSeleted = async (account) => {
setTravelAgencyList([{
label: account.travelAgencyName,
value: account.travelAgencyId
}])
accountForm.setFieldsValue(account)
setCurrentTravelAgency(account.travelAgencyId)
setAccountModalOpen(true)
}
const onNewAccount = () => {
const emptyAccount = newEmptyAccount()
accountForm.setFieldsValue(emptyAccount)
setAccountModalOpen(true)
}
const onAccountFinish = (values) => {
saveOrUpdateAccount(values)
.then(() => {
notification.info({
message: 'Notification',
description: '账号保存成功',
placement: 'top',
})
setAccountModalOpen(false)
handelAccountSearch()
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
}
const onAccountFailed = (error) => {
console.log('Failed:', error)
// form.resetFields()
}
const handleTravelAgencySearch = (newValue) => {
setDataLoading(true)
fetchTravelAgencyByName(newValue)
.then(result => {
setTravelAgencyList(result.map(r => {
return {
label: r.travel_agency_name,
value: r.travel_agency_id
}
}))
})
.finally(() => {
setDataLoading(false)
})
}
const handleTravelAgencyChange = (newValue) => {
setCurrentTravelAgency(newValue)
}
const showDisableConfirm = (account, status) => {
const confirmTitle = status ? t('account:action.enable.title') : t('account:action.disable.title')
modal.confirm({
title: confirmTitle,
icon: <ExclamationCircleFilled />,
content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
onOk() {
toggleAccountStatus(account.userId, status)
.then(() => {
handelAccountSearch()
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
},
onCancel() {
},
})
}
const showResetPasswordConfirm = (account) => {
const confirmTitle = t('account:action.resetPassword.tile')
const randomPassword = genRandomPassword()
modal.confirm({
title: confirmTitle,
icon: <ExclamationCircleFilled />,
content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
onOk() {
resetAccountPassword(account.userId, randomPassword)
.then(() => {
notification.info({
message: `请复制新密码给 [${account.realname}]`,
description: '新密码:' + randomPassword,
placement: 'top',
duration: 60,
})
})
},
onCancel() {
},
})
}
return (
<>
<Modal
centered
okButtonProps={{
autoFocus: true,
htmlType: 'submit',
}}
title={t('account:detail')}
open={isAccountModalOpen} onCancel={() => setAccountModalOpen(false)}
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
name='AccountForm'
form={accountForm}
layout='vertical'
size='large'
className='max-w-2xl'
onFinish={onAccountFinish}
onFinishFailed={onAccountFailed}
autoComplete='off'
>
{dom}
</Form>
)}
>
<Form.Item name='accountId' className='hidden' ><Input /></Form.Item>
<Form.Item name='userId' className='hidden' ><Input /></Form.Item>
<Form.Item name='lmi2_sn' className='hidden' ><Input /></Form.Item>
<Form.Item
label={t('account:username')}
name='username'
rules={[
{
required: true,
message: t('account:Validation.username'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('account:realname')}
name='realname'
rules={[
{
required: true,
message: t('account:Validation.realname'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('account:email')}
name='email'
rules={[
{
required: true,
message: t('account:Validation.email'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('account:travelAgency')}
name='travelAgencyId'
rules={[
{
required: true,
message: t('account:Validation.travelAgency'),
},
]}
>
<Select
options={travelAgencyList}
value={currentTravelAgency}
onChange={handleTravelAgencyChange}
loading={dataLoading}
showSearch
filterOption={false}
onSearch={debounce(handleTravelAgencySearch, 800)}
notFoundContent={null}
>
</Select>
</Form.Item>
<Form.Item
label={t('account:roleName')}
name='roleId'
rules={[
{
required: true,
message: t('account:Validation.role'),
},
]}
>
<Select
options={roleAllList}
filterOption={false}
notFoundContent={null}
>
</Select>
</Form.Item>
</Modal>
<Space direction='vertical' className='w-full'>
<Title level={3}>{t('account:accountList')}</Title>
<SearchForm
fieldsConfig={{
shows: ['username', 'agency'],
fieldProps: {
username: { label: t('account:username') + '/' + t('account:realname') },
agency: { label: t('account:travelAgency') },
},
sort: { username: 1, agency: 2},
}}
onSubmit={() => {
handelAccountSearch()
}}
/>
<Row>
<Col span={24}>
<Space>
<Button onClick={() => onNewAccount()}>{t('account:newAccount')}</Button>
</Space>
</Col>
</Row>
<Row>
<Col span={24}>
<Table
bordered
loading={dataLoading}
rowKey='username'
pagination={{
showQuickJumper: true,
showLessItems: true,
showSizeChanger: true,
showTotal: (total) => { return t('Total') + `${total}` }
}}
columns={accountListColumns} dataSource={accountList}
/>
</Col>
</Row>
</Space>
</>
)
}
export default Management

@ -1,24 +1,25 @@
import { Descriptions, Col, Row } from 'antd'
import { useTranslation } from 'react-i18next'
import useAuthStore from '@/stores/Auth'
import { Descriptions, Col, Row } from 'antd';
import { useStore } from '@/stores/StoreContext.js';
import { useTranslation } from 'react-i18next';
function Profile() {
const { t } = useTranslation();
const { t } = useTranslation()
const currentUser = useAuthStore(state => state.currentUser)
const { authStore } = useStore();
const { login } = authStore;
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")}>{login.username}</Descriptions.Item>
<Descriptions.Item label={t("Telephone")}>{login.telephone}</Descriptions.Item>
<Descriptions.Item label={t("Email")}>{login.emailAddress}</Descriptions.Item>
<Descriptions.Item label={t("Company")}>{login.travelAgencyName}</Descriptions.Item>
</Descriptions>
</Col>
</Row>
);
}
export default Profile
export default Profile;

@ -1,249 +0,0 @@
import RequireAuth from '@/components/RequireAuth'
import { PERM_ROLE_NEW } from '@/config'
import useAccountStore, { fetchPermissionList, fetchPermissionListByRoleId, fetchRoleList } from '@/stores/Account'
import { isEmpty } from '@/utils/commons'
import {
SyncOutlined,
} from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Space, Table, Tag, TreeSelect, Typography } from 'antd'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
function RoleList() {
const { t } = useTranslation()
const roleListColumns = [
{
title: t('account:roleName'),
dataIndex: 'role_name',
},
{
title: t('account:createdOn'),
dataIndex: 'created_on',
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss'))
},
{
title: t('account:action'),
dataIndex: 'account:action',
render: actionRender
},
]
function actionRender(_, role) {
if (role.role_id == 1) {
return (<Tag icon={<SyncOutlined spin />} color='warning'>不能修改</Tag>)
} else {
return (
<Button type='link' key='edit' onClick={() => onRoleSeleted(role)}>{t('account:action.edit')}</Button>
)
}
}
const onPermissionChange = (newValue) => {
setPermissionValue(newValue)
}
function groupByParam(array, param) {
return array.reduce((result, item) => {
(result[item[param]] = result[item[param]] || []).push(item)
return result
}, {})
}
useEffect(() => {
setDataLoading(true)
fetchRoleList()
.then(r => {
setRoleAllList(r)
})
.finally(() => {
setDataLoading(false)
})
const categoryMap = new Map([
['system', '系统管理'],
['oversea', '海外供应商'],
['domestic', '国内供应商'],
['air-ticket', '机票供应商'],
['train-ticket', '火车票供应商'],
['products', '产品价格'],
['page', '默认页面'],
]);
const permissionTree = []
fetchPermissionList()
.then(r => {
const groupPermissionData = groupByParam(r, 'res_category')
const categoryKeys = Object.keys(groupPermissionData)
categoryKeys.forEach((categoryName) => {
const permissisonList = groupPermissionData[categoryName]
const categoryGroup = {
title: categoryMap.get(categoryName),
value: categoryName,
key: categoryName,
children: permissisonList.map(p => {
return {
disableCheckbox: p.res_id == 1,
title: p.res_name,
value: p.res_id,
key: p.res_id,
}
})
}
permissionTree.push(categoryGroup)
})
setPermissionTreeData(permissionTree)
})
}, [])
const [permissionValue, setPermissionValue] = useState([])
const [permissionTreeData, setPermissionTreeData] = useState([])
const [isRoleModalOpen, setRoleModalOpen] = useState(false)
const [dataLoading, setDataLoading] = useState(false)
const [roleAllList, setRoleAllList] = useState([])
const [roleForm] = Form.useForm()
const [saveOrUpdateRole, newEmptyRole] =
useAccountStore((state) =>
[state.saveOrUpdateRole, state.newEmptyRole])
const { notification } = App.useApp()
const onRoleSeleted = (role) => {
fetchPermissionListByRoleId({ role_id: role.role_id })
.then(result => {
role.res_array = result.map(r => r.res_id)
roleForm.setFieldsValue(role)
})
setRoleModalOpen(true)
}
const onNewRole = () => {
const role = newEmptyRole()
roleForm.setFieldsValue(role)
setRoleModalOpen(true)
}
const onRoleFinish = (values) => {
saveOrUpdateRole(values)
.then(() => {
setRoleModalOpen(false)
fetchRoleList()
.then(r => {
setRoleAllList(r)
})
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
}
const onRoleFailed = (error) => {
console.log('Failed:', error)
// form.resetFields()
}
return (
<>
<Modal
centered
okButtonProps={{
autoFocus: true,
htmlType: 'submit',
}}
title={t('account:detail')}
open={isRoleModalOpen} onCancel={() => setRoleModalOpen(false)}
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
name='RoleForm'
form={roleForm}
layout='vertical'
size='large'
className='max-w-xl'
onFinish={onRoleFinish}
onFinishFailed={onRoleFailed}
autoComplete='off'
>
{dom}
</Form>
)}
>
<Form.Item name='role_id' className='hidden' ><Input /></Form.Item>
<Form.Item
label={t('account:roleName')}
name='role_name'
rules={[
{
required: true,
message: t('account:Validation.roleName'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t('account:permission')}
name='res_array'
>
<TreeSelect treeData={permissionTreeData} value={permissionValue}
popupClassName='max-w-xl overflow-auto'
placement='bottomLeft'
showSearch
allowClear
multiple
treeDefaultExpandAll
treeLine={true}
onChange={onPermissionChange}
treeCheckable={true}
showCheckedStrategy={TreeSelect.SHOW_CHILD}
placeholder={'Please select'}
className='w-full' />
</Form.Item>
</Modal>
<Space direction='vertical' className='w-full'>
<Title level={3}>{t('account:roleList')}</Title>
<Row>
<Col span={24}>
<Space>
<RequireAuth subject={PERM_ROLE_NEW}>
<Button onClick={() => onNewRole()}>{t('account:newRole')}</Button>
</RequireAuth>
</Space>
</Col>
</Row>
<Row>
<Col span={24}>
<Table
bordered
loading={dataLoading}
rowKey='role_id'
pagination={{
pageSize: 20,
showQuickJumper: true,
showLessItems: true,
showSizeChanger: true,
showTotal: (total) => { return t('Total') + `${total}` }
}}
columns={roleListColumns} dataSource={roleAllList}
/>
</Col>
</Row>
</Space>
</>
)
}
export default RoleList

@ -1,139 +0,0 @@
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 { 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";
const planListColumns = [
{
title: "团名",
key: "GRI_No",
dataIndex: "GRI_No",
// sorter: (a, b) => b.GRI_No - a.GRI_No,
},
{
title: "组团人",
key: "WL",
dataIndex: "WL",
},
{
title: "人数",
dataIndex: "PersonNum",
key: "PersonNum",
},
{
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",
},
{
title: "抵达城市",
key: "ToCity",
dataIndex: "ToCity",
},
{
title: "航班",
key: "FlightNo",
dataIndex: "FlightNo",
},
{
title: "起飞时间",
key: "FlightStart",
dataIndex: "FlightStart",
render: text => formatColonTime(text),
},
{
title: "落地时间",
key: "FlightEnd",
dataIndex: "FlightEnd",
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: "FlightInfo",
dataIndex: "FlightInfo",
render: (text, record) => <NavLink to={`/airticket/plan/${record.COLI_SN}/${record.GRI_SN}`}>{"编辑"}</NavLink>,
},
];
const Airticket = props => {
const navigate = useNavigate();
const { travelAgencyId } = usingStorage();
const [getPlanList, planList, loading] = airTicketStore(state => [state.getPlanList, state.planList, state.loading]);
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");
}, []);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<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>
</Space>
);
};
export default Airticket;

@ -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,722 +0,0 @@
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 { 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 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
),
},
];
const Airticket_form = props => {
const airInfo = props.airInfo;
const [airinfo_form] = Form.useForm();
return (
<>
<Form
form={airinfo_form}
name={"ticket_form_" + airInfo.id}
labelCol={{
span: 6,
}}
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 />,
});
});
}}
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>
<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"
onClick={() => {
ticketIssuedNotifications(userId,airInfo.CLF_SN, airInfo.OPI_SN,airinfo_form.getFieldValue('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 />,
});
});
}}>
3. 通知顾问
</Button>
</Col>
</Row>
</Form>
</>
);
};
const detail_items = () => {
return planDetail
? 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 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
useEffect(() => {
getPlanDetail(travelAgencyId, gri_sn); //
getGuestList(coli_sn); //
getVeiPlanChange(travelAgencyId, gri_sn); //
getAirPortList(); //
}, []);
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>
<Button type="link" target="_blank" href={reservationUrl}>
下载
</Button>
</Col>
</Row>
<Row>
<Divider orientation="center">{planDetail ? `${planDetail[0].GRI_No} - ${planDetail[0].WL}` : ""}</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={() => {
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;

@ -0,0 +1,16 @@
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>Oops!</h1>
<p>Sorry, an unexpected error has occurred.</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}

@ -1,206 +1,198 @@
import { useParams, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Row, Col, Space, Button, Divider, Form, Typography, Rate, Radio, Upload, Input, App, Card } from 'antd';
import { PlusOutlined } from '@ant-design/icons';
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 { useParams, useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { toJS, runInAction } from "mobx";
import { Row, Col, Space, Button, Divider, Form, Typography, Rate, Radio, Upload, Input, App, Card } from "antd";
import { useStore } from "../../stores/StoreContext.js";
import { PlusOutlined } from "@ant-design/icons";
const { Title, Text, Paragraph } = Typography;
import * as config from "@/config";
function Detail() {
const navigate = useNavigate();
const { GRI_SN, RefNo, CII_SN } = useParams();
const {travelAgencyId, loginToken} = usingStorage();
const desc = ['none', 'Unacceptable', 'Poor', 'Fair', 'Very Good', 'Excellent'];
const { notification } = App.useApp();
const [form] = Form.useForm();
const [feedbackRate, setFeedbackRate] = useState({});
const [feedbackReview, setFeedbackReview] = useState({});
const [feedbackImages, setFeedbackImages] = useState([]);
const [feedbackInfo, setFeedbackInfo] = useState({});
useEffect(() => {
// console.info('Detail.useEffect: ' + GRI_SN);
getFeedbackDetail(travelAgencyId, GRI_SN).then((res) => {
// setFeedbackRate(res.feedbackRate);
setFeedbackReview(res.feedbackReview);
});
getCustomerFeedbackDetail(travelAgencyId, GRI_SN, CII_SN).then((res) => setFeedbackRate(res));
getFeedbackImages(travelAgencyId, GRI_SN).then((res) => setFeedbackImages(res));
getFeedbackInfo(travelAgencyId, GRI_SN).then((v) => {
form.setFieldsValue({ info_content: v.EEF_Content });
setFeedbackInfo(v);
});
}, [GRI_SN]);
const HWO_Guide = feedbackRate && feedbackRate.HWO_Guide ? feedbackRate.HWO_Guide : [];
const HWO_Driver = feedbackRate && feedbackRate.HWO_Driver ? feedbackRate.HWO_Driver : [];
const HWO_Activity = feedbackRate && feedbackRate.HWO_Activity ? feedbackRate.HWO_Activity : [];
const OtherThoughts = feedbackRate && feedbackRate.OtherThoughts ? feedbackRate.OtherThoughts : '';
const signatureData = feedbackRate && feedbackRate.signatureData ? feedbackRate.signatureData : '';
const PhotoPermission = feedbackRate?.PhotoPermission === 1;
const cityName = feedbackRate?.cityName || '';
const ECI_Content = feedbackReview && feedbackReview.ECI_Content ? feedbackReview.ECI_Content : 'None';
const fileList = feedbackImages;
const handleChange = (info) => {
let newFileList = [...info.fileList];
newFileList = newFileList.map((file) => {
if (file.response && file.response.result) {
file.url = file.response.result.file_url;
}
return file;
});
setFeedbackImages(newFileList);
};
const handRemove = (info) => {
return removeFeedbackImages(info.url);
};
const onFinish = (values) => {
// console.log("Success:", values);
if (values) {
postFeedbackInfo(feedbackInfo.EEF_VEI_SN, feedbackInfo.EEF_GRI_SN, feedbackInfo.EEF_EOI_SN, values.info_content).then(() => {
notification.success({
message: `Notification`,
description: 'Submit Successful',
placement: 'top',
duration: 4,
});
});
}
};
return (
<Space direction='vertical' style={{ width: '100%' }}>
<Row gutter={16}>
<Col span={20}></Col>
<Col span={4}>
<BackBtn />
</Col>
</Row>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={18}>
<Card
type='inner'
title={
<Title level={4}>
Post Survey {RefNo} in {cityName}
</Title>
}>
<Form labelCol={{ span: 5 }}>
<Divider orientation='left'>How satisfied were you with your tour guide?</Divider>
{HWO_Guide.map((ele) => (
<Form.Item label={ele.Describe} key={ele.id}>
<Space>
<Rate disabled value={ele.rate} />
<span className='ant-rate-text'>{desc[ele.rate]}</span>
</Space>
</Form.Item>
))}
<Divider orientation='left'>How about the Driver and Car/Van?</Divider>
{HWO_Driver.map((ele) => (
<Form.Item label={ele.Describe} key={ele.id}>
<Space>
<Rate disabled value={ele.rate} />
<span className='ant-rate-text'>{desc[ele.rate]}</span>
</Space>
</Form.Item>
))}
<Divider orientation='left'>General Experience with:</Divider>
{HWO_Activity.map((ele) => (
<Form.Item label={ele.Describe} key={ele.id}>
<Space>
<Rate disabled value={ele.rate} />
<span className='ant-rate-text'>{desc[ele.rate]}</span>
</Space>
</Form.Item>
))}
<Divider orientation='left'>Would you like to give us permission to use the photos taken by the tour guide(s) during your trip which contain your portrait?</Divider>
<Paragraph>
{PhotoPermission ? (
<>
<Radio checked>Yes</Radio>
<Radio disabled={true}>No</Radio>
</>
) : (
<>
<Radio disabled={true}>Yes</Radio>
<Radio checked>No</Radio>
</>
)}
</Paragraph>
<Divider orientation='left'>Other thoughts you want to share with us:</Divider>
<Text>{OtherThoughts}</Text>
<Divider orientation='left'>Signature:</Divider>
{signatureData ? <img id='signature-img' alt='customer signature' title='customer signature' src={signatureData} /> : null}
</Form>
</Card>
</Col>
<Col span={4}></Col>
</Row>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={18}>
<Card type='inner' title={<Title level={4}>External Reviews</Title>}>
<Text>{ECI_Content}</Text>
</Card>
</Col>
<Col span={4}></Col>
</Row>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={18}>
<Card type='inner' title={<Title level={4}>Feedback from local agent</Title>}>
<Form name='feedback_detail_from' onFinish={onFinish} labelCol={{ span: 5 }} form={form}>
<Form.Item>
<Upload
name='ghhfile'
// accept="image/*"
multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${loginToken}`}
fileList={fileList}
listType='picture-card'
onChange={handleChange}
onRemove={handRemove}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload photos</div>
</div>
</Upload>
</Form.Item>
<Form.Item
name='info_content'
rules={[
{
required: true,
message: 'Please input your messages!',
},
]}>
<Input.TextArea rows={6} placeholder='Any feedback for this group you would like to send to Asia Highlights'></Input.TextArea>
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit'>
Submit
</Button>
</Form.Item>
</Form>
</Card>
</Col>
<Col span={4}></Col>
</Row>
</Space>
);
const navigate = useNavigate();
const { GRI_SN, RefNo, CII_SN } = useParams();
const { feedbackStore, authStore } = useStore();
const { feedbackReview, feedbackImages, feedbackInfo, feedbackServiceRate: feedbackRate } = feedbackStore;
const desc = ["none", "Unacceptable", "Poor", "Fair", "Very Good", "Excellent"];
const { notification } = App.useApp();
const [form] = Form.useForm();
useEffect(() => {
console.info("Detail.useEffect: " + GRI_SN);
feedbackStore.getFeedbackDetail(authStore.login.travelAgencyId, GRI_SN);
feedbackStore.getCustomerFeedbackDetail(authStore.login.travelAgencyId, GRI_SN, CII_SN);
feedbackStore.getFeedbackImages(authStore.login.travelAgencyId, GRI_SN);
feedbackStore.getFeedbackInfo(authStore.login.travelAgencyId, GRI_SN).then(v => {
form.setFieldsValue({ info_content: v.EEF_Content });
});
}, [GRI_SN]);
const HWO_Guide = feedbackRate && feedbackRate.HWO_Guide ? feedbackRate.HWO_Guide : [];
const HWO_Driver = feedbackRate && feedbackRate.HWO_Driver ? feedbackRate.HWO_Driver : [];
const HWO_Activity = feedbackRate && feedbackRate.HWO_Activity ? feedbackRate.HWO_Activity : [];
const OtherThoughts = feedbackRate && feedbackRate.OtherThoughts ? feedbackRate.OtherThoughts : "";
const signatureData = feedbackRate && feedbackRate.signatureData ? feedbackRate.signatureData : "";
const PhotoPermission = feedbackRate?.PhotoPermission === 1;
const cityName = feedbackRate.cityName;
const ECI_Content = feedbackReview && feedbackReview.ECI_Content ? feedbackReview.ECI_Content : "None";
const fileList = toJS(feedbackImages);
const handleChange = info => {
let newFileList = [...info.fileList];
newFileList = newFileList.map(file => {
if (file.response && file.response.result) {
file.url = file.response.result.file_url;
}
return file;
});
runInAction(() => {
feedbackStore.feedbackImages = newFileList;
});
};
const handRemove = info => {
return feedbackStore.removeFeedbackImages(info.url);
};
const onFinish = values => {
console.log("Success:", values);
if (values) {
feedbackStore.postFeedbackInfo(feedbackInfo.EEF_VEI_SN, feedbackInfo.EEF_GRI_SN, feedbackInfo.EEF_EOI_SN, values.info_content).then(() => {
notification.success({
message: `Notification`,
description: "Submit Successful",
placement: "top",
duration: 4,
});
});
}
};
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col span={20}>
</Col>
<Col span={4}>
<Button type="link" onClick={() => navigate("/feedback")}>
Back
</Button>
</Col>
</Row>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={18}>
<Card type="inner" title={<Title level={4}>Post Survey {RefNo} in {cityName}</Title>}>
<Form labelCol={{ span: 5 }}>
<Divider orientation="left">How satisfied were you with your tour guide?</Divider>
{HWO_Guide.map(ele => (
<Form.Item label={ele.Describe} key={ele.id}>
<Space>
<Rate disabled value={ele.rate} />
<span className="ant-rate-text">{desc[ele.rate]}</span>
</Space>
</Form.Item>
))}
<Divider orientation="left">How about the Driver and Car/Van?</Divider>
{HWO_Driver.map(ele => (
<Form.Item label={ele.Describe} key={ele.id}>
<Space>
<Rate disabled value={ele.rate} />
<span className="ant-rate-text">{desc[ele.rate]}</span>
</Space>
</Form.Item>
))}
<Divider orientation="left">General Experience with:</Divider>
{HWO_Activity.map(ele => (
<Form.Item label={ele.Describe} key={ele.id}>
<Space>
<Rate disabled value={ele.rate} />
<span className="ant-rate-text">{desc[ele.rate]}</span>
</Space>
</Form.Item>
))}
<Divider orientation="left">Would you like to give us permission to use the photos taken by the tour guide(s) during your trip which contain your portrait?</Divider>
<Paragraph>
{PhotoPermission ? (
<>
<Radio checked>Yes</Radio>
<Radio disabled={true}>No</Radio>
</>
) : (
<>
<Radio disabled={true}>Yes</Radio>
<Radio checked>No</Radio>
</>
)}
</Paragraph>
<Divider orientation="left">Other thoughts you want to share with us:</Divider>
<Text>{OtherThoughts}</Text>
<Divider orientation="left">Signature:</Divider>
{signatureData ? <img id="signature-img" alt="customer signature" title="customer signature" src={signatureData} /> : null}
</Form></Card>
</Col>
<Col span={4}></Col>
</Row>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={18}>
<Card type="inner" title={<Title level={4}>External Reviews</Title>}>
<Text>{ECI_Content}</Text>
</Card>
</Col>
<Col span={4}></Col>
</Row>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={18}>
<Card type="inner" title={<Title level={4}>Feedback from local agent</Title>}>
<Form name="feedback_detail_from" onFinish={onFinish} labelCol={{ span: 5 }} form={form}>
<Form.Item>
<Upload
name="ghhfile"
// accept="image/*"
multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${authStore.login.travelAgencyId}&token=${authStore.login.token}`}
fileList={fileList}
listType="picture-card"
onChange={handleChange}
onRemove={handRemove}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload photos</div>
</div>
</Upload>
</Form.Item>
<Form.Item
name="info_content"
rules={[
{
required: true,
message: "Please input your messages!",
},
]}>
<Input.TextArea rows={6} placeholder="Any feedback for this group you would like to send to Asia Highlights"></Input.TextArea>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
</Card>
</Col>
<Col span={4}></Col>
</Row>
</Space>
);
}
export default Detail;
export default observer(Detail);

@ -1,38 +1,29 @@
import { useParams, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { toJS, runInAction } from "mobx";
import { Row, Col, Space, Button, Divider, Form, Typography, Rate, Radio, Upload, Input, App, Card } from "antd";
import { useStore } from "../../stores/StoreContext.js";
import { PlusOutlined } from "@ant-design/icons";
import * as config from "@/config";
import { getFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
const { Title, Text, Paragraph } = Typography;
import * as config from "@/config";
function Detail() {
const navigate = useNavigate();
const { GRI_SN,RefNo } = useParams();
const {travelAgencyId, loginToken} = usingStorage();
const { feedbackStore, authStore } = useStore();
const { feedbackRate, feedbackReview, feedbackImages, feedbackInfo } = feedbackStore;
const desc = ["none", "Unacceptable", "Poor", "Fair", "Very Good", "Excellent"];
const { notification } = App.useApp();
const [form] = Form.useForm();
const [feedbackRate, setFeedbackRate] = useState({});
const [feedbackReview, setFeedbackReview] = useState({});
const [feedbackImages, setFeedbackImages] = useState([]);
const [feedbackInfo, setFeedbackInfo] = useState({});
useEffect(() => {
// console.info("Detail.useEffect: " + GRI_SN);
getFeedbackDetail(travelAgencyId, GRI_SN).then((res) => {
setFeedbackRate(res.feedbackRate);
setFeedbackReview(res.feedbackReview);
});
getFeedbackImages(travelAgencyId, GRI_SN).then((res) => setFeedbackImages(res));
getFeedbackInfo(travelAgencyId, GRI_SN).then((v) => {
form.setFieldsValue({ info_content: v.EEF_Content });
setFeedbackInfo(v);
});
console.info("Detail.useEffect: " + GRI_SN);
feedbackStore.getFeedbackDetail(authStore.login.travelAgencyId, GRI_SN);
feedbackStore.getFeedbackImages(authStore.login.travelAgencyId, GRI_SN);
feedbackStore.getFeedbackInfo(authStore.login.travelAgencyId, GRI_SN).then(v => {
form.setFieldsValue({ info_content: v.EEF_Content });
});
}, [GRI_SN]);
const HWO_Guide = feedbackRate && feedbackRate.HWO_Guide ? feedbackRate.HWO_Guide : 0;
@ -43,10 +34,10 @@ function Detail() {
const OtherThoughts = feedbackRate && feedbackRate.OtherThoughts ? feedbackRate.OtherThoughts : "";
const PhotoPermission = feedbackRate && feedbackRate.PhotoPermission && feedbackRate.PhotoPermission == "YES" ? true : false;
const ECI_Content = feedbackReview && feedbackReview.ECI_Content ? feedbackReview.ECI_Content : "None";
const fileList = (feedbackImages);
const fileList = toJS(feedbackImages);
const handleChange = info => {
// console.log(info);
console.log(info);
let newFileList = [...info.fileList];
newFileList = newFileList.map(file => {
if (file.response && file.response.result) {
@ -54,17 +45,19 @@ function Detail() {
}
return file;
});
setFeedbackImages(newFileList);
runInAction(() => {
feedbackStore.feedbackImages = newFileList;
});
};
const handRemove = info => {
return removeFeedbackImages(info.url);
return feedbackStore.removeFeedbackImages(info.url);
};
const onFinish = values => {
// console.log("Success:", values);
console.log("Success:", values);
if (values) {
postFeedbackInfo(feedbackInfo.EEF_VEI_SN, feedbackInfo.EEF_GRI_SN, feedbackInfo.EEF_EOI_SN, values.info_content).then(() => {
feedbackStore.postFeedbackInfo(feedbackInfo.EEF_VEI_SN, feedbackInfo.EEF_GRI_SN, feedbackInfo.EEF_EOI_SN, values.info_content).then(() => {
notification.success({
message: `Notification`,
description: "Submit Successful",
@ -82,7 +75,9 @@ function Detail() {
</Col>
<Col span={4}>
<BackBtn />
<Button type="link" onClick={() => navigate("/feedback")}>
Back
</Button>
</Col>
</Row>
<Row gutter={16}>
@ -166,7 +161,7 @@ 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=${authStore.login.travelAgencyId}&token=${authStore.login.token}`}
fileList={fileList}
listType="picture-card"
onChange={handleChange}
@ -201,4 +196,4 @@ function Detail() {
);
}
export default (Detail);
export default observer(Detail);

@ -1,94 +1,108 @@
import { NavLink } from 'react-router-dom';
import { useEffect } from 'react';
import { Row, Col, Space, Table, App } from 'antd';
import { useTranslation } from 'react-i18next';
import SearchForm from '@/components/SearchForm';
import useFeedbackStore from '@/stores/Feedback';
import dayjs from 'dayjs';
import { usingStorage } from '@/hooks/usingStorage';
import { NavLink } from "react-router-dom";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Input, Typography, DatePicker, App } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
const { Title } = Typography;
const feedbackListColumns = [
{
title: 'Ref.No',
dataIndex: 'EOI_Group_Name',
render: (text, record) => (
<NavLink to={record.EOI_CII_SN ? `/feedback/${record.EOI_GRI_SN}/${record.EOI_CII_SN}/${record.EOI_Group_Name}` : `/feedback/${record.EOI_GRI_SN}/${record.EOI_Group_Name}`}>
{text}
</NavLink>
),
},
{
title: 'Arrival Date',
dataIndex: 'EOI_Date',
render: (text, record) => text,
sorter: (a, b) => b.EOI_Date - a.EOI_Date,
},
{
title: 'Cities',
key: 'City',
dataIndex: 'City',
},
{
title: 'Guides',
dataIndex: 'CityGuide',
},
{
title: 'Post Survey',
dataIndex: 'Average',
render: (text, record) => `${record.EOI_CII_SN && record.feedback_Filled ? '✔' : ''} ${text ? text : ''}`, //0
sorter: (a, b) => b.Average - a.Average,
},
{
title: 'External Reviews',
dataIndex: 'TAGood',
},
{
title: "Ref.No",
dataIndex: "EOI_Group_Name",
render: (text, record) => <NavLink to={record.EOI_CII_SN ? `/feedback/${record.EOI_GRI_SN}/${record.EOI_CII_SN}/${record.EOI_Group_Name}` : `/feedback/${record.EOI_GRI_SN}/${record.EOI_Group_Name}`}>{text}</NavLink>,
},
{
title: "Arrival Date",
dataIndex: "EOI_Date",
render: (text, record) => text,
sorter: (a, b) => b.EOI_Date - a.EOI_Date,
},
{
title: "Cities",
key: "City",
dataIndex: "City",
},
{
title: "Guides",
dataIndex: "CityGuide",
},
{
title: "Post Survey",
dataIndex: "Average",
render: (text, record) => `${(record.EOI_CII_SN && record.feedback_Filled ? "✔" : "")} ${(text ? text : "")}`, //0
sorter: (a, b) => b.Average - a.Average,
},
{
title: "External Reviews",
dataIndex: "TAGood",
},
];
function Index() {
const { t } = useTranslation();
const { notification } = App.useApp();
const { notification } = App.useApp();
const { feedbackStore, authStore } = useStore();
const { feedbackList, search_date_start, search_date_end } = feedbackStore;
const [referenceNo, onNumberChange] = useState("");
const showTotal = total => `Total ${feedbackList.length} items`;
const {travelAgencyId} = usingStorage();
const [loading, feedbackList, fetchFeedbackList] = useFeedbackStore((state) => [state.loading, state.feedbackList, state.fetchFeedbackList]);
useEffect(() => {
// feedbackStore.searchFeedbackList(authStore.login.travelAgencyId, referenceNo, search_date_start.format(config.DATE_FORMAT), search_date_end.format(config.DATE_FORMAT) + " 23:59").catch(ex => {
// // notification.error({
// // message: `Error`,
// // description: ex.message,
// // placement: 'top',
// // duration: 4,
// // });
// console.log(ex.message);
// });
}, []);
const showTotal = (total) => `Total ${total} items`;
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Title level={3}></Title>
useEffect(() => {
// feedbackStore.searchFeedbackList(authStore.login.travelAgencyId, referenceNo, search_date_start.format(config.DATE_FORMAT), search_date_end.format(config.DATE_FORMAT) + " 23:59").catch(ex => {
// // notification.error({
// // message: `Error`,
// // description: ex.message,
// // placement: 'top',
// // duration: 4,
// // });
// console.log(ex.message);
// });
}, []);
return (
<Space direction='vertical' style={{ width: '100%' }}>
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
}}
fieldsConfig={{
shows: ['referenceNo', 'dates'],
fieldProps: {
dates: { label: t('group:ArrivalDate') },
},
}}
onSubmit={(err, formVal, filedsVal) => {
fetchFeedbackList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime);
}}
/>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered={true} columns={feedbackListColumns} dataSource={feedbackList} pagination={{ defaultPageSize: 20, showTotal: showTotal }} loading={loading} />
</Col>
<Col md={24} lg={24} xxl={24}></Col>
</Row>
</Space>
);
<Row gutter={16}>
<Col md={24} lg={6} xxl={4}>
<Input
placeholder="Reference Number"
onChange={e => {
onNumberChange(e.target.value);
}}
/>
</Col>
<Col md={24} lg={8} xxl={6}>
<Space direction="horizontal">
Arrival Date
<DatePicker.RangePicker
format={config.DATE_FORMAT}
allowClear={false}
style={{ width: "100%" }}
value={[search_date_start, search_date_end]}
presets={config.DATE_PRESETS}
onChange={feedbackStore.onDateRangeChange}
/>
</Space>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button
type="primary"
loading={feedbackStore.loading}
onClick={() => feedbackStore.searchFeedbackList(authStore.login.travelAgencyId, referenceNo, search_date_start.format(config.DATE_FORMAT), search_date_end.format(config.DATE_FORMAT) + " 23:59")}>
Search
</Button>
</Col>
</Row>
<Title level={3}></Title>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered={true} columns={feedbackListColumns} dataSource={toJS(feedbackList)} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
</Col>
<Col md={24} lg={24} xxl={24}></Col>
</Row>
</Space>
);
}
export default Index;
export default observer(Index);

@ -1,21 +1,22 @@
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 { useParams, useNavigate, NavLink } from "react-router-dom";
import { useEffect, useState, useRef } from "react";
import { observer } from "mobx-react";
import { toJS, runInAction } from "mobx";
import { Row, Col, Space, Button, Typography, Card, Form, Upload, Input, Divider, DatePicker, Select, App, Descriptions, Image, Steps } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import { PlusOutlined, AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } 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 { usingStorage } from "@/hooks/usingStorage";
const { Title, Text } = Typography;
const { Title,Text } = Typography;
const { TextArea } = Input;
function Detail() {
const navigate = useNavigate();
const { GMDSN, GSN } = useParams();
const { userId, travelAgencyId, loginToken } = usingStorage();
const { invoiceStore, authStore } = useStore();
const { invoicekImages, invoiceGroupInfo, invoiceProductList, invoiceCurrencyList, invoiceZDDetail } = invoiceStore;
const [form] = Form.useForm();
const [dataLoading, setDataLoading] = useState(false);
const [edited, setEdited] = useState(true); //
@ -24,14 +25,6 @@ function Detail() {
const [invoicePicList, setInvoicePicList] = 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([]);
useEffect(() => {
console.info("Detail.useEffect: " + GMDSN + "/" + GSN);
defaultShow();
@ -39,23 +32,19 @@ function Detail() {
function defaultShow() {
setDataLoading(true);
fetchInvoiceDetail(travelAgencyId, GMDSN, GSN)
invoiceStore
.fetchInvoiceDetail(GMDSN, GSN)
.then(json => {
setInvoiceZDDetail(json.invoiceZDDetail);
setInvoiceGroupInfo(json.invoiceGroupInfo);
setInvoiceProductList(json.invoiceProductList);
setInvoiceCurrencyList(json.invoiceCurrencyList);
let ZDDetail = json.invoiceZDDetail;
let ZDDetail = json.ZDDetail;
if (isNotEmpty(ZDDetail)) {
let arrLen = ZDDetail.length;
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);
return _formData;
runInAction(() => {
invoiceStore.invoiceFormData = { 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 };
});
return { 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 };
}
});
@ -79,7 +68,9 @@ function Detail() {
});
}
if (data.GMD_Dealed == false && arrLen == index + 1) {
setInvoicekImages(picList);
runInAction(() => {
invoiceStore.invoicekImages = picList;
});
}
return picList;
});
@ -99,7 +90,7 @@ function Detail() {
});
}
const fileList = invoicekImages;
const fileList = toJS(invoicekImages);
//
let arrimg = [];
if (isNotEmpty(fileList)) {
@ -115,13 +106,15 @@ function Detail() {
info_date: isNotEmpty(values["info_date"]) ? values["info_date"].format("YYYY-MM") : null,
info_images: JSON.stringify(arrimg),
};
// console.log("Success:", fieldVaule);
console.log("Success:", fieldVaule);
//
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);
invoiceStore.postEditInvoiceDetail(fieldVaule.info_gmdsn, fieldVaule.info_Currency, fieldVaule.info_money, fieldVaule.info_date, fieldVaule.info_images, "").then(data => {
console.log(data);
runInAction(() => {
let param = { info_money: fieldVaule.info_money, info_Currency: fieldVaule.info_Currency, info_date: fieldVaule.info_date };
invoiceStore.invoiceFormData = param;
});
if (data.errcode == 0) {
setEdited(true);
notification.success({
@ -144,12 +137,14 @@ function Detail() {
}
return file;
});
setInvoicekImages(newFileList);
runInAction(() => {
invoiceStore.invoicekImages = newFileList;
});
};
const handRemove = info => {
console.log(info);
removeFeedbackImages(info.url);
invoiceStore.removeFeedbackImages(info.url);
return true;
};
@ -167,7 +162,8 @@ function Detail() {
}
function addInvoice() {
postAddInvoice(userId, travelAgencyId, GSN, "", 0, "", "[]", "")
invoiceStore
.postAddInvoice(GSN, "", 0, "", "[]", "")
.then(data => {})
.finally(() => {
defaultShow();
@ -193,21 +189,6 @@ function Detail() {
}
}
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 bindSubmitForm() {
if (isNotEmpty(invoiceZDDetail)) {
@ -218,29 +199,27 @@ 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="Status">{invoiceStatus(data.FKState)}</Descriptions.Item>
<Descriptions.Item label="Due Dat">{data.GMD_PayDate}</Descriptions.Item>
<Descriptions.Item label="Status">{invoiceStore.invoiceStatus(data.FKState)}</Descriptions.Item>
</Descriptions>
</Col>
<Col span={12}>
<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 +232,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 +264,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 +290,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=${authStore.login.travelAgencyId}&FilePathName=invoice&token=${authStore.login.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,9 +333,8 @@ 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 type="link" onClick={() => navigate("/invoice")}>
Back
</Button>
</Col>
</Row>
@ -370,4 +345,4 @@ function Detail() {
);
}
export default Detail;
export default observer(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;

@ -1,19 +1,23 @@
import { NavLink, useNavigate } from "react-router-dom";
import { Row, Col, Space, Button, Table, App, Steps } from "antd";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Input, DatePicker, Typography, App, Steps ,Select } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
import { formatDate, isNotEmpty } from "@/utils/commons";
import { AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons";
import SearchForm from '@/components/SearchForm';
import dayjs from 'dayjs';
import useInvoiceStore from '@/stores/Invoice';
import { usingStorage } from "@/hooks/usingStorage";
const { Title } = Typography;
function Index() {
const {travelAgencyId, } = usingStorage();
const [invoiceList, fetchInvoiceList] = useInvoiceStore((state) => [state.invoiceList, state.fetchInvoiceList]);
const { authStore, invoiceStore } = useStore();
const { invoiceList, search_date_start, search_date_end } = invoiceStore;
const [groupNo, onGroupNoChange] = useState("");
const [OrderType, onOrderTypeChange] = useState(0); //
const navigate = useNavigate();
const { notification } = App.useApp();
const showTotal = total => `Total ${total} items`;
const showTotal = total => `Total ${invoiceList.length} items`;
const invoiceListColumns = [
{
@ -71,44 +75,84 @@ function Index() {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Title level={3}></Title>
<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 md={24} lg={4} xxl={4}>
<Input
placeholder="Reference Number"
onChange={e => {
onGroupNoChange(e.target.value);
}}
/>
</Col>
<Col md={24} lg={4} xxl={3}>
<Select
defaultValue="Status"
style={{
width: 220,
}}
onChange={ value => {onOrderTypeChange(value);}}
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',
},
]}
/>
</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>
<Col md={24} lg={8} xxl={6}>
<Space direction="horizontal">
Arrival Date
<DatePicker.RangePicker format={config.DATE_FORMAT} allowClear={true} style={{ width: "100%" }} defaultValue={[search_date_start, search_date_end]} presets={config.DATE_PRESETS} onChange={invoiceStore.onDateRangeChange} allowEmpty={true} />
</Space>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button
type="primary"
loading={invoiceStore.loading}
onClick={() => invoiceStore.fetchInvoiceList(authStore.login.travelAgencyId, groupNo, search_date_start==null?null:search_date_start.format(config.DATE_FORMAT), search_date_end==null?null:search_date_end.format(config.DATE_FORMAT),OrderType)}>
Search
</Button>
</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>
<Title level={3}></Title>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={(invoiceList)} />
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={toJS(invoiceList)} />
</Col>
</Row>
</Space>
);
}
export default (Index);
export default observer(Index);
//value={[search_date_start, search_date_end]} 20230803 zp

@ -1,97 +1,120 @@
import { NavLink, useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
import { Row, Col, Space, Table, Image } from 'antd';
import { formatDate, isNotEmpty } from '@/utils/commons';
import SearchForm from '@/components/SearchForm';
import dayjs from 'dayjs';
import BackBtn from '@/components/BackBtn';
import useInvoiceStore from '@/stores/Invoice';
import { usingStorage } from '@/hooks/usingStorage';
import { NavLink, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Input, DatePicker, Typography, App, Image } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
import { formatDate, isNotEmpty } from "@/utils/commons";
const { Title } = Typography;
function Paid() {
const {travelAgencyId, } = usingStorage();
const [invoicePaid, fetchInvoicePaid] = useInvoiceStore((state) => [state.invoicePaid, state.fetchInvoicePaid]);
const navigate = useNavigate();
const showTotal = (total) => `Total ${total} items`;
function Paid(){
const { authStore, invoiceStore } = useStore();
const { invoicePaid, search_date_start, search_date_end } = invoiceStore;
const [groupNo, onGroupNoChange] = useState("");
const navigate = useNavigate();
const showTotal = total => `Total ${invoicePaid.length} items`;
useEffect(() => {
fetchInvoicePaid(travelAgencyId, '', '', '');
}, []);
const invoicePaidColumns = [
{
title: 'Payment ref.NO',
dataIndex: 'fl_finaceNo',
key: 'fl_finaceNo',
render: (text, record) => <NavLink to={`/invoice/paid/detail/${record.key}`}>{text}</NavLink>,
},
{
title: 'Payment date',
key: 'fl_adddate',
dataIndex: 'fl_adddate',
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ''),
},
{
title: 'Number of bills',
key: 'fcount',
dataIndex: 'fcount',
},
{
title: 'Total amount',
key: 'pSum',
dataIndex: 'pSum',
//render: (text, record) => (isNotEmpty(record.GMD_Currency) ? record.GMD_Currency + " " + text : text),
},
{
title: 'Bank statement',
key: 'fl_pic',
dataIndex: 'fl_pic',
render: showPIc,
},
];
function showPIc(text, record) {
let strPic = record.fl_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 '';
useEffect (() => {
invoiceStore.fetchInvoicePaid(authStore.login.travelAgencyId,"","","");
},[]);
const invoicePaidColumns = [
{
title: "Payment ref.NO",
dataIndex: "fl_finaceNo",
key: "fl_finaceNo",
render: (text, record) => <NavLink to={`/invoice/paid/detail/${record.key}`}>{text}</NavLink>,
},
{
title: "Payment date",
key: "fl_adddate",
dataIndex: "fl_adddate",
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
},
{
title: "Number of bills",
key: "fcount",
dataIndex: "fcount",
},
{
title: "Total amount",
key: "pSum",
dataIndex: "pSum",
//render: (text, record) => (isNotEmpty(record.GMD_Currency) ? record.GMD_Currency + " " + text : text),
},
{
title: "Bank statement",
key: "fl_pic",
dataIndex: "fl_pic",
render: showPIc,
},
];
function showPIc(text,record) {
let strPic = record.fl_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}>
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
}}
fieldsConfig={{
shows: ['referenceNo', 'dates'],
fieldProps: {
referenceNo: { col: 5 },
dates: { col: 10, label: 'Date' },
},
}}
onSubmit={(err, formVal, filedsVal) => {
fetchInvoicePaid(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.enddate);
}}
/>
</Col>
<Col span={4}>
<BackBtn to={'/invoice'} />
</Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered columns={invoicePaidColumns} dataSource={(invoicePaid)} />
</Col>
</Row>
</Space>
);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col span={20}>
</Col>
<Col span={4}>
<Button type="link" onClick={() => navigate("/invoice")}>
Back
</Button>
</Col>
</Row>
<Title level={3}></Title>
<Row gutter={16}>
<Col md={24} lg={6} xxl={4}>
<Input
placeholder="Reference Number"
onChange={e => {
onGroupNoChange(e.target.value);
}}
/>
</Col>
<Col md={24} lg={8} xxl={6}>
<Space direction="horizontal">
Date
<DatePicker.RangePicker format={config.DATE_FORMAT} allowClear={false} style={{ width: "100%" }} value={[search_date_start, search_date_end]} presets={config.DATE_PRESETS} onChange={invoiceStore.onDateRangeChange} />
</Space>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button
type="primary"
loading={invoiceStore.loading}
onClick={() => invoiceStore.fetchInvoicePaid(authStore.login.travelAgencyId, groupNo, search_date_start.format(config.DATE_FORMAT), search_date_end.format(config.DATE_FORMAT))}>
Search
</Button>
</Col>
</Row>
<Title level={3}></Title>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered columns={invoicePaidColumns} dataSource={toJS(invoicePaid)} />
</Col>
</Row>
</Space>
);
}
export default (Paid);
export default observer(Paid);

@ -1,63 +1,67 @@
import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Row, Col, Space, Table, Typography } from 'antd';
import { formatDate, isNotEmpty } from '@/utils/commons';
import BackBtn from '@/components/BackBtn';
import { fetchInvoicePaidDetail } from '@/stores/Invoice';
import { usingStorage } from '@/hooks/usingStorage';
import { NavLink, useNavigate ,useParams} from "react-router-dom";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Typography } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
import { formatDate, isNotEmpty } from "@/utils/commons";
const { Title } = Typography;
function PaidDetail() {
const navigate = useNavigate();
const {travelAgencyId, } = usingStorage();
const { flid } = useParams();
function PaidDetail(){
const navigate = useNavigate();
const { authStore, invoiceStore } = useStore();
const { invoicePaidDetail } = invoiceStore;
const { flid } = useParams();
useEffect(() => {
invoiceStore.fetchInvoicePaidDetail(authStore.login.travelAgencyId,flid);
const [invoicePaidDetail, setInvoicePaidDetail] = useState([]);
useEffect(() => {
fetchInvoicePaidDetail(travelAgencyId, flid).then(res => setInvoicePaidDetail(res));
}, [flid]);
},[flid]);
const invoicePaidColumns = [
{
title: 'Ref.NO',
dataIndex: 'fl2_GroupName',
key: 'fl2_GroupName',
},
{
title: 'Arrival date',
key: 'fl2_ArriveDate',
dataIndex: 'fl2_ArriveDate',
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ''),
},
{
title: 'Payment amount',
key: 'fl2_price',
dataIndex: 'fl2_price',
},
{
title: 'Currency',
key: 'fl2_memo',
dataIndex: 'fl2_memo',
},
];
const invoicePaidColumns = [
{
title: "Ref.NO",
dataIndex: "fl2_GroupName",
key: "fl2_GroupName",
},
{
title: "Arrival date",
key: "fl2_ArriveDate",
dataIndex: "fl2_ArriveDate",
render: (text, record) => (isNotEmpty(text) ? formatDate(new Date(text)) : ""),
},
{
title: "Payment amount",
key: "fl2_price",
dataIndex: "fl2_price",
},
{
title: "Currency",
key: "fl2_memo",
dataIndex: "fl2_memo",
},
];
return (
<Space direction='vertical' style={{ width: '100%' }}>
<Row gutter={16}>
<Col span={20}></Col>
<Col span={4}>
<BackBtn />
</Col>
</Row>
<Title level={3}></Title>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered columns={invoicePaidColumns} dataSource={invoicePaidDetail} />
</Col>
</Row>
</Space>
);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col span={20}>
</Col>
<Col span={4}>
<Button type="link" onClick={() => navigate("/invoice/paid")}>
Back
</Button>
</Col>
</Row>
<Title level={3}></Title>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered columns={invoicePaidColumns} dataSource={toJS(invoicePaidDetail)} />
</Col>
</Row>
</Space>
);
}
export default PaidDetail;
export default observer(PaidDetail);

@ -1,44 +1,42 @@
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';
import { NavLink, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Input, Typography, Badge, Divider } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
import * as comm from "@/utils/commons";
import dayjs from "dayjs";
const { Title, Paragraph } = Typography;
const { Title, Paragraph, Text } = Typography;
function Detail() {
const { t } = useTranslation();
const { CCP_BLID } = useParams();
const {userId} = usingStorage();
const { noticeStore, authStore } = useStore();
const { noticeInfo } = noticeStore;
const { CCP_BLID } = useParams();
const [noticeInfo, setNoticeInfo] = useState({});
useEffect(() => {
// console.info("notice detail .useEffect " + CCP_BLID);
fetchNoticeDetail(userId, CCP_BLID).then((res) => {
setNoticeInfo(res);
});
}, []);
useEffect(() => {
console.info("notice detail .useEffect " + CCP_BLID);
noticeStore.getNoticeDetail(authStore.login.userId, CCP_BLID);
}, []);
return (
<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>
<Paragraph>
<div dangerouslySetInnerHTML={{ __html: comm.escape2Html(noticeInfo.CCP_BLContent) }}></div>
</Paragraph>
</Col>
<Col span={4}>
<BackBtn />
</Col>
</Row>
</Space>
);
return (
<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>
<Paragraph>
<div dangerouslySetInnerHTML={{ __html: comm.escape2Html(noticeInfo.CCP_BLContent) }}></div>
</Paragraph>
</Col>
<Col span={4}>
<NavLink to="/notice">Back</NavLink>
</Col>
</Row>
</Space>
);
}
export default Detail;
export default observer(Detail);

@ -1,21 +1,23 @@
import { NavLink } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Typography, Badge, List } from "antd";
import useNoticeStore, { fetchBulletinList } from '@/stores/Notice';
import { usingStorage } from "@/hooks/usingStorage";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Input, Typography, Badge, List } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
import * as comm from "@/utils/commons";
import dayjs from "dayjs";
const { Title, Paragraph, Text } = Typography;
function Index() {
const { userId, } = usingStorage();
const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount);
const { noticeStore, authStore } = useStore();
const { noticeList } = noticeStore;
const [noticeList, setNoticeList] = useState([]);
useEffect(() => {
// console.info("notice.useEffect", authStore.login.userId);
fetchBulletinList(userId).then(data => {
setNoticeList(data);
});
getBulletinUnReadCount(userId); //
console.info("notice.useEffect");
noticeStore.getBulletinList(authStore.login.userId);
noticeStore.getBulletinUnReadCount(authStore.login.userId); //
}, []);
return (
@ -24,7 +26,7 @@ function Index() {
<Col span={4}></Col>
<Col span={16}>
<List
dataSource={(noticeList)}
dataSource={toJS(noticeList)}
renderItem={item => (
<List.Item>
<Typography.Text>[{item.CCP_LastEditTime}]</Typography.Text>
@ -38,4 +40,4 @@ function Index() {
);
}
export default (Index);
export default observer(Index);

@ -1,200 +0,0 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { App, Empty, Button, Collapse, Table, Space } from 'antd';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
import { useTranslation } from 'react-i18next';
import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index';
import { cloneDeep, isEmpty, 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';
const PriceTable = ({ productType, dataSource, refresh }) => {
const { t } = useTranslation('products');
const { travel_agency_id, use_year, audit_state } = useParams();
const isPermitted = useAuthStore(state => state.isPermitted);
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
const { message, notification } = App.useApp();
const stateMapVal = useProductsAuditStatesMapVal();
const [renderData, setRenderData] = useState(dataSource);
// console.log(dataSource);
const handleAuditPriceItem = (state, row, rowIndex) => {
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => {
if (json.errcode === 0) {
message.success(json.errmsg);
if (typeof refresh === 'function') {
// refresh(); // debug: ,
// const newData = structuredClone(renderData);
const newData = cloneDeep(renderData);
newData.splice(rowIndex, 1, {...row, audit_state_id: state, });
setRenderData(newData);
}
}
})
.catch((ex) => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
});
});
};
const rowStyle = (r, tri) => {
const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; //
const [infoI, quoteI] = r.rowSpanI;
const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : ''; // 线
const editedCls = (r.audit_state_id <= 0 && isNotEmpty(r.lastedit_changed)) ? '!bg-red-100' : ''; // <=, :
const editedCls_ = isNotEmpty(r.lastedit_changed) ? (r.audit_state_id === 0 ? '!bg-red-100' : '!bg-sky-100') : '';
return [trCls, bigTrCls, editedCls].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 || '';
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit` : isPermitted(PERM_PRODUCTS_OFFER_PUT) ? `/products/edit` : '';
return isNotEmpty(itemLink) ? <span onClick={() => setEditingProduct({info: r.info})}><Link to={itemLink} >{title}</Link></span> : title;
} },
// ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []),
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
{ key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`,
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''),
},
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') },
{
key: 'state',
title: t('State'),
render: (_, r) => {
const stateCls = ` text-${stateMapVal[`${r.audit_state_id}`]?.color} `;
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
},
},
{
title: '',
key: 'action',
render: (_, r, ri) =>
(Number(r.audit_state_id)) === 0 ? (
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Space>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</Space>
</RequireAuth>
) : null,
},
];
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, }} dataSource={renderData} rowKey={(r) => r.id} />;
};
/**
*
*/
const TypesPanels = (props) => {
const { t } = useTranslation();
const [loading, agencyProducts] = useProductsStore((state) => [state.loading, state.agencyProducts]);
// console.log(agencyProducts);
const productsTypes = useProductsTypes();
const [activeKey, setActiveKey] = useState([]);
const [showTypes, setShowTypes] = useState([]);
useEffect(() => {
// ; , ; ,
const hasDataTypes = Object.keys(agencyProducts);
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => ({
...ele,
extra: t('Table.Total', { total: agencyProducts[ele.value].length }),
children: (
<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(', '),
info: c.info,
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}),
rowSpan: i === 0 ? c.quotation.length : 0,
rowSpanI: [ri, i],
}))
),
[]
)}
refresh={props.refresh}
/>
),
}));
setShowTypes(_show);
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
return () => {};
}, [productsTypes, agencyProducts]);
const onCollapseChange = (_activeKey) => {
setActiveKey(_activeKey);
};
return isEmpty(agencyProducts) ? <Empty /> : <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 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,
})
});
};
return (
<>
<SecondHeaderWrapper loading={loading} backTo={isPermitted(PERM_PRODUCTS_MANAGEMENT) ? `/products` : false} header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} />} >
{/* <PrintContractPDF /> */}
<TypesPanels refresh={handleGetAgencyProducts} />
</SecondHeaderWrapper>
</>
);
};
export default Audit;

@ -1,58 +0,0 @@
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 { 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';
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,
});
});
};
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>
);
}
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)}
destroyOnClose
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,166 +0,0 @@
import { useState, useEffect } from 'react';
import { App, Form, Modal, DatePicker, Divider, Switch } 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';
dayjs.extend(arraySupport);
export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const isPermitted = useAuthStore((state) => state.isPermitted);
useEffect(() => {
onFormInstanceReady(form);
}, []);
const onValuesChange = (changeValues, allValues) => {};
return (
<Form layout='horizontal' form={form} name='form_in_modal' initialValues={initialValues} onValuesChange={onValuesChange} >
{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>}
<Form.Item name={`products_types`} label={t('products:ProductType')} >
<ProductsTypesSelector maxTagCount={1} mode={'multiple'} placeholder={t('All')} />
</Form.Item>
{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={'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>
<Form.Item name={'with_quote'} label={`${t('products:CopyFormMsg.withQuote')}`}>
<Switch checkedChildren={'含报价金额'} unCheckedChildren={'仅人等+日期'} />
</Form.Item>
</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) },
};
let dest = {};
const { agency, year, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
}
}
// omit empty
// Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
return dest;
};
/**
*
*/
export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubmit, onCancel, initialValues, loading, copyModalVisible, setCopyModalVisible }) => {
const { t } = useTranslation();
const { notification, message } = App.useApp();
const [formInstance, setFormInstance] = useState();
const [copyLoading, setCopyLoading] = useState(false);
const handleCopyAgency = async (param) => {
param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency;
setCopyLoading(true);
// console.log(param);
// const toID = param.target_agency;
const success = await copyAgencyDataAction({...param, source_agency: source.sourceAgency.travel_agency_id}).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={600}
open={open}
title={`${t('Copy')}${t('products:#')}${t('products:Offer')}`}
okText='确认'
// cancelText='Cancel'
okButtonProps={{
autoFocus: true,
}}
confirmLoading={copyLoading}
onCancel={() => {
onCancel();
formInstance?.resetFields();
}}
destroyOnClose
onOk={async () => {
try {
const values = await formInstance?.validateFields();
// formInstance?.resetFields();
const dest = formValuesMapper(values);
handleCopyAgency(dest);
} catch (error) {
console.log('Failed:', error);
}
}}>
<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;

@ -1,176 +0,0 @@
import { useEffect, useState, useSyncExternalStore } from 'react';
import { useParams } from 'react-router-dom';
import { useTranslation } from 'react-i18next';
import { App, Table, Button, Modal, Popconfirm } from 'antd';
import { getAgencyProductExtrasAction, searchPublishedProductsAction, addProductExtraAction, delProductExtrasAction } from '@/stores/Products/Index';
import { cloneDeep, pick } from '@/utils/commons';
import SearchForm from '@/components/SearchForm';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
import { useProductsTypesMapVal } from '@/hooks/useProductsSets';
import { usingStorage } from '@/hooks/usingStorage';
import useProductsStore from '@/stores/Products/Index';
const NewAddonModal = ({ onPick, ...props }) => {
// 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);
const [loading, setLoading] = useState(false); // bind loading
const [searchLoading, setSearchLoading] = useState(false);
const [searchResult, setSearchResult] = useState([]);
const onSearchProducts = async (values) => {
const copyObject = cloneDeep(values);
const { starttime, endtime, year, ...param } = copyObject;
setSearchLoading(true);
setSearchResult([]);
const search_year = year || use_year;
const result = await searchPublishedProductsAction({ ...param, use_year: search_year, travel_agency_id });
setSearchResult(result);
setSearchLoading(false);
};
const handleAddExtras = async (item) => {
if (typeof onPick === 'function') {
onPick(item);
}
};
const searchResultColumns = [
{ key: 'ptype', dataIndex: 'type', width: '6rem', title: t('products:ProductType'), render: (text, r) => productsTypesMapVal[text]?.label || text },
{ key: 'code', dataIndex: 'code', width: '6rem', title: t('products:Code') },
{ key: 'title', dataIndex: 'title', width: '16rem', title: t('products:Title') },
// {
// title: t('products:price'),
// dataIndex: ['quotation', '0', 'adult_cost'],
// width: '10rem',
// render: (_, { quotation }) => `${quotation[0].adult_cost} ${quotation[0].currency} / ${quotation[0].unit_name}`,
// },
{
key: 'action',
title: '',
width: 150,
render: (_, record) => (
<Button className='text-primary' onClick={() => handleAddExtras(record)}>
绑定此项目
</Button>
),
},
];
const paginationProps = {
showTotal: (total) => t('Table.Total', { total }),
};
return (
<>
<Button type='primary' onClick={() => setOpen(true)} className='mt-2'>
{t('New')} {t('products:EditComponents.Extras')}
</Button>
<Modal width={'95%'} style={{ top: 20 }} open={open} title={'添加附加'} footer={false} onCancel={() => setOpen(false)} destroyOnClose>
<SearchForm
fieldsConfig={{
shows: ['year', 'keyword', 'products_types', 'city'], // 'dates',
fieldProps: {
year: { rules: [{ required: true }] },
keyword: { label: t('products:Title'), col: 4 },
},
// sort: { keyword: 100 },
}}
initialValue={
{
// dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
// year: dayjs().add(1, 'year'),
}
}
onSubmit={(err, formVal, filedsVal) => {
onSearchProducts(formVal);
}}
/>
<Table
size={'small'}
key={'searchProductsTable'}
rowKey={'id'}
loading={searchLoading}
dataSource={searchResult}
columns={searchResultColumns}
pagination={searchResult.length <= 10 ? false : paginationProps}
/>
</Modal>
</>
);
};
/**
*
*/
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 [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 });
setExtrasData(data);
};
const handleNewAddOn = async (item) => {
// setExtrasData(prev => [].concat(prev, [item]));
// todo: ;
const _item = pick(item, ['id', 'title', 'code']);
const newSuccess = await addProductExtraAction({ travel_agency_id, id: productId, extras: [_item] });
newSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
await handleGetAgencyProductExtras();
}
const handleDelAddon = async (item) => {
const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, del_extras_ids: [item.id] });
delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
await handleGetAgencyProductExtras();
};
useEffect(() => {
if (productId) handleGetAgencyProductExtras();
return () => {};
}, [productId]);
const columns = [
{ title: t('products:Title'), dataIndex: ['info', 'title'], width: '16rem', },
{
title: '',
dataIndex: 'operation',
width: '4rem',
render: (_, r) => (
<Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r.info)} okText={t('Yes')} >
<Button size='small' type='link' danger>
{t('Delete')}
</Button>
</Popconfirm>
),
},
];
return (
<>
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
<h2>{t('products:EditComponents.Extras')}</h2>
<Table dataSource={extrasData} columns={columns} bordered pagination={false} rowKey={(r) => r.info.id} />
<NewAddonModal onPick={handleNewAddOn} />
</RequireAuth>
</>
);
};
export default Extras;

@ -1,312 +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,
getAgencyAllExtrasAction,
} from "@/stores/Products/Index";
import { isEmpty, objectMapper } from "@/utils/commons";
import useAuthStore from "@/stores/Auth";
import RequireAuth from "@/components/RequireAuth";
// import PrintContractPDF from './PrintContractPDF';
import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from "@/config";
import dayjs from "dayjs";
import VendorSelector from "@/components/VendorSelector";
import AuditStateSelector from "@/components/AuditStateSelector";
import { usingStorage } from "@/hooks/usingStorage";
import AgencyContract from "../Print/AgencyContract";
// import AgencyContract from "../Print/AgencyContract_v0903";
import { saveAs } from "file-saver";
import { Packer } from "docx";
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 [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
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 { getRemarkList } = useProductsStore((selector) => ({
getRemarkList: selector.getRemarkList,
}));
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,
});
});
};
const handleDownload = async () => {
// await refresh();
const agencyExtras = await getAgencyAllExtrasAction(switchParams);
const remarks = await getRemarkList()
const documentCreator = new AgencyContract();
const doc = documentCreator.create([
switchParams,
activeAgency,
agencyProducts,
agencyExtras,
remarks
]);
const _d = dayjs().format("YYYYMMDD_HH.mm.ss.SSS"); // Date.now().toString(32)
Packer.toBlob(doc).then((blob) => {
saveAs(
blob,
`${activeAgency.travel_agency_name}${pickYear}年地接合同-${_d}.docx`
);
});
};
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>
{/* todo: export, 审核完成之后才能导出 */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size="small" onClick={handleDownload}>
{t("Export")} .docx
</Button>
{/* <PrintContractPDF /> */}
</RequireAuth>
{/* {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}/${audit_state}/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,150 +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, pick } 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('');
return () => {};
}, [activeAgency, editingProduct]);
const [infoEditStatus, setInfoEditStatus] = useState('');
const [lgcEdits, setLgcEdits] = useState({});
const onValuesChange = (changedValues, forms) => {
// console.log('onValuesChange', changedValues);
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 {
setInfoEditStatus('2');
}
};
const onSave = async (err, values, forms) => {
values.travel_agency_id = activeAgency.travel_agency_id;
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": "",
"edit_status": infoEditStatus || editingProduct.info.edit_status,
};
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', '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
// return false; // debug: 0
/** 提交保存 */
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,434 +0,0 @@
import { useEffect, useState } from 'react';
import { App, Form, Input, Row, Col, Select, Button, InputNumber, Checkbox } from 'antd';
import { objectMapper, isEmpty, isNotEmpty } 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 } 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();
form.setFieldValue('city', editingProduct?.info?.city_id ? { value: editingProduct?.info?.city_id, label: editingProduct?.info?.city_name } : undefined);
form.setFieldValue('dept', { value: editingProduct?.info?.dept_id, label: editingProduct?.info?.dept_name });
const lgc_details_mapped = (editingProduct?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
form.setFieldValue('lgc_details_mapped', lgc_details_mapped);
form.setFieldValue('quotation', editingProduct?.quotation);
form.setFieldValue('display_to_c', editingProduct.info?.display_to_c || '0');
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 = formValuesMapper(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 = formValuesMapper(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}
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(
'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 {...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 || midCol
),
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(
'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 formValuesMapper = (values) => {
const destinationObject = {
'city': [
{ key: 'city_id', transform: (value) => value?.value || value?.key || '' },
{ key: 'city_name', transform: (value) => value?.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' },
};
let dest = {};
const { city, dept, product_title, ...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,495 +0,0 @@
import { useState } from 'react'
import { Table, Form, Modal, Button, Radio, Input, Flex, Card, InputNumber, Checkbox, DatePicker, Space, App, Tooltip } from 'antd'
import { useTranslation } from 'react-i18next'
import { CloseOutlined, StarTwoTone, PlusOutlined, ExclamationCircleFilled, 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'
const { RangePicker } = DatePicker
const batchSetupInitialValues = {
'defList': [
//
{
'useDateList': [
{
'useDate': [
dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')
]
}
],
'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().add(1, 'year').subtract(2, 'M').startOf('M'), dayjs().add(1, '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 defaultPriceValue = {
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
}
const defaultUseDate = {
'useDate': [dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')]
}
const defaultDefinitionValue = {
'useDateList': [defaultUseDate],
'unitId': '0',
'currency': 'RMB',
'weekend': [],
'priceList': [defaultPriceValue]
}
const ProductInfoQuotation = ({ editable, ...props }) => {
const { onChange } = props
const { t } = useTranslation()
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false)
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false)
const { modal, notification } = App.useApp()
const [quotationForm] = Form.useForm()
const [batchSetupForm] = Form.useForm()
const datePresets = useDatePresets()
const [quotationList, newEmptyQuotation, appendQuotationList, saveOrUpdateQuotation, deleteQuotation] =
useProductsStore((state) => [state.quotationList, state.newEmptyQuotation, state.appendQuotationList, state.saveOrUpdateQuotation, state.deleteQuotation])
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 = () => {
const emptyQuotation = newEmptyQuotation()
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 onDeleteQuotation = (quotation) => {
modal.confirm({
title: '请确认',
icon: <ExclamationCircleFilled />,
content: '你要删除这条价格吗?',
onOk() {
deleteQuotation(quotation)
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
},
})
}
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' overlayInnerStyle={{width: '24rem'}} 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) => `${record.group_size_min}-${record.group_size_max}`,
},
{
title: (<>{t('products:use_dates')} <Tooltip placement='top' overlayInnerStyle={{width: '24rem'}} title={t('products:FormTooltip.UseDates')}><QuestionCircleOutlined className='text-gray-500' /></Tooltip> </>),
dataIndex: 'use_dates',
// width: '6rem',
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) => {
// const _rowEditable = [-1,3].includes(quotation.audit_state_id);
const _rowEditable = true; // test: 0
return (
<Space>
<Button type='link' disabled={!_rowEditable} onClick={() => onQuotationSeleted(quotation)}>{t('Edit')}</Button>
<Button type='link' danger disabled={!_rowEditable} onClick={() => onDeleteQuotation(quotation)}>{t('Delete')}</Button>
</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)}
destroyOnClose
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(defaultUseDate)} 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('account:detail')}
open={isQuotationModalOpen} onCancel={() => setQuotationModalOpen(false)}
destroyOnClose
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>
<Form.Item
label={t('products:group_size')}
name='group_size_min'
rules={[
{
required: true,
message: t('products:Validation.group_size_min'),
},
]}
>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label={t('products:group_size')}
name='group_size_max'
rules={[
{
required: true,
message: t('products:Validation.group_size_max'),
},
]}
>
<InputNumber 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,179 +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, 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([]);
const [rawTreeData, setRawTreeData] = useState([]);
const [flattenTreeData, setFlattenTreeData] = useState([]);
const [expandedKeys, setExpandedKeys] = 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] || []).map((product) => {
const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {});
return {
// title: product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || '',
title: `${product.info.city_name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || ''),
// key: `${ele.value}-${product.info.id}`,
key: product.info.id,
_raw: product,
isLeaf: true,
}}),
// ``
// _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]);
const [searchValue, setSearchValue] = useState('');
const onSearch = ({ target: { value } }) => {
// const { value } = e.target;
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 handleNodeSelect = (selectedKeys, { node }) => {
if (node._raw) {
setEditingProduct(node._raw);
} 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={[editingProduct?.info?.id || editingProduct?.info?.product_type_id]}
switcherIcon={<CaretDownOutlined />}
onSelect={handleNodeSelect}
treeData={treeData}
expandedKeys={expandedKeys}
autoExpandParent={autoExpandParent}
onExpand={onExpand}
titleRender={titleRender}
/>
</div>
);
};
export default ProductsTree;

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

Loading…
Cancel
Save