Compare commits

..

1 Commits

Author SHA1 Message Date
Lei OT 5388c8783a perf: report index修改表格; 修改批评表; 1 year ago

@ -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,80 +7,24 @@ 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
dayjs https://dayjs.gitee.io/docs/zh-CN/manipulate/start-of
antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
反馈表案例 https://www.chinahighlights.com/customerservice/feedback/PostTourSurveyFormToWLGH.asp?LGC=1&COLI_SN=988185&MEI_SN=954295&Email=jennroth18@hotmail.com&ToC=0&ShowType=&page_class=4&dei_sn=28&country=30,490
国内供应商平台 http://p.mycht.cn/index.aspx
文档预览 https://github.com/cyntler/react-doc-viewer
wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start
pdf生成 https://github.com/ivmarcos/react-to-pdf
react-pdf https://react-pdf.org
生成Docx文档 https://docx.js.org/#/?id=welcome
需求文档 https://www.kdocs.cn/l/csZrIZlpuF2i
dayjs https://dayjs.gitee.io/docs/zh-CN/manipulate/start-of
antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
反馈表案例 https://www.chinahighlights.com/customerservice/feedback/PostTourSurveyFormToWLGH.asp?LGC=1&COLI_SN=988185&MEI_SN=954295&Email=jennroth18@hotmail.com&ToC=0&ShowType=&page_class=4&dei_sn=28&country=30,490
国内供应商平台 http://p.mycht.cn/index.aspx
文档预览 https://github.com/cyntler/react-doc-viewer
wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start
pdf生成 https://github.com/ivmarcos/react-to-pdf
react-pdf https://react-pdf.org
## 阿里云OSS
Bucket 名称global-highlights-hub
Endpointoss-cn-hongkong.aliyuncs.com
global-highlights-hub.oss-cn-hongkong.aliyuncs.com
反馈表测试链接
http://202.103.68.111:5173/feedback/330948
---
Bucket 名称global-highlights-hub
Endpointoss-cn-hongkong.aliyuncs.com
global-highlights-hub.oss-cn-hongkong.aliyuncs.com
```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,96 +0,0 @@
use Tourmanager
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_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('采购年份', 'route=/products/pick-year', 'page')
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1)

Binary file not shown.

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Highlights Hub</title>
<title>Global Highlights Hub</title>
<style>
.loading{width:150px;height:8px;border-radius:4px;margin:0 auto;margin-top:200px;position:relative;background:#777;overflow:hidden}
.loading span{display:block;width:100%;height:100%;border-radius:3px;background:#00b96b;animation:changePosition 4s linear infinite}
@ -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,48 +1,28 @@
{
"name": "global-highlights-hub",
"name": "global.highlights.hub",
"private": true,
"version": "2.2.5",
"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.27.0",
"dayjs": "^1.11.13",
"docx": "^8.5.0",
"file-saver": "^2.0.5",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^14.1.2",
"react-router-dom": "^6.30.1",
"react-to-pdf": "^1.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^4.5.7"
"antd": "^5.4.2",
"mobx": "^6.9.0",
"mobx-react": "^7.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"react-router-dom": "^6.10.0",
"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: {},
},
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -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"
}

@ -1,110 +0,0 @@
{
"lang": {
"en": "English",
"zh": "中文"
},
"Search": "Search",
"Reset": "Reset",
"Cancel": "Cancel",
"Submit": "Submit",
"Confirm": "Confirm",
"Close": "Close",
"Save": "Save",
"New": "New",
"Edit": "Edit",
"Audit": "Audit",
"Delete": "Delete",
"Add": "Add",
"View": "View",
"Back": "Back",
"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"
},
"operator": "Operator",
"time": "Time",
"Login": "Login",
"Username": "Username",
"Realname": "Realname",
"Password": "Password",
"ChangePassword": "Change password",
"Profile": "Profile",
"Logout": "Logout",
"LoginTimeout": "Login timeout",
"LoginTimeoutTip": "Please input your password",
"userProfile": "User Profile",
"Telephone": "Telephone",
"Email": "Email address",
"Address": "Address",
"Company": "Company",
"Department": "Department",
"datetime": {
"thisWeek": "This Week",
"lastWeek": "Last Week",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"nextMonth": "Next Month",
"lastThreeMonth": "Last Three Month",
"nextThreeMonth": "Next Three Month",
"firstHalfYear": "First Half Year",
"latterHalfYear": "Latter Half Year",
"thisYear": "This Year"
},
"weekdays": {
"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"
}
}

@ -1,17 +0,0 @@
{
"ArrivalDate": "Arrival Date",
"RefNo": "Reference number",
"unconfirmed": "Unconfirmed",
"Pax": "Pax",
"Status": "Status",
"City": "City",
"Guide": "Guide",
"ResSendingDate": "Res. sending date",
"3DGuideTip": "Reservations without the tour guide information will be highlighted in red if the arrival date is within 3 days.",
"Attachments": "Attachments",
"ConfirmationDate": "Confirmation Date",
"ConfirmationDetails": "Confirmation Details",
"PNR": "PASSAGER NAME RECORD",
"#": "#"
}

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

@ -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": "权限"
}

@ -1,110 +0,0 @@
{
"lang": {
"en": "English",
"zh": "中文"
},
"Search": "查询",
"Reset": "重置",
"Cancel": "取消",
"Submit": "提交",
"Confirm": "确认",
"Close": "关闭",
"Save": "保存",
"New": "新增",
"Edit": "编辑",
"Audit": "审核",
"Delete": "删除",
"Add": "添加",
"View": "查看",
"Back": "返回",
"Download": "下载",
"Upload": "上传",
"Preview": "预览",
"Total": "总数",
"Action": "操作",
"Import": "导入",
"Export": "导出",
"Copy": "复制",
"sureCancel": "确定取消?",
"sureDelete": "确定删除?",
"sureSubmit": "确定提交?",
"Yes": "是",
"No": "否",
"Success": "成功",
"Failed": "失败",
"All": "所有",
"Table": {
"Total": "共 {{total}} 条"
},
"operator": "操作",
"time": "时间",
"Login": "登录",
"Username": "账号",
"Realname": "姓名",
"Password": "密码",
"ChangePassword": "修改密码",
"Profile": "账户中心",
"Logout": "退出",
"LoginTimeout": "登录超时",
"LoginTimeoutTip": "请输入密码",
"userProfile": "账号信息",
"Telephone": "联系电话",
"Email": "电子邮箱",
"Address": "公司地址",
"Company": "公司名称",
"Department": "部门",
"datetime": {
"thisWeek": "本周",
"lastWeek": "上周",
"thisMonth": "本月",
"lastMonth": "上月",
"nextMonth": "下月",
"lastThreeMonth": "前三个月",
"nextThreeMonth": "后三个月",
"firstHalfYear": "上半年",
"latterHalfYear": "下半年",
"thisYear": "今年"
},
"weekdays": {
"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": "已打款"
}
}

@ -1,16 +0,0 @@
{
"ArrivalDate": "抵达日期",
"RefNo": "团号",
"unconfirmed": "未确认",
"Pax": "人数",
"Status": "状态",
"City": "城市",
"Guide": "导游",
"ResSendingDate": "发送时间",
"3DGuideTip": "红色突出显示:抵达日期在 3 天内,没有导游信息的预订。",
"Attachments": "附件",
"ConfirmationDate": "确认日期",
"ConfirmationDetails": "确认信息",
"PNR": "旅客订座记录",
"#": "#"
}

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

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

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

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

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

@ -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 maxTagCount={1} {...fieldComProps.agency} />
</Form.Item>,
fieldProps?.agency?.col || 6
),
item(
"audit_state", // : /
99,
<Form.Item name={`audit_state`} initialValue={at(props, "initialValue.audit_state")[0] || { value: "", label: "Status" }}>
<AuditStateSelector {...fieldComProps.audit_state} />
</Form.Item>,
fieldProps?.audit_state?.col || 3
),
item(
"airticket_state", //-1 0 1
99,
<Form.Item name="airticket_state" label="出票状态" initialValue={at(props, "initialValue.airticket_state")[0] || { value: "-1", label: "所有" }}>
<Select
labelInValue
options={[
{ value: "-1", label: "所有" },
{ value: "0", label: "未出票" },
{ value: "1", label: "已出票" },
]}
/>
</Form.Item>,
fieldProps?.airticket_state?.col || 4
),
item(
"plan_state", //-1 0123
99,
<Form.Item name="plan_state" label="计划状态" initialValue={at(props, "initialValue.plan_state")[0] || { value: "-1", label: "所有" }}>
<Select
labelInValue
options={[
{ value: "-1", label: "所有" },
{ value: "0", label: "新计划" },
{ value: "1", label: "已确认" },
{ value: "2", label: "有变更" },
{ value: "3", label: "已取消" },
]}
/>
</Form.Item>,
fieldProps?.plan_state?.col || 4
),
item(
"products_types", //
99,
<Form.Item name={`products_types`} label={t("products:ProductType")} {...fieldProps.products_types} initialValue={at(props, "initialValue.products_types")[0] || undefined}>
<ProductsTypesSelector maxTagCount={1} {...fieldComProps.products_types} />
</Form.Item>,
fieldProps?.products_types?.col || 6
),
item(
"dept", //
99,
<Form.Item name={`dept`} label={t("products:Dept")} {...fieldProps.dept} initialValue={at(props, "initialValue.dept")[0] || undefined}>
<DeptSelector {...fieldComProps.dept} />
</Form.Item>,
fieldProps?.dept?.col || 6
),
item(
"city", //
99,
<Form.Item name={`city`} label={t("products:City")} {...fieldProps.city} initialValue={at(props, "initialValue.city")[0] || undefined}>
<CitySelector {...fieldComProps.city} />
</Form.Item>,
fieldProps?.city?.col || 4
),
item(
"unconfirmed", //
99,
<Form.Item name={`unconfirmed`} valuePropName="checked" initialValue={at(props, "initialValue.unconfirmed") || false}>
<Checkbox>{t("group:unconfirmed")}</Checkbox>
</Form.Item>,
fieldProps?.unconfirmed?.col || 2
),
];
baseChildren = baseChildren
.map(x => {
x.hide = false;
if (props.sort === undefined) {
return x;
}
const tmpSort = props.sort;
for (const key in tmpSort) {
if (Object.prototype.hasOwnProperty.call(tmpSort, key)) {
if (x.name === key) {
x.sort = tmpSort[key];
}
}
}
return x;
})
.map(x => {
if (props.hides.length === 0 && props.shows.length === 0) {
return x;
}
if (props.hides.length === 0) {
x.hide = !props.shows.includes(x.name);
} else if (props.shows.length === 0) {
x.hide = props.hides.includes(x.name);
}
return x;
})
.filter(x => !x.hide)
.sort((a, b) => {
return a.sort < b.sort ? -1 : 1;
});
const children = [];
const leftStyle = {}; // { borderRight: '1px solid #dedede' };
for (let i = 0; i < baseChildren.length; i++) {
let style = {}; // { padding: '0px 2px' };
style = i % 2 === 0 && baseChildren[i].col === 12 ? { ...style, ...leftStyle } : style;
style = !baseChildren[i].hide ? { ...style, display: "block" } : { ...style, display: "none" };
const Item = (
<Col key={String(i)} style={style} {...layoutProps} {...baseChildren[i].col}>
{baseChildren[i].render}
</Col>
);
children.push(Item);
}
return children;
}
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,52 +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 HT3_HOST = 'https://hub.globalhighlights.com/ht3.0'
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, '')
const __BUILD_DATE__ = `__BUILD_DATE__`;
const __GIT_HEAD__ = `__GIT_HEAD__`
export const BUILD_VERSION = import.meta.env.PROD ? __BUILD_VERSION__ : import.meta.env.MODE;
export const BUILD_DATE = import.meta.env.PROD ? __BUILD_DATE__ : new Date().toLocaleString();
export const GIT_HEAD = import.meta.env.PROD ? __GIT_HEAD__ : 'current';
// 权限常量定义
// 账号、权限管理
// 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'; // 信息.审核 @deprecated
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;
};

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

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

@ -1,214 +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 = () => {
const stateSets = useProductsAuditStates();
const stateMapVal = stateSets.reduce((r, c) => ({ ...r, [`${c.value}`]: c }), {});
return stateMapVal;
};
/**
*
*/
export const useProductsTypesFieldsets = (type) => {
const [isPermitted] = useAuthStore((state) => [state.isPermitted]);
const infoDefault = [['city', 'city_list'], ['title']];
const infoAdmin = ['title', 'product_title', 'code', 'remarks', 'dept']; // 'display_to_c'
const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : [];
const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : [];
const subTypeD = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sub_type_D'] : [];
const sortOrder = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sort_order'] : [];
const infoTypesMap = {
'6': [[...infoDisplay], []],
'B': [['km', ...infoDisplay], []],
'J': [[...infoRecDisplay, 'duration', ...infoDisplay, ...sortOrder], ['description']],
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay, ...sortOrder], ['description']],
'D': [[...infoRecDisplay, 'duration', ...infoDisplay, ...subTypeD, ...sortOrder], ['description']],
'7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']],
'R': [[...infoDisplay], ['description']],
'8': [[...infoDisplay], []],
};
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, // 信息的审核状态 1已发布2已审核
'sort_order': '',
'sub_type_D': '', // 包价类型, 值保存在`item_type`字段中
'item_type': '', // 产品子类型的值
'city_list': [],
},
lgc_details: [
{
'title': '',
'descriptions': '',
'lgc': 1,
'id': '',
'edit_status': 2,
},
],
quotation: [
{
'id': '',
'adult_cost': 0,
'child_cost': 0,
'currency': 'RMB',
'unit_id': '1',
'unit_name': '每团',
'group_size_min': 1,
'group_size_max': 2,
'use_dates_start': '',
'use_dates_end': '',
'weekdays': '',
'audit_state_id': -1,
'audit_state_name': '',
'lastedit_changed': 'new',
},
],
};
};
export const PackageTypes = [
{ key: '35001', value: '35001', label: '飞机接送' },
{ key: '35002', value: '35002', label: '车站接送' },
{ key: '35003', value: '35003', label: '码头接送' },
{ key: '35004', value: '35004', label: '一天游' },
{ key: '35005', value: '35005', label: '半天游' },
{ key: '35006', value: '35006', label: '夜间活动' },
{ key: '35007', value: '35007', label: '大车游' },
{ key: '35008', value: '35008', label: '单车单导' },
{ key: '35009', value: '35009', label: '单租车' },
{ key: '35010', value: '35010', label: '单导游' },
{ key: '35011', value: '35011', label: '火车站接送' },
{ key: '35012', value: '35012', label: '门票预定' },
{ key: '35013', value: '35013', label: '车导费' },
{ key: '35014', value: '35014', label: '其它(餐补等)' },
];
export const formatGroupSize = (min, max, suffix = false) => {
return max === 1000 ? min <= 1 ? '不分人等' : `${min}人以上` : (`${min}-${max}`+(suffix ? '人' : ''));
};

@ -1,98 +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 getValue = (key) => {
if (window.localStorage) {
return window.localStorage.getItem(key)
} else if (window.sessionStorage) {
return window.sessionStorage.getItem(key)
} else {
console.error('browser not support localStorage and sessionStorage.')
return ''
}
}
const setProperty = (key, value) => {
const webStorage = getStorage()
const typeAndKey = key.split(':')
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,
getValue,
setStorage: (key, value) => {
setProperty(key, value)
},
clearStorage: () => {
getStorage().clear()
Object.assign(persistObject, {})
}
}
}

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

@ -1,56 +0,0 @@
import i18n from 'i18next';
// 用于检测浏览器中的用户语言,
// https://github.com/i18next/i18next-browser-languageDetector
// 通过localStorage.getItem('i18nextLng')取出当前语言环境
import LanguageDetector from 'i18next-browser-languagedetector';
import Backend from 'i18next-http-backend';
import { initReactI18next } from 'react-i18next';
// import en from './locales/en.json';
// import zh from './locales/zh.json';
i18n
.use(Backend)
.use(LanguageDetector)
.use(initReactI18next)
// https://www.i18next.com/overview/configuration-options
.init({
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'group', 'vendor', 'account', 'products'],
defaultNS: 'common',
detection: {
// convertDetectedLanguage: 'Iso15897',
convertDetectedLanguage: (lng) => {
const langPart = lng.split('-')[0];
return langPart;
// return lng.replace('-', '_');
},
},
supportedLngs: ['en', 'zh'],
// resources: {
// en: { translation: en },
// zh: { translation: zh },
// },
fallbackLng: 'en',
// fallbackLng: (code) => {
// if (!code || code === 'en') return ['en'];
// const fallbacks = []; // [code];
// // add pure lang
// const langPart = code.split('-')[0];
// if (langPart !== code) fallbacks.push(langPart);
// fallbacks.push('en');
// console.log('fallbacks', fallbacks);
// return fallbacks;
// },
preload: ['en'],
interpolation: {
escapeValue: false,
},
// keySeparator: false,
debug: false,
});
export default i18n;

@ -1,46 +0,0 @@
## 目录结构
├── locales
│ ├── en
│ │ ├── common.json
│ │ ├── [ns].json
│ ├── zh
│ │ ├── common.json
│ │ ├── [ns].json
1. 翻译的资源文件位于 public/locales 文件夹,文件夹结构是 [语言] > [命名空间].json.
[语言]命名使用短格式, 暂不考虑区分地区差异. 如: en-GB, en-US, en-AU 均使用`en`
2. common.json 是公共的 json 文件,存放一些公共的 key-value
3. [ns].json 是命名空间 json 文件,存放一些命名空间的 key-value
4. 命名空间根据自己的需求/功能模块来创建,比如:`group`(团计划), `feedback`(反馈表)等
5. json 文件中的 key 使用驼峰命名
⭐DEV
1. 翻译的资源文件位于 public 下, 切换语言时异步请求.
2. 开发过程编辑了 locales/\*.json 后, 需要刷新页面才会生效. 热加载不生效
3. 新增的命名空间, 添加到 src/i18n/index.js 中配置的 `init({ ns })`
⭐⭐ 组件中使用
1. 默认 common 命名空间
2. 取值: `t('[ns]:[json_path]')`, json path 的层级使用`.`分隔
```js
import { useTranslation } from 'react-i18next';
const { t } = useTranslation();
// const { t, i18n } = useTranslation('group'); // 指定命名空间
// 默认common命名空间
<button >{t('Search')}</button>
<button >{t('datetime.thisWeek')}</button>
// 命名空间取值
<button >{t('group:ArrivalDate')}</button>
```
⭐⭐⭐ 格式: 日期, 金额等
[文档 Formatting](https://www.i18next.com/translation-function/formatting)

@ -1,145 +1,85 @@
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'
} 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 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'
configure({
useProxies: "ifavailable",
enforceActions: "observed",
computedRequiresReaction: true,
observableRequiresReaction: false,
reactionRequiresObservable: true,
disableErrorBoundaries: process.env.NODE_ENV == "production"
});
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import ImageViewer from '@/views/ImageViewer';
import CustomerImageViewer from '@/views/CustomerImageViewer';
import PickYear from './views/products/PickYear'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_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>},
{ path: "products/pick-year",element: <RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><PickYear /></RequireAuth>},
//
]
},
{
element: <Standlone />,
children: [
{ path: '/login', element: <Login /> },
{ path: '/logout', element: <Logout /> },
{ path: '/image-viewer/:GRI_SN/:GRI_No', element: <ImageViewer /> },
{ path: '/customer-image/:key', element: <CustomerImageViewer /> },
]
}
])
}
const initAppliction = async () => {
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await useAuthStore.getState().initAuth()
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')
const rootStore = new RootStore();
if (!root) throw new Error('No root element found')
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
future={{
v7_startTransition: true,
}}
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</ThemeContext.Provider>
//</React.StrictMode>
)
}
initAppliction()
</StoreContext.Provider>
//</React.StrictMode>
);

@ -1,106 +0,0 @@
import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } from '@/config';
import { fetchJSON } from '@/utils/request'
import { usingStorage } from "@/hooks/usingStorage";
export const sendNotify = async (message) => {
const { userId, travelAgencyId } = usingStorage();
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup';
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: `${message}\\n\\nID: ${userId}, ${travelAgencyId} | ${PROJECT_NAME} (${BUILD_VERSION})`,
};
return fetchJSON(notifyUrl, params).then((json) => {
if (json.errcode === 0) {
console.info('发送通知成功');
} else {
throw new Error(json?.errmsg + ': ' + json.errcode);
}
});
};
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);
window.onerror = async function (msg, url, lineNo, columnNo, error) {
// iframe rrweb Cannot set property attributeName of #<MutationRecord> which has only a getter
//
if (url && url.indexOf('https://page-spy.mycht.cn/plugin/rrweb/index.min.js') > -1) {
console.info('ignore rrweb error')
} else {
// 3
const now = Date.now()
await window.$harbor.uploadPeriods({
startTime: now - 3 * 60000,
endTime: now,
remark: `\`onerror\`自动上传. ${msg}`,
})
}
}
});
};
export const uploadPageSpyLog = async () => {
if (import.meta.env.DEV) return true;
if (window.$pageSpy) {
try {
// await window.$harbor.upload() // { clearCache: true, remark: '' }
// 1 , upload : 413 Payload Too Large
const now = Date.now();
await window.$harbor.uploadPeriods({
startTime: now - 60 * 60000,
endTime: now,
});
return true;
} catch (error) {
return false;
}
} else {
return false;
}
}
/**
* @deprecated
* @outdated
*/
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,186 +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'
import { isEmpty } from "@/utils/commons";
const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN'
const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID'
const KEY_USER_ID = 'G-INT:USER_ID'
const KEY_I18N = 'i18nextLng'
const WILDCARD_TOKEN = '*'
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
}
const initialState = {
loginStatus: 0,
defaltRoute: '',
currentUser: {
username: '',
realname: '',
rolesName: '',
emailAddress: '',
travelAgencyName: '',
},
permissionList: []
}
const useAuthStore = create(devtools((set, get) => ({
...initialState,
initAuth: async () => {
const { loadUserPermission } = get()
const { setStorage, getValue, loginToken } = usingStorage()
const language = getValue(KEY_I18N)
appendRequestParams("lgc", language === "zh" ? 2 : 1)
// Dev 模式使用 localStorage会有 token 失效情况,需要手动删除
// Prod 环境没有该问题
const userJson = await fetchUserDetail(loginToken)
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: isEmpty(userJson.real_name) ? userJson.LoginName : userJson.real_name,
rolesName: userJson.roles_name,
emailAddress: userJson.LMI_listmail,
travelAgencyName: isEmpty(userJson.VName) ? userJson.LMI_VEI_SN : userJson.VName
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.LoginName}-${userJson.VName}`)
},
authenticate: async (usr, pwd) => {
const { initAuth } = get()
const { setStorage } = usingStorage()
const { token: loginToken } = await fetchLoginToken(usr, pwd)
}
setStorage(KEY_LOGIN_TOKEN, loginToken)
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();
}
}
await initAuth()
this.tokenInterval = setInterval(() => checkTokenTimeout(), 1000*60*20);
}
set(() => ({
loginStatus: 302
}))
},
logout() {
this.root.clearSession();
runInAction(() => {
this.login.timeout = true;
});
}
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]
}
tokenInterval = null;
set(() => ({
defaultRoute: deaultPage,
permissionList: permissionResult.map(p => p.res_pattern)
}))
},
logout: () => {
const { currentUser } = get()
const { clearStorage } = usingStorage()
clearStorage()
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
})
},
// 根据某项数据来判断是否有权限
//
// INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
// VALUES ('审核CH直销产品', '[125, 375]', 'data')
//
// const PERM_PRODUCTS_AUDIT_CH = '[125, 375]'
isAllowed: (perm, data) => {
return true
},
}), { name: 'authStore' }))
export default useAuthStore
});
}
VendorList = [];//海外供应商列表
login = {
token: '',
userId: 0, // LMI_SN
username: '0',
travelAgencyId: 0, // VEI_SN
travelAgencyName: '',
telephone: '',
emailAddress: '',
cityId: 0,
timeout: false
}
}
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.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,463 +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 }
}
/**
* 产品价格快照
*/
export const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
/**
* 修改产品的类型
*/
export const moveProductTypeAction = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_move`, params)
return errcode !== 0 ? [] : result;
};
const defaultRemarkList = [
{id: 0, "product_type_id": "6","Memo": ""},
{id: 0, "product_type_id": "B","Memo": ""},
{id: 0, "product_type_id": "J","Memo": ""},
{id: 0, "product_type_id": "Q","Memo": ""},
{id: 0, "product_type_id": "7","Memo": ""},
{id: 0, "product_type_id": "R","Memo": ""},
{id: 0, "product_type_id": "D","Memo": ""}
]
const initialState = {
loading: false,
searchValues: {}, // 客服首页: 搜索条件
agencyList: [], // 客服首页: 搜索结果
activeAgency: {}, // 审核/编辑 页: 当前的供应商
activeAgencyState: null,
agencyProducts: {}, // 审核/编辑 页: 供应商产品列表
editingProduct: {}, // 编辑页: 当前编辑的产品
quotationList: [], // 编辑页: 当前产品报价列表
editing: false,
switchParams: {}, // 头部切换参数
}
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: (useDates) => ({
id: null,
adult_cost: 0,
child_cost: 0,
currency: 'RMB',
unit_id: '0',
group_size_min: 1,
group_size_max: 10,
use_dates: useDates,
weekdayList: [],
fresh: true // 标识是否是新记录,新记录才用添加列表
}),
appendQuotationList: (defList) => {
const { activeAgency, editingProduct, quotationList } = get()
const generatedList = []
defList.forEach(definition => {
definition?.useDateList.map(useDateItem => {
const mappedPriceList = definition?.priceList.map(price => {
return {
id: null,
adult_cost: price.priceInput.audultPrice,
child_cost: price.priceInput.childrenPrice,
group_size_min: price.priceInput.numberStart,
group_size_max: price.priceInput.numberEnd,
currency: definition.currency,
unit_id: definition.unitId,
// 保持和 API 返回格式一致,日期要转换为字符串
use_dates_start: useDateItem.useDate[0].format('YYYY-MM-DD'),
use_dates_end: useDateItem.useDate[1].format('YYYY-MM-DD'),
weekdays: definition.weekend.join(','),
WPI_SN: editingProduct.info.id,
WPP_VEI_SN: activeAgency.travel_agency_id,
lastedit_changed: {},
audit_state_id: -1,
key: generateId(),
fresh: false
}
})
generatedList.push(...mappedPriceList)
})
})
const mergedList = [...quotationList,...generatedList]
set(() => ({
quotationList: mergedList
}))
return mergedList
},
saveOrUpdateQuotation: (formValues) => {
const { activeAgency, editingProduct, quotationList } = get()
let mergedList = []
formValues.WPI_SN = editingProduct.info.id
formValues.WPP_VEI_SN = activeAgency.travel_agency_id
formValues.use_dates_start = formValues.use_dates[0].format('YYYY-MM-DD')
formValues.use_dates_end = formValues.use_dates[1].format('YYYY-MM-DD')
formValues.weekdays = formValues.weekdayList.join(',')
if (formValues.fresh) {
formValues.key = generateId()
formValues.lastedit_changed = {}
formValues.audit_state_id = -1 // 新增,
formValues.fresh = false // 添加到列表后就不是新纪录,保存要修改原来记录
mergedList = [...quotationList,...[formValues]]
} else {
mergedList = quotationList.map(prevQuotation => {
if (prevQuotation.key === formValues.key) {
const changedObject = {}
for (const [key, value] of Object.entries(formValues)) {
if (key === 'use_dates' || key === 'id' || key === 'key' || key === 'weekdayList'
|| key === 'WPI_SN' || key === 'WPP_VEI_SN') continue
const preValue = prevQuotation[key]
const hasChanged = preValue !== value
if (hasChanged) {
changedObject[key] = preValue
}
}
return {
...prevQuotation,
audit_state_id: -1,
adult_cost: formValues.adult_cost,
child_cost: formValues.child_cost,
currency: formValues.currency,
unit_id: formValues.unit_id,
group_size_min: formValues.group_size_min,
group_size_max: formValues.group_size_max,
use_dates_start: formValues.use_dates_start,
use_dates_end: formValues.use_dates_end,
weekdays: formValues.weekdays,
lastedit_changed: changedObject
}
} else {
return prevQuotation
}
})
}
set(() => ({
quotationList: mergedList
}))
return mergedList
},
deleteQuotation: async(quotation) => {
const { editingProduct, quotationList, agencyProducts } = get()
const productTypeId = editingProduct.info.product_type_id;
const quotationId = quotation.id
const newQuotationList = quotationList.filter(q => {
return q.key != quotation.key
})
const newProductList = agencyProducts[productTypeId].map(p => {
if (p.info.id == editingProduct.info.id) {
return {
...editingProduct,
quotation: newQuotationList
}
} else {
return p
}
})
set({
agencyProducts: {
...agencyProducts,
[productTypeId]: newProductList
},
quotationList: newQuotationList
})
if (isNotEmpty(quotationId)) {
const { success } = await deleteQuotationAction(quotationId)
if (success) {
return Promise.resolve(newQuotationList)
} else {
return Promise.reject(newQuotationList)
}
}
},
// side effects
searchAgency: async (param) => {
const { setLoading, setAgencyList } = get();
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
}
})
set(() => ({
reservationDetail: {
referenceNumber: planDetail.GRI_Name,
tourGuide: planDetail.Guide,
arrivalDate: planDetail.eoi_getdate,
reservationId: reservationId
},
confirmationList: mapConfirmationList
}))
},
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)
});
}
const postUrl = HT_HOST + '/service-cusservice/PTConfirmPlanChange'
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';
return postForm(postUrl, formData)
.then(json => {
if (json.errcode == 0 && json.Result.length == 1) {
getReservationDetail(travelAgencyId, reservationDetail.reservationId)
return json
if (json.errcode != 0) {
throw new Error(json.errmsg + ': ' + json.errcode);
}
})
},
});
}
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'
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';
return postForm(postUrl, formData)
.then(json => {
if (json.errcode != 0) {
throw new Error(json.errmsg + ': ' + json.errcode)
if (json.errcode == 0 && json.Result.length == 1) {
this.fetchReservation(this.reservationDetail.reservationId);
return json;
}
})
},
});
}
editReservation(reservation) {
this.selectedReservation = reservation;
}
editConfirmation(confirmation) {
this.selectedConfirmation = confirmation;
}
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()
updatePropertyValue(name, value) {
runInAction(() => {
this[name] = value;
});
}
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(',')
cityList = [];
set((state) => ({
selectedReservation: {
...state.selectedReservation,
guide: reservationGuide,
},
}))
selectedReservation = null;
selectedConfirmation = null;
arrivalDateRange = [];
referenceNo = '';
return reservationGuide
} else {
throw new Error(json.errmsg + ': ' + json.errcode)
}
})
reservationList = [];
reservationDetail = {
referenceNumber: '', arrivalDate: '', tourGuide: ''
};
reservationPage = {
current: 1,
size: 10,
total: 0
}
}), { name: 'reservationStore' }))
confirmationList = [
];
}
export default useReservationStore
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,211 +0,0 @@
import { create } from "zustand";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST, DATE_FORMAT } from "@/config";
import dayjs from "dayjs";
const trainTicketStore = create((set, get) => ({
loading: false,
setLoading: loading => set({ loading }),
setPlanList: planList => set({ planList }),
setPlanDetail: planDetail => set({ planDetail }),
setGuestList: guestList => set({ guestList }),
setVEIFlightBill: vEIFlightBill => set({ vEIFlightBill }),
setVeiPlanChangeTxt: veiPlanChangeTxt => set({ veiPlanChangeTxt }),
async getPlanList(vei_sn, GRI_Name, TimeStart, TimeEnd, plan_state, airticket_state) {
const { setLoading, setPlanList } = get();
setLoading(true);
const searchParams = {
vei_sn: vei_sn,
FlightDate1: TimeStart,
FlightDate2: TimeEnd,
GRI_Name: GRI_Name,
FlightStatus: plan_state,
TicketIssued: airticket_state,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetTrainPlan`, searchParams);
const _result = errcode !== 0 ? [] : result;
setPlanList(_result);
setLoading(false);
},
async getPlanDetail(vei_sn, gri_sn) {
const { setPlanDetail } = get();
const searchParams = {
vei_sn: vei_sn,
gri_sn: gri_sn,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetTrainPlanDetail`, searchParams);
const _result = errcode !== 0 ? [] : result;
setPlanDetail(_result);
//return _result.filter(item => isNotEmpty(item.GRI_No));
return "dsadsd";
},
async postFlightDetail(CLF_SN, GRI_SN, VEI_SN, original_values, info_object) {
const formData = new FormData();
formData.append("CLF_SN", CLF_SN ? CLF_SN : "");
formData.append("GRI_SN", GRI_SN);
formData.append("VEI_SN", VEI_SN);
for (const [key, value] of Object.entries(original_values)) {
formData.append(key, value); //先用原始数据填充一遍,确保复制了全部数据到新表
}
for (const [key, value] of Object.entries(info_object)) {
formData.set(key, value); //再用新值覆盖
}
formData.set("StartDate", dayjs(info_object.StartDate).format(DATE_FORMAT)); //再用新值覆盖
//是否出票的值true、false变为1或0
formData.set("TicketIssued", info_object.TicketIssued ? 1 : 0);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_info";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//删除火车信息
async delete_flight_info(CLF_SN) {
const searchParams = {
CLF_SN: CLF_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_info`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
async getGuestList(coli_sn) {
const { setGuestList } = get();
const searchParams = {
COLI_SN: coli_sn,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightGuestInfo`, searchParams);
const _result = errcode !== 0 ? [] : result;
setGuestList(_result);
},
//获取账单列表
async getVEIFlightBill(VEI_SN, GRI_Name, CheckStatus, FlightDate1, FlightDate2) {
const { setLoading, setVEIFlightBill } = get();
setLoading(true);
const searchParams = {
VEI_SN: VEI_SN,
GRI_Name: GRI_Name,
CheckStatus: CheckStatus,
FlightDate1: FlightDate1,
FlightDate2: FlightDate2,
ServiceType: 2,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetVEIFlightBill`, searchParams);
const _result = errcode !== 0 ? [] : result;
setVEIFlightBill(_result);
setLoading(false);
},
//保存费用
async postFlightCost(values) {
const formData = new FormData();
for (const [key, value] of Object.entries(values)) {
formData.append(key, value); //先用原始数据填充一遍,确保复制了全部数据到新表
}
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_cost";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//删除费用
async deleteFlightCost(CLC_SN) {
const searchParams = {
CLC_SN: CLC_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_cost`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
//获取变更信息
async getVeiPlanChange(VEI_SN, GRI_SN) {
const { setVeiPlanChangeTxt } = get();
const searchParams = {
VEI_SN: VEI_SN,
GRI_SN: GRI_SN,
ServiceType: 2,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetVeiFlightPlanChange`, searchParams);
const _result = errcode !== 0 ? [] : result;
setVeiPlanChangeTxt(_result);
},
//提交变更确认
async postVeiFlightPlanConfirm(VEI_SN, GRI_SN, LMI_SN, ConfirmInfo) {
const formData = new FormData();
formData.append("VEI_SN", VEI_SN);
formData.append("GRI_SN", GRI_SN);
formData.append("LMI_SN", LMI_SN);
formData.append("ServiceType", 2);
formData.append("ConfirmInfo", ConfirmInfo);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/VeiFlightPlanConfirm";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//提交账单
async postVEIFlightBillSubmit(VEI_SN, values) {
const formData = new FormData();
formData.append("vei_sn", VEI_SN);
formData.append("ServiceType", 2);
formData.append("billdata", JSON.stringify(values));
const postUrl = HT_HOST + "/Service_BaseInfoWeb/VEIFlightBillSubmit";
return postForm(postUrl, formData).then(json => {
if (json.errcode == 0) {
return json;
} else {
throw new Error(json.errmsg + ": " + json.errcode);
}
});
},
//通知顾问查看车票信息
async ticketIssuedNotifications(LMI_SN, CLF_SN, OPI_SN, FlightMemo_messages,ticketImages) {
const searchParams = {
CLF_SN: CLF_SN,
OPI_SN: OPI_SN,
LMI_SN: LMI_SN,
ServiceType: 2,
FlightMemo_messages: FlightMemo_messages,
ticketImages: JSON.stringify(ticketImages),
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/TicketIssuedNotifications`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
// 定义座位表对象
seatTable: [
{ code: "9", name: "商务座", enName: "Business Class Seat" },
{ code: "P", name: "特等座", enName: "Superior Seat" },
{ code: "M", name: "一等座", enName: "First Class Seat" },
{ code: "O", name: "二等座", enName: "Second Class Seat" },
{ code: "6", name: "高级软卧", enName: "Superior Soft Sleeper" },
{ code: "5", name: "包厢硬卧", enName: "Hard Sleeper in Private Compartment" },
{ code: "4", name: "软卧", enName: "Soft Sleeper" },
{ code: "F", name: "动卧", enName: "High - Speed Train Sleeper" },
{ code: "3", name: "硬卧", enName: "Hard Sleeper" },
{ code: "2", name: "软座", enName: "Soft Seat" },
{ code: "1", name: "硬座", enName: "Hard Seat" },
{ code: "Q", name: "观光座", enName: "Sightseeing Seat" },
{ code: "H", name: "其它", enName: "Other" },
{ code: "WZ", name: "无座", enName: "Standing - room Only" },
{ code: "YDW", name: "一等卧", enName: "First - Class Sleeper" },
{ code: "EDW", name: "二等卧", enName: "Second - Class Sleeper" },
],
}));
export default trainTicketStore;

@ -82,10 +82,25 @@ export function isNotEmpty(val) {
return val !== undefined && val !== null && val !== "";
}
// export function isEmpty(val) {
// return val === undefined || val === null || val === "";
// }
export function prepareUrl(url) {
return new UrlBuilder(url);
}
// export function debounce(fn, delay = 500) {
// let timer;
// return e => {
// e.persist();
// clearTimeout(timer);
// timer = setTimeout(() => {
// fn(e);
// }, delay);
// };
// }
export function throttle(fn, delay, atleast) {
let timeout = null,
startTime = new Date();
@ -114,7 +129,7 @@ export function escape2Html(str) {
var output = temp.innerText || temp.textContent;
temp = null;
return output;
}
}
export function formatPrice(price) {
return Math.ceil(price).toLocaleString();
@ -124,6 +139,7 @@ export function formatPercent(number) {
return Math.round(number * 100) + "%";
}
/**
* ! 不支持计算 Set Map
* @param {*} val
@ -132,23 +148,23 @@ export function formatPercent(number) {
* false if: 'false', 'undefined'
*/
export function isEmpty(val) {
// return val === undefined || val === null || val === "";
return [Object, Array].includes((val || {}).constructor) && !Object.entries(val || {}).length;
// 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);
export const sortBy = (key) => {
return (a, b) => (a[key] > b[key] ? 1 : b[key] > a[key] ? -1 : 0);
};
/**
* Object排序keys
*/
export const sortKeys = obj =>
Object.keys(obj)
.sort()
.reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {});
export const sortKeys = (obj) =>
Object.keys(obj)
.sort()
.reduce((a, k2) => ({ ...a, [k2]: obj[k2] }), {});
/**
* 数组排序, 给定排序数组
@ -158,48 +174,41 @@ export const sortKeys = obj =>
* @returns
*/
export const sortArrayByOrder = (items, keyName, keyOrder) => {
return items.sort((a, b) => {
return keyOrder.indexOf(a[keyName]) - keyOrder.indexOf(b[keyName]);
});
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];
}
});
}
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;
return result;
}
/**
@ -208,16 +217,16 @@ export function merge(...objects) {
* @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];
return array.reduce((groups, item) => {
const key = typeof callback === 'function' ? callback(item) : item[callback];
if (!groups[key]) {
groups[key] = [];
}
if (!groups[key]) {
groups[key] = [];
}
groups[key].push(item);
return groups;
}, {});
groups[key].push(item);
return groups;
}, {});
}
/**
@ -226,12 +235,12 @@ export function groupBy(array = [], callback) {
* @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;
}, {});
return keys.reduce((obj, key) => {
if (object && Object.prototype.hasOwnProperty.call(object, key)) {
obj[key] = object[key];
}
return obj;
}, {});
}
/**
@ -241,68 +250,44 @@ export function pick(object, keys) {
* @returns
*/
export function omit(object, keysToOmit) {
return Object.fromEntries(Object.entries(object).filter(([key]) => !keysToOmit.includes(key)));
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') {
export function cloneDeep(value) {
// return structuredClone(value);
if (typeof value !== 'object' || value === null) {
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);
}
const result = Array.isArray(value) ? [] : {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
// 处理普通属性
result[key] = cloneDeep(value[key], visited);
result[key] = cloneDeep(value[key]);
}
}
return result;
}
/**
* 向零四舍五入, 固定精度设置
*/
function curriedFix(precision = 0) {
return function (number) {
// Shift number by precision places
const shift = Math.pow(10, precision);
const shiftedNumber = number * shift;
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);
// Round to nearest integer
const roundedNumber = Math.round(shiftedNumber);
// Shift back decimal place
return roundedNumber / shift;
};
// Shift back decimal place
return roundedNumber / shift;
};
}
/**
* 向零四舍五入, 保留2位小数
@ -328,106 +313,106 @@ export const fixToInt = curriedFix(0);
*
*/
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;
}
}
});
// Loop through array mapping
if (Array.isArray(input)) {
return input.map((obj) => objectMapper(obj, keyMap));
}
return mappedObj;
}
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 || key] = value;
}
}
});
return mappedObj;
}
return input;
return input;
}
/**
* 创建一个对应于对象路径的值数组
*/
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;
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]]];
}
}
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;
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 const numberFormatter = number => {
return new Intl.NumberFormat().format(number);
export const numberFormatter = (number) => {
return new Intl.NumberFormat().format(number);
};
/**
@ -437,200 +422,193 @@ export const numberFormatter = number => {
* 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);
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 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 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);
};
};
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 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);
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 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 "";
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 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];
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;
}
}
}
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);
}
// 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)));
}
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);
}
};
// 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();
}
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);
}
});
} catch (error) {
console.error('Error clearing caches or unregistering service worker:', error);
}
};
//格式化为冒号时间2010转为20:10
export const formatColonTime = text => {
const hours = text.substring(0, 2);
const minutes = text.substring(2);
return `${hours}:${minutes}`;
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);
}
});
};
// 生成唯一 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,145 +1,203 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react'
import {
Popover, Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp,
Button, Form, Input
} from 'antd'
import { DownOutlined } from '@ant-design/icons'
import 'antd/dist/reset.css'
import AppLogo from '@/assets/highlights_travel_600_550.png'
import { isEmpty } from '@/utils/commons'
import { useTranslation } from 'react-i18next'
import zhLocale from 'antd/locale/zh_CN'
import enLocale from 'antd/locale/en_US'
import 'dayjs/locale/zh-cn'
import ErrorBoundary from '@/components/ErrorBoundary'
import { BUILD_VERSION, GIT_HEAD, 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 LogUploader from '@/components/LogUploader'
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";
const { Header, Content, Footer } = Layout;
const { Title } = Typography;
let items = [];
const items_default = [
{
label: <Link to="/account/change-password">Change password</Link>,
key: "0",
},
{
label: <Link to="/account/profile">Profile</Link>,
key: "1",
},
{
type: "divider",
},
{
label: <Link to="/login?out">Logout</Link>,
key: "3",
},
];
const item_manager =
[
{
label: <Link to="/account/change-password">Change password</Link>,
key: "0",
},
{
label: <Link to="/account/profile">Profile</Link>,
key: "1",
},
{
type: "divider",
},
{
label: <Link to="/login?out">Logout</Link>,
key: "3",
},
{
label:<Link to="/account/change-vendor">Change Vendor</Link>,
key:"4",
},
];
function App() {
const { t, i18n } = useTranslation()
const { colorPrimary } = useThemeContext()
const [isPermitted, currentUser] = useAuthStore(
(state) => [state.isPermitted, state.currentUser])
const { loginToken } = usingStorage()
const noticeUnRead = useNoticeStore((state) => state.noticeUnRead)
const href = useHref()
const navigate = useNavigate()
function App() {
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 isProductPermitted = isPermitted(PERM_PRODUCTS_MANAGEMENT) || isPermitted(PERM_PRODUCTS_INFO_PUT)
const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/pick-year'
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<ConfigProvider locale={antdLng}
<ConfigProvider
theme={{
token: {
colorPrimary: colorPrimary,
colorPrimary: "#00b96b",
},
algorithm: theme.defaultAlgorithm,
}}>
<AntApp>
<FloatButton.Group
shape='square'
style={{
insetInlineEnd: 94,
}}
<Modal
centered
closable={false}
maskClosable={false}
footer={null}
open={login.timeout}
>
<LogUploader />
<FloatButton.BackTop />
</FloatButton.Group>
<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-12 my-2 mr-6 ml-0' alt='App logo' />
<Title level={3}>Login timeout</Title>
<span>Please input your password</span>
<Space direction="horizontal">
<Input.Password value={password}
onChange={(e) => setPassword(e.target.value)}
onPressEnter={() => onSubmit()}
addonBefore={login.username} />
<Button
onClick={() => onSubmit()}
>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">Reservation</Link> },
{ key: "invoice", label: <Link to="/invoice">Invoice</Link> },
{ key: "feedback", label: <Link to="/feedback">Feedback</Link> },
{ key: "report", label: <Link to="/report">Report</Link> },
{
key: 'notice',
key: "notice",
label: (
<Link to='/notice'>
{t('menu.Notice')}
{noticeUnRead ? <Badge dot /> : ''}
<Link to="/notice">
Notice
{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}>
<Col span={4}>
<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>
@ -147,15 +205,20 @@ function App() {
</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}({GIT_HEAD})</Footer>
<Footer></Footer>
</Layout>
</ErrorBoundary>
</AntApp>
</ConfigProvider>
)
);
}
export default App
export default observer(App);

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

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

@ -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,116 +1,119 @@
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { Button, Form, Input, Row, Radio, App, Typography } from "antd";
import { useTranslation } from "react-i18next";
import useAuthStore from "@/stores/Auth";
import { appendRequestParams } from "@/utils/request";
import { useNavigate, useLocation } from "react-router-dom";
import { useEffect } from 'react';
import { Button, Checkbox, Form, Input, Row, App } from 'antd';
import { useStore } from '@/stores/StoreContext.js';
function Login() {
const [authenticate, loginStatus, defaultRoute] = useAuthStore((state) => [
state.authenticate,
state.loginStatus,
state.defaultRoute,
]);
const { t, i18n } = useTranslation();
const { authStore, noticeStore } = useStore();
const { notification } = App.useApp();
const navigate = useNavigate();
const location = useLocation();
const [form] = Form.useForm();
const handleLngChange = (lng) => {
appendRequestParams("lgc", lng === "zh" ? 2 : 1);
i18n.changeLanguage(lng);
};
const defaultLng = i18n.language ?? "zh";
appendRequestParams("lgc", defaultLng === "zh" ? 2 : 1);
useEffect(() => {
if (loginStatus === 302) {
navigate(defaultRoute);
useEffect (() => {
if (location.search === '?out') {
authStore.logout();
navigate('/login');
}
}, [loginStatus]);
return () => {
// unmount...
};
}, []);
const onFinish = (values) => {
authenticate(values.username, values.password).catch((ex) => {
console.error(ex);
notification.error({
message: t("Validation.Title"),
description: t("Validation.LoginFailed"),
placement: "top",
duration: 4,
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 => {
notification.error({
message: `Notification`,
description: 'Login failed. Incorrect username or password.',
placement: 'top',
duration: 4,
});
});
});
};
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
console.log('Failed:', errorInfo);
};
return (
<>
<Typography.Title className="text-center" level={3}>
Highlights Hub
</Typography.Title>
<Row justify="center" align="middle" className="min-h-96">
<Form
name="login"
layout="vertical"
form={form}
size="large"
labelCol={{
span: 8,
}}
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
<Form
name="basic"
// layout="vertical"
form={form}
size="large"
labelCol={{
span: 8,
}}
wrapperCol={{
span: 16,
}}
style={{
maxWidth: 600,
}}
initialValues={{
remember: true,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label="Username"
name="username"
rules={[
{
required: true,
message: 'Please input your username!',
},
]}
>
<Input />
</Form.Item>
<Form.Item
label="Password"
name="password"
rules={[
{
required: true,
message: 'Please input your password!',
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item
wrapperCol={{
span: 24,
}}
className="max-w-xl"
initialValues={{
language: defaultLng,
offset: 8,
span: 16,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
>
<Form.Item
label={t("Username")}
name="username"
rules={[
{
required: true,
message: t("Validation.UsernameIsEmpty"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("Password")}
name="password"
rules={[
{
required: true,
message: t("Validation.PasswordIsEmpty"),
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item name="language">
<Radio.Group onChange={(e) => handleLngChange(e.target.value)}>
<Radio value="zh">中文</Radio>
<Radio value="en">English</Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="w-full">
{t("Login")}
</Button>
</Form.Item>
</Form>
</Row>
</>
<Button type="primary" htmlType="submit" style={{width: "100%"}}>
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,36 +1,57 @@
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/highlights_travel_600_550.png";
import { useThemeContext } from "@/stores/ThemeContext";
import { BUILD_VERSION, GIT_HEAD } from "@/config";
import AppLogo from "@/assets/logo-gh.png";
import { useStore } from "@/stores/StoreContext.js";
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 text-center">
<img src={AppLogo} className="h-12 my-2 mr-6 ml-0" alt="App logo" />
<Layout
style={{
minHeight: "100vh",
}}>
<Header className="header" style={{ position: "sticky", top: 0, zIndex: 1, width: "100%" }}>
<Row gutter={{ md: 24 }} justify="center">
<Col span={4}>
<img src={AppLogo} className="logo" alt="App logo" />
</Col>
<Col span={20}><Title style={{ color: "white", marginTop: "3.5px" }}>Global Highlights Hub</Title></Col>
</Row>
</Header>
<Content className="p-6 m-0 min-h-72 bg-white">
<Content
style={{
padding: 24,
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}>
<Outlet />
</Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}({GIT_HEAD})</Footer>
<Footer></Footer>
</Layout>
</AntApp>
</ConfigProvider>
);
}
export default Standlone;
export default observer(Standlone);

@ -1,92 +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, defaultRoute] = useAuthStore((state) => [
state.changeUserPassword,
state.defaultRoute,
])
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!',
},
]}
>
@ -95,17 +91,17 @@ function ChangePassword() {
<Form.Item>
<Space size="middle">
<Button type="primary" htmlType="submit">
{t('Submit')}
Save
</Button>
<Button onClick={() => navigate(defaultRoute)}>
{t('Cancel')}
<Button onClick={() => navigate('/reservation/newest')}>
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,374 +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>
<Button type='link' key='editAccount' onClick={() => onAccountSeleted(account)}>{t('account:action.edit')}</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)}
destroyOnHidden
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,23 @@
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';
function Profile() {
const { t } = useTranslation()
const currentUser = useAuthStore(state => state.currentUser)
const { authStore } = useStore();
const { login } = authStore;
return (
<Row>
<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 title="User Profile" layout="vertical" column={2}>
<Descriptions.Item label="Username">{login.username}</Descriptions.Item>
<Descriptions.Item label="Telephone">{login.telephone}</Descriptions.Item>
<Descriptions.Item label="Email address">{login.emailAddress}</Descriptions.Item>
<Descriptions.Item label="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 {
PushpinTwoTone,
} 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 (<Button type='text'><PushpinTwoTone twoToneColor="#c0192a" /></Button>)
} 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)}
destroyOnHidden
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,732 +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 = airInfo => {
// 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>
</Form>
<Form
labelCol={{
span: 6,
}}
wrapperCol={{
span: 16,
}}
initialValues={{ ...airInfo }}
onFinish={values => {
ticketIssuedNotifications(userId, airInfo.CLF_SN, airInfo.OPI_SN, values.FlightMemo_messages)
.then(() => {
notification.success({
message: `成功`,
description: "提醒信息已发出!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
//airinfo_form.setFieldValue('FlightMemo_messages','')
})
.catch(() => {
notification.error({
message: `错误`,
description: "提醒失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
}}
autoComplete="off">
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label="提醒信息" name="FlightMemo_messages">
<Input placeholder="没有提醒请留空,信息会抄送给上下站地接" />
</Form.Item>
<Form.Item label="已发提醒" name="FlightMemo">
<Input.TextArea rows={4} readOnly disabled />
</Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button type="primary" htmlType="submit">
3. 通知顾问
</Button>
</Col>
</Row>
</Form>
</>
);
};
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(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={async () => {
await postFlightDetail("", gri_sn, travelAgencyId, { FlightNo: "新的记录", FlightStatus: 1 }, []); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" icon={<PlusOutlined />}>
新增航班记录
</Button>
</Popconfirm>
</p>
</Col>
</Row>
<Row>
<Divider orientation="left">计划变更</Divider>
<Col md={24} lg={12} xxl={12}>
<Space direction="vertical" style={{ width: "90%" }}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ChangeText} />
<Button
type="primary"
onClick={() => {
showModal_confirmInfo(veiPlanChangeTxt && veiPlanChangeTxt.ChangeText);
}}
disabled={isEmpty(veiPlanChangeTxt) || isEmpty(veiPlanChangeTxt.ChangeText)}>
确认变更
</Button>
</Space>
</Col>
<Col md={24} lg={12} xxl={12}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ConfirmInfo} />
</Col>
</Row>
<Modal title="变更" open={isModalOpen_confirmInfo} onOk={handleOk_confirmInfo} onCancel={handleCancel_confirmInfo}>
<Form
form={confirmInfo_form}
labelCol={{
span: 5,
}}>
<Form.Item label="确认信息" name="ConfirmInfo" rules={[{ required: true }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="费用信息"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
okText="保存"
cancelText="关闭"
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
{isAddNew ? (
<>
<Button type="primary" onClick={() => handleOk(false)}>
添加并继续新增
</Button>{" "}
<Button type="primary" onClick={() => handleOk(true)}>
添加并关闭
</Button>
</>
) : (
<OkBtn />
)}
</>
)}>
<Form
form={ticket_form}
labelCol={{
span: 5,
}}>
<Form.Item label="状态" name="CostType">
<Select
style={{
width: 160,
}}
onChange={onChangeType}
options={[
{
value: "出票",
label: "出票",
},
{
value: "改签",
label: "改签",
},
{
value: "退票",
label: "退票",
},
]}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="PNR" name="PNR" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="票号" name="TicketNo" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label="机票类型" name="FlightType" rules={[{ required: true }]}>
<Select
style={{
width: 160,
}}
options={[
{
value: "成人",
label: "成人",
},
{
value: "儿童",
label: "儿童",
},
{
value: "婴儿",
label: "婴儿",
},
]}
/>
</Form.Item>
<Form.Item label="机票价格" name="Cost" rules={[{ required: true }]}>
<Input
placeholder="含基建和税"
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
</>
)}
<Form.Item label="服务费" name="ServiceFee" rules={[{ required: true }]}>
<Input
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="折扣" name="Discount" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
placeholder="如 0.9"
/>
</Form.Item>
<Form.Item label="选择客人" name="MEI_Name66">
<Radio.Group
onChange={e => guestList_OnChange(e)}
style={{
minWidth: 320,
}}>
<List
bordered
dataSource={guestList_select()}
renderItem={item => (
<List.Item>
<Radio value={item.value}>{item.label}</Radio>
</List.Item>
)}></List>
</Radio.Group>
{/* <Select onChange={value => guestList_OnChange(value)} options={guestList_select()} placeholder="如果列表里面没有客人信息,请手动录到备注里" /> */}
</Form.Item>
</>
)}
<Form.Item label="客人信息/备注" name="Memo">
<Input.TextArea rows={4} disabled={isTicketType} />
</Form.Item>
<Form.Item name="CLF_SN" hidden>
<input />
</Form.Item>
<Form.Item name="GRI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="VEI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="CLC_SN" hidden>
<input />
</Form.Item>
</Form>
</Modal>
</Space>
);
};
export default AirticketPlan;

@ -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,208 +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 {ImageUploader} from '@/components/ImageUploader';
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({});
const [ossKey, setOssKey] = useState('');
useEffect(() => {
const key = `ghh/${GRI_SN}-${RefNo}/tourguide_image/travel-agency-${travelAgencyId}`;
setOssKey(key);
}, []);
useEffect(() => {
// console.info('Detail.useEffect: ' + GRI_SN);
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}>
</Upload>
</Form.Item>
<ImageUploader osskey={ossKey} style={{margin: '16px'}}/>
<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,44 +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";
import {ImageUploader} from '@/components/ImageUploader';
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({});
const [ossKey, setOssKey] = useState('');
useEffect(() => {
const key = `ghh/${GRI_SN}-${RefNo}/tourguide_image/travel-agency-${travelAgencyId}`;
setOssKey(key);
}, []);
useEffect(() => {
// console.info("Detail.useEffect: " + GRI_SN);
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;
@ -49,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) {
@ -60,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",
@ -85,10 +72,12 @@ function Detail() {
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col span={20}>
</Col>
<Col span={4}>
<BackBtn />
<Button type="link" onClick={() => navigate("/feedback")}>
Back
</Button>
</Col>
</Row>
<Row gutter={16}>
@ -172,14 +161,17 @@ function Detail() {
name="ghhfile"
// accept="image/*"
multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${loginToken}`}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${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>
<ImageUploader osskey={ossKey} />
<Form.Item
name="info_content"
rules={[
@ -204,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({
@ -129,14 +122,14 @@ function Detail() {
description: "Success Submit!",
placement: "top",
duration: 4,
});
});
}
});
}
};
const handleChange = info => {
//console.log(info);
console.log(info);
let newFileList = [...info.fileList];
newFileList = newFileList.map(file => {
if (file.response && file.response.result) {
@ -144,12 +137,14 @@ function Detail() {
}
return file;
});
setInvoicekImages(newFileList);
runInAction(() => {
invoiceStore.invoicekImages = newFileList;
});
};
const handRemove = info => {
//console.log(info);
removeFeedbackImages(info.url);
console.log(info);
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>
]}
>
<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>
<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={2} 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);
//value={[search_date_start, search_date_end]} 20230803 zp
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,42 +1,42 @@
import { useParams } from "react-router-dom";
import { NavLink, useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Typography, Divider } from "antd";
import { fetchNoticeDetail } from "@/stores/Notice";
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
import { 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 { CCP_BLID } = useParams();
const { userId } = usingStorage();
const { noticeStore, authStore } = useStore();
const { noticeInfo } = noticeStore;
const { CCP_BLID } = useParams();
const [noticeInfo, setNoticeInfo] = useState({});
useEffect(() => {
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 className="whitespace-pre-line">
{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,272 +0,0 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { App, Empty, Button, Collapse, Table, Space, Alert } from 'antd';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
import { useTranslation } from 'react-i18next';
import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index';
import { cloneDeep, groupBy, isEmpty, isNotEmpty } from '@/utils/commons';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config';
import Header from './Detail/Header';
import dayjs from 'dayjs';
import { usingStorage } from '@/hooks/usingStorage';
import { ClockCircleOutlined, PlusCircleFilled } from '@ant-design/icons';
import ProductQuotationLogPopover, { columnsSets } from './Detail/ProductQuotationLogPopover';
const PriceTable = ({ productType, dataSource, refresh }) => {
const { t } = useTranslation('products');
const { travel_agency_id, use_year, audit_state } = useParams();
const isPermitted = useAuthStore(state => state.isPermitted);
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
const { message, notification } = App.useApp();
const stateMapVal = useProductsAuditStatesMapVal();
const [renderData, setRenderData] = useState(dataSource);
// console.log(dataSource);
const [logOpenPriceRow, setLogOpenPriceRow] = useState(null); // price id
const handleAuditPriceItem = (state, row, rowIndex) => {
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' : ''; // 线
// && isNotEmpty(r.lastedit_changed)
const editedCls = (r.audit_state_id === 0 ) ? '!bg-amber-100' : ''; // ,
const newCls = (r.audit_state_id === -1 ) ? '!bg-sky-100' : ''; // ,
const editedCls_ = isNotEmpty(r.lastedit_changed) ? (r.audit_state_id === 0 ? '!bg-red-100' : '!bg-sky-100') : '';
const lodHighlightCls = (r.id === logOpenPriceRow ) ? '!bg-violet-300 !text-violet-900' : '';
return [trCls, bigTrCls, newCls, editedCls, lodHighlightCls].join(' ');
};
const 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 (
<div className=''>
{isNotEmpty(itemLink) ? (
<div className='' onClick={() => setEditingProduct({ info: r.info })}>
<Link to={itemLink}>{title}</Link>
</div>
) : (
title
)}
</div>
);
},
},
...columnsSets(t),
{
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) =>
[-1, 0, 3].includes(Number(r.audit_state_id)) ? (
<>
<Space className='w-full [&>*:last-child]:ms-auto'>
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
{Number(r.audit_state_id) === 0 && (
<div className='flex gap-2'>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</div>
)}
</RequireAuth>
<ProductQuotationLogPopover
method={'history'}
{...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
onOpenChange={(open) => setLogOpenPriceRow(open ? r.id : null)}
/>
</Space>
</>
) : null,
},
// {
// title: '',
// key: 'action2',
// width: '6rem',
// className: 'bg-white align-bottom',
// onCell: (r, index) => ({ rowSpan: r.rowSpan }),
// render: (_, r) => {
// const showPublicBtn = null; // r.pendingQuotation ? <Popover title='' trigger={['click']}> <Button size='small' className='ml-2' onClick={() => { }}></Button></Popover> : null;
// const btn2 = !r.showPublicBtn ? (
// <>
// <ProductQuotationSnapshotPopover
// // <ProductQuotationLogPopover
// method={'snapshot'}
// {...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
// triggerProps={{ type: 'primary', ghost: true, size: 'small' }}
// placement='bottom'
// className='max-w-[1000px]'
// />
// </>
// ) : null;
// return <div className='flex flex-col gap-2 justify-end'>{btn2}</div>;
// },
// },
];
return (
<Table
size={'small'}
className='border-collapse'
rowHoverable={false}
rowClassName={rowStyle}
pagination={false}
{...{ columns }}
dataSource={renderData}
rowKey={(r) => r.id}
/>
);
};
/**
*
*/
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);
let tempKey = '';
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => {
const _children = 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],
showPublicBtn: c.quotation.some(q2 => [0, 3].includes(q2.audit_state_id)),
}))
),
[]
);
tempKey = _children.length > 0 && tempKey==='' ? ele.key : tempKey;
const _childrenByState = groupBy(_children, 'audit_state_id');
// if (_children.length > 0) console.log('PriceTable\n'+ele.value+'\n', _children)
return {
...ele,
extra: <Space>
{_childrenByState['1']?.length > 0 && <Alert showIcon type='success' className='py-1 text-xs' message={_childrenByState['1']?.length || 0} />}
{_childrenByState['2']?.length > 0 && <Alert showIcon type='success' className='py-1 text-xs' message={_childrenByState['2']?.length || 0} icon={<ClockCircleOutlined />} />}
{_childrenByState['0']?.length > 0 && <Alert showIcon type='warning' className='py-1 text-xs' message={_childrenByState['0']?.length || 0} />}
{_childrenByState['3']?.length > 0 && <Alert showIcon type='error' className='py-1 text-xs' message={_childrenByState['3']?.length || 0} />}
{_childrenByState['-1']?.length > 0 && <Alert showIcon type='info' className='py-1 text-xs' message={_childrenByState['-1']?.length || 0} icon={<PlusCircleFilled />} />}
<span>{t('Table.Total', { total: _children.length })}</span>
</Space>,
children: (
<PriceTable
// loading={loading}
productType={ele.value}
dataSource={_children}
refresh={props.refresh}
/>
),
}});
setShowTypes(_show);
setActiveKey(isEmpty(_show) ? [] : [tempKey]);
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;

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

Loading…
Cancel
Save