Merge branch 'main' of github.com:hainatravel/GHHub

perf/export-docx
YCC 2 years ago
commit d97638b843

@ -7,27 +7,39 @@ 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]
## 相关文档
需求文档 https://www.kdocs.cn/l/csZrIZlpuF2i
dayjs https://dayjs.gitee.io/docs/zh-CN/manipulate/start-of
antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
反馈表案例 https://www.chinahighlights.com/customerservice/feedback/PostTourSurveyFormToWLGH.asp?LGC=1&COLI_SN=988185&MEI_SN=954295&Email=jennroth18@hotmail.com&ToC=0&ShowType=&page_class=4&dei_sn=28&country=30,490
国内供应商平台 http://p.mycht.cn/index.aspx
文档预览 https://github.com/cyntler/react-doc-viewer
wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start
pdf生成 https://github.com/ivmarcos/react-to-pdf
react-pdf https://react-pdf.org
npm version premajor --no-git-tag-version
1.0.0 -> 2.0.0-0
--preid beta | alpha | rc
npm version prerelease --preid beta --no-git-tag-version
2.0.0-0 -> 2.0.0-1 -> 2.0.0-2 ..n -> 2.0.0-n
npm version patch --no-git-tag-version
2.0.0-n -> 2.0.0
## 阿里云OSS
Bucket 名称global-highlights-hub
Endpointoss-cn-hongkong.aliyuncs.com
global-highlights-hub.oss-cn-hongkong.aliyuncs.com
"push:tag": "npm version patch && git.exe push --progress "origin" main:main"
"push:tag": "npm version patch && git push origin master"
## 相关文档
需求文档 https://www.kdocs.cn/l/csZrIZlpuF2i
dayjs https://dayjs.gitee.io/docs/zh-CN/manipulate/start-of
antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
反馈表案例 https://www.chinahighlights.com/customerservice/feedback/PostTourSurveyFormToWLGH.asp?LGC=1&COLI_SN=988185&MEI_SN=954295&Email=jennroth18@hotmail.com&ToC=0&ShowType=&page_class=4&dei_sn=28&country=30,490
国内供应商平台 http://p.mycht.cn/index.aspx
文档预览 https://github.com/cyntler/react-doc-viewer
wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start
pdf生成 https://github.com/ivmarcos/react-to-pdf
react-pdf https://react-pdf.org
## 阿里云OSS
Bucket 名称global-highlights-hub
Endpointoss-cn-hongkong.aliyuncs.com
global-highlights-hub.oss-cn-hongkong.aliyuncs.com
反馈表测试链接
http://202.103.68.111:5173/feedback/330948
反馈表测试链接
http://202.103.68.111:5173/feedback/330948
---

@ -45,22 +45,44 @@ VALUES ('技术研发部')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有权限', '*', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('最新团计划', '/reservation/newest', 'oversea')
VALUES ('管理账号', '/account/management', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('账单', '/invoice', 'oversea')
VALUES ('新增账号', '/account/new', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('禁用账号', '/account/disable', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('重置密码', '/account/reset-password', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('管理角色', '/account/role-new', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有海外功能', '/oversea/all', 'oversea')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有国内功能', '/domestic/all', 'domestic')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有机票功能', '/air-ticket/all', 'air-ticket')
-- 价格管理
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('管理产品', '/products/*', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('审核信息', '/products/info/audit', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('录入信息', '/products/info/put', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('账号权限管理', '/account/management', 'system')
VALUES ('审核价格', '/products/offer/audit', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('新增角色', '/account/new-role', 'system')
VALUES ('录入价格', '/products/offer/put', 'products')
-- 默认页面
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('最新计划', 'route=/reservation/newest', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('机票订票', 'route=/airticket', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(客服)', 'route=/products', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(供应商)', 'route=/products/edit', 'page')
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1)
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (6, 2)
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (6, 3)
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (6, 4)
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (6, 5)

Binary file not shown.

@ -14,13 +14,6 @@
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,7 +1,7 @@
{
"name": "global.highlights.hub",
"name": "global-highlights-hub",
"private": true,
"version": "2.0.0",
"version": "2.0.0-alpha.0",
"type": "module",
"scripts": {
"dev": "vite",

@ -8,5 +8,29 @@
"CurrentPassword": "Please input your password.",
"NewPassword": "Please input your new password.",
"ReenterPassword": "Please reenter your password."
}
},
"createdOn": "Created on",
"action": "Action",
"action.edit": "Edit",
"action.enable": "Enable",
"action.disable": "Disable",
"action.enable.title": "Do you want to enable account?",
"action.disable.title": "Do you want to disable account?",
"action.resetPassword": "Reset Password",
"action.resetPassword.tile": "Do you want to reset password?",
"accountList": "Account List",
"newAccount": "New Account",
"detail": "Detail",
"username": "Username",
"realname": "Realname",
"travelAgency": "Travel Agency",
"travelAgencyName": "Travel Agency Name",
"email": "Email",
"lastLogin": "Last Login",
"roleList": "Role List",
"newRole": "New Role",
"roleName": "Role Name",
"permission": "Permission"
}

@ -10,7 +10,9 @@
"Confirm": "Confirm",
"Close": "Close",
"Save": "Save",
"New": "New",
"Edit": "Edit",
"Audit": "Audit",
"Delete": "Delete",
"Add": "Add",
"View": "View",
@ -19,9 +21,27 @@
"Upload": "Upload",
"preview": "Preview",
"Total": "Total",
"Action": "Action",
"Import": "Import",
"Export": "Export",
"Copy": "Copy",
"sureCancel": "Are you sure to cancel?",
"sureDelete":"Are you sure to delete?",
"Yes": "Yes",
"Success": "Success",
"Failed": "Failed",
"All": "All",
"Table": {
"Total": "Total {{total}} items"
},
"Login": "Login",
"Username": "Username",
"Realname": "Realname",
"Password": "Password",
"ChangePassword": "Change password",
@ -45,13 +65,32 @@
"lastThreeMonth": "Last Three Month",
"thisYear": "This Year"
},
"weekdays": {
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Sunday"
},
"weekdaysShort": {
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat",
"7": "Sun"
},
"menu": {
"Reservation": "Reservation",
"Invoice": "Invoice",
"Feedback": "Feedback",
"Notice": "Notice",
"Report": "Report",
"Airticket": "AirTicket"
"Airticket": "AirTicket",
"Products": "Products"
},
"Validation": {
"Title": "Notification",

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

@ -0,0 +1,118 @@
{
"ProductType": "Product Type",
"type": {
"Experience": "Experience",
"Car": "Transport Services",
"Guide": "Guide Services",
"Package": "Package Tour",
"Attractions": "Attractions",
"Meals": "Meals",
"Extras": "Extras",
"UltraService": "Ultra Service"
},
"EditComponents": {
"info": "Product Information",
"Quotation": "Quotation",
"Extras": "Components"
},
"auditState": {
"New": "New",
"Pending": "Pending",
"Approved": "Approved",
"Rejected": "Rejected",
"Published": "Published"
},
"auditStateAction": {
"New": "New",
"Pending": "Pending",
"Approved": "Approve",
"Rejected": "Reject",
"Published": "Publish"
},
"PriceUnit": {
"0": "Person",
"1": "Group",
"title": "Price Unit"
},
"Status": "Status",
"State": "State",
"Title": "Title",
"Vendor": "Vendor",
"AuState": "Audit State",
"CreatedBy": "Created By",
"CreateDate": "Create Date",
"AuditedBy": "Audited By",
"AuditDate": "Audit Date",
"OpenHours": "Open Hours",
"Duration": "Duration",
"KM": "KM",
"RecommendsRate": "RecommendsRate",
"OpenWeekdays": "Open Weekdays",
"DisplayToC": "Display To C",
"Dept": "Dept",
"productProject": "Product project",
"Code": "Code",
"City": "City",
"Remarks": "Remarks",
"tourTime": "Tour time",
"recommendationRate": "Recommends rate",
"Name": "Name",
"Price":"Price",
"Description":"Description",
"supplierQuotation": "Supplier quotation",
"addQuotation": "Add quotation",
"bindingProducts": "Binding products",
"addBinding": "Add binding",
"adultPrice": "Adult price",
"childrenPrice": "Child price",
"currency": "Currency",
"Types": "Type",
"number": "Number",
"validityPeriod":"Validity period",
"operation": "Operation",
"price": "Price",
"weekends":"Weekends",
"Quotation": "Quotation",
"Offer": "Offer",
"Unit": "Unit",
"GroupSize": "Group Size",
"UseDates": "Use Dates",
"Weekdays": "Weekdays",
"OnWeekdays": "On Weekdays: ",
"Unlimited": "Unlimited",
"UseYear": "Use Year",
"AgeType": {
"Type": "Age Type",
"Adult": "Adult",
"Child": "Child"
},
"CopyFormMsg": {
"Source": "Source ",
"target": "Target ",
"requiredVendor": "Please pick a target vendor",
"requiredTypes": "Please select product types",
"requiredDept": "Please pick a owner department"
},
"quotationTable": {
"Adult": "Adult",
"Child": "Child",
"Currency": "Currency",
"Unit": "Unit",
"GroupSize": "Group Size",
"UseDates": "Use Dates",
"Operation": "Operation",
"Weekdays": "Weekdays"
},
"#": "Products"
}

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

@ -10,7 +10,9 @@
"Confirm": "确认",
"Close": "关闭",
"Save": "保存",
"New": "新增",
"Edit": "编辑",
"Audit": "审核",
"Delete": "删除",
"Add": "添加",
"View": "查看",
@ -19,9 +21,27 @@
"Upload": "上传",
"preview": "预览",
"Total": "总数",
"Action": "操作",
"Import": "导入",
"Export": "导出",
"Copy": "复制",
"sureCancel": "确定取消?",
"sureDelete":"确定删除?",
"Yes": "是",
"Success": "成功",
"Failed": "失败",
"All": "所有",
"Table": {
"Total": "共 {{total}} 条"
},
"Login": "登录",
"Username": "账号",
"Realname": "姓名",
"Password": "密码",
"ChangePassword": "修改密码",
@ -45,13 +65,32 @@
"lastThreeMonth": "前三个月",
"thisYear": "今年"
},
"weekdays": {
"1": "周一",
"2": "周二",
"3": "周三",
"4": "周四",
"5": "周五",
"6": "周六",
"7": "周日"
},
"weekdaysShort": {
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "日"
},
"menu": {
"Reservation": "团预订",
"Invoice": "账单",
"Feedback": "反馈表",
"Notice": "通知",
"Report": "质量评分",
"Airticket": "机票订票"
"Airticket": "机票订票",
"Products": "产品管理"
},
"Validation": {
"Title": "温馨提示",

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

@ -0,0 +1,118 @@
{
"ProductType": "项目类型",
"type": {
"Experience": "综费",
"Car": "车费",
"Guide": "导游",
"Package": "包价线路",
"Attractions": "景点",
"Meals": "餐费",
"Extras": "附加项目",
"UltraService": "超公里"
},
"EditComponents": {
"info": "产品信息",
"Quotation": "报价",
"Extras": "绑定项目"
},
"auditState": {
"New": "新增",
"Pending": "待审核",
"Approved": "已审核",
"Rejected": "未通过",
"Published": "已发布"
},
"auditStateAction": {
"New": "新增",
"Pending": "待审核",
"Approved": "审核通过",
"Rejected": "审核拒绝",
"Published": "审核发布"
},
"PriceUnit": {
"0": "每人",
"1": "每团",
"title": "报价单位"
},
"Status": "状态",
"State": "状态",
"Title": "名称",
"Vendor": "供应商",
"AuState": "审核状态",
"CreatedBy": "提交人员",
"CreateDate": "提交时间",
"AuditedBy": "审核人员",
"AuditDate": "审核时间",
"OpenHours": "游览时间",
"Duration": "游览时长",
"KM": "公里数",
"RecommendsRate": "推荐指数",
"OpenWeekdays": "周开放日",
"DisplayToC": "报价信显示",
"Dept": "小组",
"productProject": "产品项目",
"Code": "简码",
"City": "城市",
"Remarks": "备注",
"tourTime": "游览时间",
"recommendationRate": "推荐指数",
"Name":"名称",
"Price":"价格",
"Description":"描述",
"supplierQuotation": "供应商报价",
"addQuotation": "添加报价",
"bindingProducts": "绑定产品",
"addBinding": "添加绑定",
"adultPrice": "成人价",
"childrenPrice": "儿童价",
"currency": "币种",
"Types": "类型",
"number": "人等",
"validityPeriod":"有效期",
"operation": "操作",
"price": "价格",
"Quotation": "报价",
"Offer": "报价",
"Unit": "单位",
"GroupSize": "人等",
"UseDates": "使用日期",
"Weekdays": "周末",
"OnWeekdays": "周: ",
"Unlimited": "不限",
"UseYear": "年份",
"AgeType": {
"Type": "人群",
"Adult": "成人",
"Child": "儿童"
},
"CopyFormMsg": {
"Source": "源",
"target": "目标",
"requiredVendor": "请选择目标供应商",
"requiredTypes": "请选择产品类型",
"requiredDept": "请选择所属小组"
},
"quotationTable": {
"Adult": "成人",
"Child": "儿童",
"Currency": "币种",
"Unit": "类型",
"GroupSize": "人等",
"UseDates": "使用日期",
"Weekdays": "周末",
"Operation": "Operation"
},
"#": "产品"
}

@ -1,23 +1,6 @@
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';
.logo {
float: left;
height: 36px;
margin: 16px 24px 16px 0;
background: rgba(255, 255, 255, 0.3);
}
.reservation-highlight {
color: rgba(255, 255, 255, 1);
background-color: rgba(255, 0, 0, 0.6);
}
#error-page {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
width: 100%;
.ant-table-wrapper.border-collapse table {
border-collapse: collapse;
}

@ -0,0 +1,12 @@
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;

@ -0,0 +1,29 @@
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;

@ -0,0 +1,60 @@
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;

@ -0,0 +1,23 @@
import { Select } from 'antd';
import { useProductsTypes } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
//
export const fetchVendorList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q })
return errcode !== 0 ? [] : result
}
const ProductsTypesSelector = ({...props}) => {
const productsTypes = useProductsTypes();
const { t } = useTranslation();
return (
<>
<Select labelInValue allowClear placeholder={t('products:ProductType')} options={productsTypes} {...props}/>
</>
);
};
export default ProductsTypesSelector;

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

@ -1,16 +1,22 @@
import { useEffect } from 'react';
import { Form, Input, Row, Col, Select, DatePicker, Space, Button } from 'antd';
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 usePresets from '@/hooks/usePresets';
import { useDatePresets } from '@/hooks/useDatePresets';
import { useTranslation } from 'react-i18next';
import SearchInput from './SearchInput';
import AuditStateSelector from './AuditStateSelector';
import DeptSelector from './DeptSelector';
import ProductsTypesSelector, { fetchVendorList } from './ProductsTypesSelector';
import CitySelector from '@/components/CitySelector';
const { RangePicker } = DatePicker;
const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
const SearchForm = ({ initialValue, onSubmit, onReset, confirmText, formName, formLayout, loading, ...props }) => {
const { t } = useTranslation();
const presets = usePresets();
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();
@ -23,11 +29,12 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
shows: [],
...props.fieldsConfig,
};
const { confirmText } = props;
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) : '') },
@ -36,6 +43,36 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
{ key: 'endtime', transform: (arrVal) => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : '') },
],
'invoiceStatus': { key: 'invoiceStatus', transform: (value) => value?.value || value?.key || '', default: '' },
'audit_state': { key: 'audit_state', transform: (value) => value?.value || value?.key || '', default: '' },
'agency': {
key: 'agency',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'year': [
{ key: 'year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') },
],
'products_types': {
key: 'products_types',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'dept': {
key: 'dept',
transform: (value) => {
console.log(value);
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'city': {
key: 'city',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'unconfirmed': { key: 'unconfirmed', transform: (value) => value ? 1 : 0 },
};
let dest = {};
const { dates, ...omittedValue } = values;
@ -57,9 +94,9 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
}, []);
const onFinish = (values) => {
console.log('Received values of form, origin form value: ', values);
console.log('Received values of form, origin form value: \n', values);
const dest = formValuesMapper(values);
console.log('form value send to onSubmit:', dest);
console.log('form value send to onSubmit:\n', dest);
const str = new URLSearchParams(dest).toString();
setFormValues(values);
setFormValuesToSub(dest);
@ -83,16 +120,21 @@ const SearchForm = ({ initialValue, onSubmit, onReset, ...props }) => {
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} name='advanced_search' className='orders-search-form' onFinish={onFinish} onValuesChange={onValuesChange}>
<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'>
<Button size={'middle'} type='primary' htmlType='submit' loading={loading}>
{confirmText || t('Search')}
</Button>
{/* <Button size="small" onClick={onReset}>
@ -132,19 +174,27 @@ function getFields(props) {
};
let baseChildren = [];
baseChildren = [
item(
'keyword',
99,
<Form.Item name='keyword' {...fieldProps.keyword}>
<Input allowClear {...fieldComProps.keyword} />
</Form.Item>,
fieldProps?.keyword?.col || 6
),
item(
'referenceNo',
1,
99,
<Form.Item name='referenceNo' label={t('group:RefNo')} {...fieldProps.referenceNo}>
<Input placeholder={t('group:RefNo')} allowClear />
<Input placeholder={t('group:RefNo')} allowClear />
</Form.Item>,
fieldProps?.referenceNo?.col || 4
fieldProps?.referenceNo?.col || 6
),
item(
'PNR',
2,
<Form.Item name='PNR' label="PNR">
<Input placeholder={t('group:PNR')} allowClear />
99,
<Form.Item name='PNR' label='PNR'>
<Input placeholder={t('group:PNR')} allowClear />
</Form.Item>,
fieldProps?.PNR?.col || 4
),
@ -153,7 +203,6 @@ function getFields(props) {
99,
<Form.Item name={`invoiceStatus`} initialValue={at(props, 'initialValue.invoiceStatus')[0] || { value: '0', label: 'Status' }}>
<Select
style={{ width: '100%' }}
labelInValue
options={[
{ value: '0', label: 'Status' },
@ -178,21 +227,78 @@ function getFields(props) {
),
item(
'username',
3,
99,
<Form.Item name='username' label={t('account:username')} {...fieldProps.username}>
<Input placeholder={t('account:username')} allowClear />
<Input placeholder={t('account:username')} allowClear />
</Form.Item>,
fieldProps?.username?.col || 4
),
/**
*
*/
item(
'realname',
4,
<Form.Item name='realname' label={t('account:realname')} {...fieldProps.realname}>
<Input placeholder={t('account:realname')} allowClear />
'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?.realname?.col || 4
fieldProps?.year?.col || 3
),
item(
'agency',
99,
<Form.Item name='agency' label={t('products:Vendor')} {...fieldProps.agency} initialValue={at(props, 'initialValue.agency')[0]}>
<SearchInput
placeholder={t('products:Vendor')}
mode={'multiple'}
maxTagCount={0}
{...fieldComProps.agency}
fetchOptions={fetchVendorList}
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
/>
</Form.Item>,
fieldProps?.agency?.col || 6
),
item(
'audit_state',
99,
<Form.Item name={`audit_state`} initialValue={at(props, 'initialValue.audit_state')[0] || { value: '', label: 'Status' }}>
<AuditStateSelector {...fieldComProps.audit_state} />
</Form.Item>,
fieldProps?.audit_state?.col || 3
),
item(
'products_types',
99,
<Form.Item name={`products_types`} label={t('products:ProductType')} {...fieldProps.products_types} initialValue={at(props, 'initialValue.products_types')[0] || undefined}>
<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) => {

@ -0,0 +1,49 @@
import React, { useMemo, useRef, useState } from 'react';
import { Select, Spin } from 'antd';
import { debounce, objectMapper } from '@/utils/commons';
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState([]);
const fetchRef = useRef(0);
const debounceFetcher = useMemo(() => {
const loadOptions = (value) => {
fetchRef.current += 1;
const fetchId = fetchRef.current;
setOptions([]);
setFetching(true);
fetchOptions(value).then((newOptions) => {
const mapperOptions = newOptions.map(ele => objectMapper(ele, props.map));
if (fetchId !== fetchRef.current) {
// for fetch callback order
return;
}
setOptions(mapperOptions);
setFetching(false);
});
};
return debounce(loadOptions, debounceTimeout);
}, [fetchOptions, debounceTimeout]);
return (
<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;

@ -0,0 +1,31 @@
import { Outlet, useNavigate } from 'react-router-dom';
import { Layout, Flex, theme, Spin, Divider } from 'antd';
import BackBtn from './BackBtn';
const { Content, Header } = Layout;
const HeaderWrapper = ({ children, header, loading, ...props }) => {
const navigate = useNavigate();
const {
token: { colorBgContainer },
} = theme.useToken();
return (
<>
<Spin spinning={loading || false}>
<Layout className=' bg-white'>
<Header className='header px-6 h-10 ' style={{ background: 'white' }}>
<Flex justify={'space-between'} align={'center'} className='h-full'>
{/* {header} */}
<div className='grow h-full'>{header}</div>
<BackBtn />
</Flex>
</Header>
<Divider className='my-2' />
<Content className='' style={{ backgroundColor: colorBgContainer }}>
{children || <Outlet />}
</Content>
</Layout>
</Spin>
</>
);
};
export default HeaderWrapper;

@ -18,7 +18,7 @@ export const PERM_ACCOUNT_MANAGEMENT = '/account/management'
export const PERM_ACCOUNT_NEW = '/account/new'
export const PERM_ACCOUNT_DISABLE = '/account/disable'
export const PERM_ACCOUNT_RESET_PASSWORD = '/account/reset-password'
export const PERM_ROLE_NEW = '/account/role/new'
export const PERM_ROLE_NEW = '/account/role-new'
// 海外供应商
// category: oversea
@ -31,3 +31,10 @@ export const PERM_DOMESTIC = '/domestic/all'
// 机票供应商
// category: air-ticket
export const PERM_AIR_TICKET = '/air-ticket/all'
// 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核
export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入
export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核
export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入

@ -1,8 +1,9 @@
import { useEffect, useState } from 'react';
import dayjs from "dayjs";
import { useTranslation } from 'react-i18next';
import i18n from '@/i18n';
const usePresets = () => {
export const useDatePresets = () => {
const [presets, setPresets] = useState([]);
const { t, i18n } = useTranslation();
@ -39,4 +40,21 @@ const usePresets = () => {
return presets;
}
export default usePresets;
export const useWeekdays = () => {
const [data, setData] = useState([]);
const { t, i18n } = useTranslation();
useEffect(() => {
const newData = [
{ value: '1', label: t('weekdays.1') },
{ value: '2', label: t('weekdays.2') },
{ value: '3', label: t('weekdays.3') },
{ value: '4', label: t('weekdays.4') },
{ value: '5', label: t('weekdays.5') },
{ value: '6', label: t('weekdays.6') },
{ value: '7', label: t('weekdays.7') },
];
setData(newData);
return () => {};
}, [i18n.language]);
return data;
};

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

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

@ -1,13 +1,30 @@
import React, { useState } from 'react';
import { Dropdown, Menu } from 'antd';
import { useState, useEffect } from 'react';
import { Dropdown } from 'antd';
import { useTranslation } from 'react-i18next';
import { appendRequestParams } from '@/utils/request';
const i18n_to_htcode = {
'zh': 2,
'en': 1,
};
export const useDefaultLgc = () => {
const { i18n } = useTranslation();
return { language: i18n_to_htcode[i18n.language], };
};
/**
* 语言选择组件
*/
const Language = () => {
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation();
const [selectedKeys, setSelectedKeys] = useState([i18n.language]);
useEffect(() => {
appendRequestParams('lgc', i18n_to_htcode[i18n.language]);
return () => {};
}, [i18n.language]);
//
const handleChangeLanguage = ({ key }) => {
setSelectedKeys([key]);

@ -17,7 +17,7 @@ i18n
backend: {
loadPath: '/locales/{{lng}}/{{ns}}.json',
},
ns: ['common', 'group', 'vendor', 'account'],
ns: ['common', 'group', 'vendor', 'account', 'products'],
defaultNS: 'common',
detection: {
// convertDetectedLanguage: 'Iso15897',

@ -1,95 +1,110 @@
import React from "react";
import { configure } from "mobx";
import ReactDOM from "react-dom/client";
import React from 'react'
import ReactDOM from 'react-dom/client'
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import "@/assets/global.css";
import App from "@/views/App";
import Standlone from "@/views/Standlone";
import Login from "@/views/Login";
import Logout from "@/views/Logout";
import Index from "@/views/index";
import ErrorPage from "@/components/ErrorPage";
} from 'react-router-dom'
import '@/assets/global.css'
import App from '@/views/App'
import Standlone from '@/views/Standlone'
import Login from '@/views/Login'
import Logout from '@/views/Logout'
import ErrorPage from '@/components/ErrorPage'
import RequireAuth from '@/components/RequireAuth'
import ReservationNewest from "@/views/reservation/Newest";
import ReservationDetail from "@/views/reservation/Detail";
import ChangePassword from "@/views/account/ChangePassword";
import AccountProfile from "@/views/account/Profile";
import AccountManagement from "@/views/account/Management";
import RoleList from "@/views/account/RoleList";
import FeedbackIndex from "@/views/feedback/Index";
import FeedbackDetail from "@/views/feedback/Detail";
import FeedbackCustomerDetail from "@/views/feedback/CustomerDetail";
import ReportIndex from "@/views/report/Index";
import NoticeIndex from "@/views/notice/Index";
import NoticeDetail from "@/views/notice/Detail";
import InvoiceIndex from "@/views/invoice/Index";
import InvoiceDetail from "@/views/invoice/Detail";
import InvoicePaid from "@/views/invoice/Paid";
import InvoicePaidDetail from "@/views/invoice/PaidDetail";
import Airticket from "@/views/airticket/Index";
import AirticketPlan from "@/views/airticket/Plan";
import ReservationNewest from '@/views/reservation/Newest'
import ReservationDetail from '@/views/reservation/Detail'
import ChangePassword from '@/views/account/ChangePassword'
import AccountProfile from '@/views/account/Profile'
import AccountManagement from '@/views/account/Management'
import RoleList from '@/views/account/RoleList'
import FeedbackIndex from '@/views/feedback/Index'
import FeedbackDetail from '@/views/feedback/Detail'
import FeedbackCustomerDetail from '@/views/feedback/CustomerDetail'
import ReportIndex from '@/views/report/Index'
import NoticeIndex from '@/views/notice/Index'
import NoticeDetail from '@/views/notice/Detail'
import InvoiceIndex from '@/views/invoice/Index'
import InvoiceDetail from '@/views/invoice/Detail'
import InvoicePaid from '@/views/invoice/Paid'
import InvoicePaidDetail from '@/views/invoice/PaidDetail'
import Airticket from '@/views/airticket/Index'
import AirticketPlan from '@/views/airticket/Plan'
import { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from './stores/Auth'
import { isNotEmpty } from '@/utils/commons'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET } from '@/config'
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
import './i18n';
import './i18n'
configure({
useProxies: "ifavailable",
enforceActions: "observed",
computedRequiresReaction: true,
observableRequiresReaction: false,
reactionRequiresObservable: true,
disableErrorBoundaries: process.env.NODE_ENV == "production"
});
const { createRoot } = ReactDOM
const router = createBrowserRouter([
{
path: "/",
element: <App />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Index /> },
{ 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: <RequireAuth subject={PERM_OVERSEA} result={true}><NoticeIndex /></RequireAuth>},
{ path: "notice/:CCP_BLID", element: <RequireAuth subject={PERM_OVERSEA} result={true}><NoticeDetail /></RequireAuth>},
{ path: "invoice",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
{ path: "invoice/detail/:GMDSN/:GSN",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
{ path: "invoice/paid",element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
{ path: "invoice/paid/detail/:flid", element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
{ path: "airticket",element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
{ path: "airticket/plan/:coli_sn",element:<AirticketPlan />},
]
},
{
element: <Standlone />,
children: [
{ path: "/login", element: <Login /> },
{ path: "/logout", element: <Logout /> },
]
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/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
{ path: 'airticket',element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
{ path: 'airticket/plan/:coli_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
]
},
{
element: <Standlone />,
children: [
{ path: '/login', element: <Login /> },
{ path: '/logout', element: <Logout /> },
]
}
])
}
const initAppliction = async () => {
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await useAuthStore.getState().initAuth()
}
]);
const router = await initRouter()
createRoot(document.getElementById('root')).render(
//<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</ThemeContext.Provider>
//</React.StrictMode>
)
}
ReactDOM.createRoot(document.getElementById("root")).render(
//<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</ThemeContext.Provider>
//</React.StrictMode>
);
initAppliction()

@ -2,11 +2,15 @@ import { loadScript } from '@/utils/commons';
import { PROJECT_NAME } from '@/config';
export const loadPageSpy = (title) => {
if (import.meta.env.DEV || window.$pageSpy) return
const PageSpySrc = [
'https://page-spy.mycht.cn/page-spy/index.min.js',
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js',
'https://page-spy.mycht.cn/plugin/rrweb/index.min.js',
];
Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
//
PageSpy.registerPlugin(new DataHarborPlugin({ maximum: 2 * 1024 * 1024 }));

@ -1,5 +1,6 @@
import { create } from 'zustand'
import { fetchJSON, postForm } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons'
import { HT_HOST } from "@/config"
import { usingStorage } from '@/hooks/usingStorage'
@ -10,6 +11,13 @@ export const postAccountStatus = async (formData) => {
return errcode !== 0 ? {} : result
}
export const postAccountPassword = async (formData) => {
const { errcode, result } = await postForm(
`${HT_HOST}/service-CooperateSOA/reset_account_password`, formData)
return errcode !== 0 ? {} : result
}
export const fetchAccountList = async (params) => {
const { errcode, result } = await fetchJSON(
@ -38,49 +46,90 @@ export const fetchRoleList = async () => {
return errcode !== 0 ? {} : result
}
export const fetchPermissionList = async () => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_all_permission_list`)
return errcode !== 0 ? {} : result
}
export const fetchPermissionListByRoleId = async (params) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_role_permission_list`, params)
return errcode !== 0 ? {} : result
}
export const fetchTravelAgencyByName = async (name) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/Service_BaseInfoWeb/VendorList`, {q: name})
return errcode !== 0 ? {} : result
}
const useAccountStore = create((set, get) => ({
accountList: [],
selectedAccount: null,
toggleAccountStatus: async (userId, status) => {
selectAccount: (account) => {
set(() => ({
selectedAccount: account
}))
const statusValue = status ? 'enable' : 'disable'
const formData = new FormData()
formData.append('lmi_sn', userId)
formData.append('account_status', statusValue)
return postAccountStatus(formData)
},
disableAccount: async (accountId) => {
resetAccountPassword: async (userId, password) => {
const formData = new FormData()
formData.append('wu_id', accountId)
formData.append('account_status', 'enable')
formData.append('lmi_sn', userId)
formData.append('newPassword', password)
const result = await postAccountStatus(formData)
return postAccountPassword(formData)
},
newEmptyRole: () => {
return {
role_id: null,
role_name: '',
role_ids: ''
}
},
console.info(result)
newEmptyAccount: () => {
return {
accountId: null,
userId: null,
lmi2_sn: null,
username: '',
realname: '',
email: '',
travelAgencyId: null,
roleId: ''
}
},
saveOrUpdateRole: async (formValues) => {
const formData = new FormData()
formData.append('role_id', formValues.role_id)
formData.append('role_name', formValues.role_name)
formData.append('res_ids', '2,3')
formData.append('res_ids', formValues.res_array.join(','))
return postRoleForm(formData)
},
saveOrUpdateAccount: async (formValues) => {
const { selectedAccount } = get()
const { userId } = usingStorage()
const formData = new FormData()
formData.append('wu_id', selectedAccount.userId)
formData.append('lmi_sn', selectedAccount.lmi_sn)
formData.append('lmi2_sn', selectedAccount.lmi2_sn)
formData.append('wu_id', formValues.accountId)
formData.append('lmi_sn', formValues.userId)
formData.append('lmi2_sn', formValues.lmi2_sn)
formData.append('user_name', formValues.username)
formData.append('real_name', formValues.realname)
formData.append('email', formValues.email)
formData.append('travel_agency_id', formValues.travelAgencyId)
formData.append('roles', formValues.roleId)
@ -90,10 +139,13 @@ const useAccountStore = create((set, get) => ({
},
searchAccountByCriteria: async (formValues) => {
let travel_agency_ids = null
if (isNotEmpty(formValues.agency)) {
travel_agency_ids = formValues.agency.map((ele) => ele.key).join(',')
}
const searchParams = {
username: formValues.username,
realname: formValues.realname,
travel_agency_ids: travel_agency_ids,
lgc: 2
}
@ -101,16 +153,18 @@ const useAccountStore = create((set, get) => ({
const mapAccoutList = resultArray.map((r) => {
return {
userId: r.wu_id,
lmi_sn: r.lmi_sn,
accountId: r.wu_id,
userId: r.lmi_sn,
lmi2_sn: r.lmi2_sn,
username: r.user_name,
realname: r.real_name,
email: r.email,
lastLogin: r.wu_lastlogindate,
travelAgency: r.travel_agency_name,
travelAgencyName: r.travel_agency_name,
travelAgencyId: r.travel_agency_id,
roleId: r.roles,
disabled: r.wu_limitsign,
// 数据库支持逗号分隔多角色(5,6,7),目前界面只需单个。
roleId: isEmpty(r.roles) ? 0 : parseInt(r.roles),
role: r.roles_name,
}
})

@ -7,7 +7,8 @@ import { usingStorage } from '@/hooks/usingStorage'
const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN'
const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID'
const KEY_USER_ID = 'G-INT:USER_ID'
const KEY_USER_DETAIL = 'G-JSON:USER_DETAIL'
const WILDCARD_TOKEN = '*'
export const fetchLoginToken = async (username, password) => {
@ -28,86 +29,115 @@ export const fetchUserDetail = async (loginToken) => {
return errcode !== 0 ? {} : Result
}
export const fetchPermissionListByUserId = async (userId) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-CooperateSOA/get_account_permission_list`, { lmi_sn: userId})
return errcode !== 0 ? {} : result
}
async function fetchLastRequet() {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`)
return errcode !== 0 ? {} : result
}
const initialState = {
tokenInterval: null,
tokenTimeout: true,
loginStatus: 0,
defaltRoute: '',
currentUser: {
username: '',
realname: '',
rolesName: '',
emailAddress: '',
travelAgencyName: '',
},
permissionList: []
}
const useAuthStore = create((set, get) => ({
tokenInterval: null,
...initialState,
tokenTimeout: false,
initAuth: async () => {
const { startTokenInterval, loadUserPermission } = get()
const { setStorage, loginToken } = usingStorage()
loginStatus: 0,
const userJson = await fetchUserDetail(loginToken)
loginUser: {
token: '',
telephone: '',
emailAddress: '',
cityId: 0,
permissionList: [],
},
appendRequestParams('token', loginToken)
appendRequestParams('lmi_sn', userJson.LMI_SN)
isPermitted: (perm) => {
return true
// 以上是 Hardcode 判断
// 以下是权限列表从数据库读取后使用的方法
// return this.permissionList.some((value, key, arry) => {
// if (value.indexOf(WILDCARD_TOKEN) > -1) {
// return true
// }
// if (value === perm) {
// return true
// }
// return false
// })
setStorage(KEY_USER_ID, userJson.LMI_SN)
setStorage(KEY_TRAVEL_AGENCY_ID, userJson.LMI_VEI_SN)
await loadUserPermission(userJson.LMI_SN)
set(() => ({
currentUser: {
username: userJson.LoginName,
realname: userJson.real_name,
rolesName: userJson.roles_name,
emailAddress: userJson.LMI_listmail,
travelAgencyName: userJson.VName,
}
}))
startTokenInterval()
loadPageSpy(userJson.real_name)
},
validateUserPassword: async (usr, pwd) => {
const { startTokenInterval } = get()
authenticate: async (usr, pwd) => {
const { initAuth } = get()
const { setStorage } = usingStorage()
const { token: loginToken } = await fetchLoginToken(usr, pwd)
const userDetail = await fetchUserDetail(loginToken)
setStorage(KEY_LOGIN_TOKEN, loginToken)
await initAuth()
set(() => ({
loginUser: {
telephone: userDetail.LkPhone,
emailAddress: userDetail.LMI_listmail,
cityId: userDetail.citysn,
},
tokenTimeout: false,
loginStatus: 302
}))
},
setStorage(KEY_LOGIN_TOKEN, loginToken)
setStorage(KEY_USER_ID, userDetail.LMI_SN)
setStorage(KEY_TRAVEL_AGENCY_ID, userDetail.LMI_VEI_SN)
setStorage(KEY_USER_DETAIL, {username: userDetail.LoginName, travelAgencyName: userDetail.VName})
appendRequestParams('token', loginToken)
// loadPageSpy(`${json.Result.VName}-${json.Result.LoginName}`)
startTokenInterval()
loadUserPermission: async(userId) => {
let deaultPage = '/'
const permissionResult = await fetchPermissionListByUserId(userId)
const pageList = permissionResult.filter(p => {
return p.res_category === 'page'
})
if (pageList.length > 0) {
const resPattern = pageList[0].res_pattern
const splitResult = resPattern.split('=')
if (splitResult.length > 1)
deaultPage = splitResult[1]
}
set(() => ({
defaultRoute: deaultPage,
permissionList: permissionResult.map(p => p.res_pattern)
}))
},
logout: () => {
const { tokenInterval } = get()
const { tokenInterval, currentUser } = get()
const { clearStorage } = usingStorage()
clearStorage()
clearInterval(tokenInterval)
set(() => ({
loginUser: {
},
loginStatus: 0,
tokenInterval: null,
tokenTimeout: true
...initialState,
currentUser: {
username: currentUser.username
}
}))
},
startTokenInterval: () => {
const { loginTimeout } = get()
const { logout } = get()
async function checkTokenTimeout() {
const { LastReqDate } = await fetchLastRequet()
@ -115,43 +145,55 @@ const useAuthStore = create((set, get) => ({
const now = new Date()
const diffTime = now.getTime() - lastReqDate.getTime()
const diffHours = diffTime/1000/60/60
if (diffHours > 4) {
loginTimeout()
if (diffHours > 1) {
logout()
}
}
const interval = setInterval(() => checkTokenTimeout(), 1000*60*20)
const interval = setInterval(() => checkTokenTimeout(), 1000*60*10)
set(() => ({
tokenInterval: interval
}))
},
loginTimeout: () => {
const { tokenInterval } = get()
// TODO: 这里没有清理 token刷新后可以正常使用系统
clearInterval(tokenInterval)
set(() => ({
tokenTimeout: true
}))
},
// TODO: 迁移到 Account.js
changeUserPassword: (password, newPassword) => {
const { userId } = usingStorage()
const formData = new FormData();
formData.append('UserID', userId);
formData.append('Password', password);
formData.append('NewPassword', newPassword);
const postUrl = HT_HOST + '/service-CooperateSOA/SetPassword';
const formData = new FormData()
formData.append('UserID', userId)
formData.append('Password', password)
formData.append('NewPassword', newPassword)
const postUrl = HT_HOST + '/service-CooperateSOA/SetPassword'
return postForm(postUrl, formData)
.then(json => {
if (json.errcode == 0) {
return json;
return json
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
throw new Error(json.errmsg + ': ' + json.errcode)
}
});
})
},
isPermitted: (perm) => {
const { permissionList } = get()
// 测试权限使用:
// if (perm === '/account/management') return false
// if (perm === '/account/role/new') return false
// return true
// 以上是 Hardcode 判断
// 以下是权限列表从数据库读取后使用的方法
return permissionList.some((value) => {
if (value.indexOf(WILDCARD_TOKEN) == 0) {
return true
}
if (value === perm) {
return true
}
return false
})
},
}))
export default useAuthStore
export default useAuthStore

@ -1,4 +1,3 @@
import { makeAutoObservable, runInAction } from 'mobx';
import { fetchJSON, postForm } from '@/utils/request';
import { groupBy } from '@/utils/commons';
import * as config from '@/config';

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

@ -0,0 +1,145 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { groupBy } from '@/utils/commons';
export const searchAgencyAction = async (param) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param);
return errcode !== 0 ? [] : result;
};
/**
* 搜索所有产品, 返回产品列表
* ! 只有审核通过, 已发布的
* @param {object} params { keyword, use_year, product_types, travel_agency_id, city }
*/
export const searchPublishedProductsAction = async (param) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/web_products_search`, param);
return errcode !== 0 ? [] : result;
};
export const copyAgencyDataAction = async (postbody) => {
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const { errcode, result } = await postForm(`${HT_HOST}/Service_BaseInfoWeb/agency_products_copy`, formData);
return errcode === 0 ? true : false;
};
export const getAgencyProductsAction = async (param) => {
const _param = { ...param, use_year: (param.use_year || '').replace('all', ''), audit_state: (param.audit_state || '').replace('all', '') };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/travel_agency_products`, _param);
return errcode !== 0 ? { agency: {}, products: [] } : result;
};
/**
*
*/
export const addProductExtraAction = async (body) => {
return true; // test: 先不更新到HT
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_add`, body);
return errcode === 0 ? true : false;
};
/**
*
*/
export const delProductExtrasAction = async (body) => {
return true; // test: 先不更新到HT
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras_del`, body);
return errcode === 0 ? true : false;
};
/**
* 获取指定产品的附加项目
* @param {object} param { id, travel_agency_id, use_year }
*/
export const getAgencyProductExtrasAction = async (param) => {
const _param = { ...param, use_year: (param.use_year || '').replace('all', '') };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param);
return errcode !== 0 ? [] : result;
};
export const postProductsQuoteAuditAction = async (auditState, quoteRow) => {
const postbody = {
audit_state: auditState,
id: quoteRow.id,
travel_agency_id: quoteRow.travel_agency_id,
};
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/quotation_audit`, formData);
return json;
// return errcode !== 0 ? {} : result;
};
export const postProductsAuditAction = async (auditState, infoRow) => {
const postbody = {
audit_state: auditState,
id: infoRow.id,
travel_agency_id: infoRow.travel_agency_id,
};
const formData = new FormData();
Object.keys(postbody).forEach((key) => {
formData.append(key, postbody[key]);
});
const json = await postForm(`${HT_HOST}/Service_BaseInfoWeb/travel-agency-products-audit`, formData);
return json;
// const { errcode, result } = json;
// return errcode !== 0 ? {} : result;
};
const initialState = {
loading: false,
searchValues: {},
agencyList: [],
activeAgency: {},
agencyProducts: {},
editingProduct: {},
};
export const useProductsStore = create(
devtools((set, get) => ({
// 初始化状态
...initialState,
// state actions
setLoading: (loading) => set({ loading }),
setSearchValues: (searchValues) => set({ searchValues }),
setAgencyList: (agencyList) => set({ agencyList }),
setActiveAgency: (activeAgency) => set({ activeAgency }),
setAgencyProducts: (agencyProducts) => set({ agencyProducts }),
setEditingProduct: (editingProduct) => set({ editingProduct }),
reset: () => set(initialState),
// side effects
searchAgency: async (param) => {
const { setLoading, setAgencyList } = get();
setLoading(true);
const res = await searchAgencyAction(param);
setAgencyList(res);
setLoading(false);
},
getAgencyProducts: async (param) => {
const { setLoading, setActiveAgency, setAgencyProducts } = get();
setLoading(true);
const res = await getAgencyProductsAction(param);
const productsData = groupBy(res.products, (row) => row.info.product_type_id);
setAgencyProducts(productsData);
setActiveAgency(res.agency);
setLoading(false);
},
getAgencyProductExtras: async (param) => {
const res = await getAgencyProductExtrasAction(param);
// todo:
},
}))
);
export default useProductsStore;

@ -8,7 +8,7 @@ export const fetchCityList = async (travelAgencyId, reservationId) => {
const { errcode, Result } = await fetchJSON(
`${HT_HOST}/service-cusservice/PTGetCityGuide`,
{ VEI_SN: travelAgencyId, GRI_SN: reservationId, LGC: 1 })
return errcode !== 0 ? {} : Result;
return errcode !== 0 ? {} : Result
}
export const fetchPlanDetail = async (travelAgencyId, reservationId) => {
@ -31,7 +31,7 @@ export const fetchAttachList = async (reservationId) => {
const { errcode, result } = await fetchJSON(
`${HT_HOST}/service-fileServer/PlanChangeFileList`,
{ GRI_SN: reservationId })
return errcode !== 0 ? {} : result;
return errcode !== 0 ? {} : result
}
const useReservationStore = create((set, get) => ({
@ -91,26 +91,26 @@ const useReservationStore = create((set, get) => ({
}))
},
fetchReservationList: (formVal, current=1) => {
fetchReservationList: (formValues, current=1) => {
const { travelAgencyId } = usingStorage()
const { reservationPage } = get()
// 设置为 0后端会重新计算总数当跳转第 X 页时可用原来的总数。
const totalNum = current == 1 ? 0 : reservationPage.total;
const totalNum = current == 1 ? 0 : reservationPage.total
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/GetPlanSearchList')
.append('VEI_SN', travelAgencyId)
.append('GroupNo', formVal.referenceNo)
.append('DateStart', formVal.startdate)
.append('DateEnd', formVal.enddate)
.append('NotConfirm', '')//status)// Todo: 待解决
.append('GroupNo', formValues.referenceNo)
.append('DateStart', formValues.startdate)
.append('DateEnd', formValues.enddate)
.append('NotConfirm', formValues.unconfirmed)
.append('TotalNum', totalNum)
.append('PageSize', reservationPage.size)
.append('PageIndex', current)
.build();
.build()
return fetchJSON(fetchUrl)
.then(json => {
if (json.errcode == 0) {
const mapReservationList = (json?.Result??[]).map((data, index) => {
const mapReservationList = (json?.Result??[]).map((data) => {
return {
key: data.vas_gri_sn,
reservationId: data.vas_gri_sn,
@ -132,32 +132,32 @@ const useReservationStore = create((set, get) => ({
}
}))
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
throw new Error(json.errmsg + ': ' + json.errcode)
}
});
})
},
fetchAllGuideList: () => {
const { travelAgencyId } = usingStorage()
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetGuideList')
.append('VEI_SN', travelAgencyId)
.build();
.build()
return fetchJSON(fetchUrl)
.then(json => {
if (json.errcode == 0) {
const guideList = (json?.Result??[]).map((data, index) => {
const guideList = (json?.Result??[]).map((data) => {
return {
guideId: data.TGI_SN,
guideName: data.TGI2_Name,
mobileNo: data.TGI_Mobile
}
});
return guideList;
})
return guideList
} else {
throw new Error(json.errmsg + ': ' + json.errcode);
throw new Error(json.errmsg + ': ' + json.errcode)
}
});
})
},
getReservationDetail: async (reservationId) => {
@ -167,8 +167,8 @@ const useReservationStore = create((set, get) => ({
const mapConfirmationList = planChangeList.map((data) => {
const filterAttchList = attachListJson.filter(attch => {
return attch.PCI_SN === data.PCI_SN;
});
return attch.PCI_SN === data.PCI_SN
})
return {
key: data.PCI_SN,
PCI_Changetext: data.PCI_Changetext,
@ -209,7 +209,7 @@ const useReservationStore = create((set, get) => ({
getReservationDetail(travelAgencyId, reservationDetail.reservationId)
return json
}
});
})
},
setupCityGuide: (cityId, guideId) => {
@ -229,7 +229,7 @@ const useReservationStore = create((set, get) => ({
if (json.errcode != 0) {
throw new Error(json.errmsg + ': ' + json.errcode)
}
});
})
},
@ -241,7 +241,7 @@ const useReservationStore = create((set, get) => ({
.append('VEI_SN', travelAgencyId)
.append('GRI_SN', selectedReservation.reservationId)
.append('LGC', 1)
.build();
.build()
return fetchJSON(fetchUrl)
.then(json => {
@ -252,10 +252,6 @@ const useReservationStore = create((set, get) => ({
return data.GuideName
}).join(',')
runInAction(() => {
selectedReservation.guide = reservationGuide
})
set((state) => ({
selectedReservation: {
...state.selectedReservation,
@ -271,4 +267,4 @@ const useReservationStore = create((set, get) => ({
}
}))
export default useReservationStore
export default useReservationStore

@ -255,23 +255,47 @@ export function omit(object, keysToOmit) {
/**
* 深拷贝
*/
export function cloneDeep(value) {
// return structuredClone(value);
if (typeof value !== "object" || value === null) {
return value;
}
const result = Array.isArray(value) ? [] : {};
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
result[key] = cloneDeep(value[key]);
}
}
return result;
export function cloneDeep(value, visited = new WeakMap()) {
// 处理循环引用
if (visited.has(value)) {
return visited.get(value);
}
// 特殊对象和基本类型处理
if (value instanceof Date) {
return new Date(value);
}
if (value instanceof RegExp) {
return new RegExp(value.source, value.flags);
}
if (value === null || typeof value !== 'object') {
return value;
}
// 创建一个新的WeakMap项以避免内存泄漏
let result;
if (Array.isArray(value)) {
result = [];
visited.set(value, result);
} else {
result = {};
visited.set(value, result);
}
for (const key of Object.getOwnPropertySymbols(value)) {
// 处理Symbol属性
result[key] = cloneDeep(value[key], visited);
}
for (const key in value) {
if (Object.prototype.hasOwnProperty.call(value, key)) {
// 处理普通属性
result[key] = cloneDeep(value[key], visited);
}
}
return result;
}
/**
* 向零四舍五入, 固定精度设置
*/

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

@ -113,8 +113,14 @@ export function postForm(url, data) {
}
export function postJSON(url, obj) {
const initParams = getRequestInitParams();
const params4get = Object.assign({}, initParams);
const params = new URLSearchParams(params4get).toString();
const ifp = url.includes('?') ? '&' : '?';
const fUrl = params !== '' ? `${url}${ifp}${params}` : url;
const headerObj = getRequestHeader()
return fetch(url, {
return fetch(fUrl, {
method: 'POST',
body: JSON.stringify(obj),
headers: {

@ -4,8 +4,7 @@ import { Layout, Menu, ConfigProvider, theme, Dropdown, Space, Row, Col, Badge,
import { DownOutlined } from '@ant-design/icons';
import 'antd/dist/reset.css';
import AppLogo from '@/assets/logo-gh.png';
import { isEmpty } from '@/utils/commons';
import { appendRequestParams } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons';
import Language from '../i18n/LanguageSwitcher';
import { useTranslation } from 'react-i18next';
import zhLocale from 'antd/locale/zh_CN';
@ -15,72 +14,68 @@ import ErrorBoundary from '@/components/ErrorBoundary'
import { BUILD_VERSION, } from '@/config';
import useNoticeStore from '@/stores/Notice';
import useAuthStore from '@/stores/Auth'
import { useThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'
const { Header, Content, Footer } = Layout;
const { Title } = Typography;
function App() {
const { t, i18n } = useTranslation();
const { t, i18n } = useTranslation()
const { colorPrimary } = useThemeContext()
const [password, setPassword] = useState('')
const [validateUserPassword, tokenTimeout] = useAuthStore(
(state) => [state.validateUserPassword, state.tokenTimeout])
const [authenticate, tokenTimeout, isPermitted, currentUser] = useAuthStore(
(state) => [state.authenticate, state.tokenTimeout, state.isPermitted, state.currentUser])
const { loginToken, userDetail } = usingStorage()
const { loginToken } = usingStorage()
const noticeUnRead = useNoticeStore((state) => state.noticeUnRead)
const href = useHref()
const navigate = useNavigate()
const location = useLocation()
// /p...
const needToLogin = href !== '/login' && isEmpty(loginToken)
if (!needToLogin) {
appendRequestParams('token', loginToken)
}
useEffect(() => {
if (needToLogin) {
navigate('/login')
}
}, [href])
useEffect(() => {
window.gtag('event', 'page_view', { page_location: window.location.href });
}, [location]);
const onSubmit = () => {
validateUserPassword(userDetail?.username, password)
authenticate(currentUser?.username, password)
.catch(ex => {
console.error(ex)
alert(t('Validation.LoginFailed'))
})
setPassword('')
};
}
const splitPath = href.split('/');
let defaultPath = 'reservation';
const splitPath = href.split('/')
let defaultPath = 'notice'
if (splitPath.length > 1) {
defaultPath = splitPath[1];
defaultPath = splitPath[1]
}
const {
token: { colorBgContainer },
} = theme.useToken();
const [antdLng, setAntdLng] = useState(enLocale);
useEffect(() => {
setAntdLng(i18n.language === 'en' ? enLocale : zhLocale);
}, [i18n.language]);
}, [i18n.language])
return (
<ConfigProvider locale={antdLng}
theme={{
token: {
colorPrimary: '#00b96b',
colorPrimary: colorPrimary,
// "sizeStep": 3,
// "sizeUnit": 3,
},
algorithm: theme.defaultAlgorithm,
}}>
@ -99,32 +94,30 @@ function App() {
<Input.Password value={password}
onChange={(e) => setPassword(e.target.value)}
onPressEnter={onSubmit}
addonBefore={userDetail?.username} />
addonBefore={currentUser?.username} />
<Button
onClick={onSubmit}
>{t('Submit')}</Button></Space>
</Modal>
<Layout
style={{
minHeight: '100vh',
}}>
<Header className='header' style={{ position: 'sticky', top: 0, zIndex: 1, width: '100%' }}>
<Layout className='min-h-screen'>
<Header className='sticky top-0 z-10 w-full'>
<Row gutter={{ md: 24 }} justify='end' align='middle'>
<Col span={16}>
<Col span={14}>
<NavLink to='/'>
<img src={AppLogo} className='logo' alt='App logo' />
<img src={AppLogo} className='float-left h-9 my-4 mr-6 ml-0 bg-white/30' alt='App logo' />
</NavLink>
<Menu
theme='dark'
mode='horizontal'
selectedKeys={[defaultPath]}
items={[
{ key: 'reservation', label: <Link to='/reservation/newest'>{t('menu.Reservation')}</Link> },
{ key: 'invoice', label: <Link to='/invoice'>{t('menu.Invoice')}</Link> },
{ key: 'feedback', label: <Link to='/feedback'>{t('menu.Feedback')}</Link> },
{ key: 'report', label: <Link to='/report'>{t('menu.Report')}</Link> },
{ key: 'airticket', label: <Link to='/airticket'>{t('menu.Airticket')}</Link> },
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_PRODUCTS_MANAGEMENT) ? { key: 'products', label: <Link to='/products'>{t('menu.Products')}</Link> } : null,
{
key: 'notice',
label: (
@ -137,10 +130,10 @@ function App() {
]}
/>
</Col>
<Col span={4}>
<Title level={3} style={{ color: 'white', marginBottom: '0', display: 'flex', justifyContent: 'end' }}>
{userDetail?.travelAgencyName}
</Title>
<Col span={6}>
<h3 className='text-white mb-0 flex justify-end'>
{currentUser?.travelAgencyName}
</h3>
</Col>
<Col span={2}>
<Dropdown
@ -148,20 +141,18 @@ function App() {
items: [...[
{ label: <Link to='/account/change-password'>{t('ChangePassword')}</Link>, key: '0' },
{ label: <Link to='/account/profile'>{t('Profile')}</Link>, key: '1' },
{ label: <Link to='/account/management'>{t('account:management.tile')}</Link>, key: '3' },
{ label: <Link to='/account/role-list'>{t('account:management.roleList')}</Link>, key: '4' },
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' },
],
{ type: 'divider' },
{ label: <>v{BUILD_VERSION}</>, key: 'BUILD_VERSION' },
]
],
}}
trigger={['click']}
>
<a onClick={e => e.preventDefault()}>
<Space>
{userDetail?.username}
<div className='line-clamp-1'>{currentUser?.realname}</div>
<DownOutlined />
</Space>
</a>
@ -172,21 +163,15 @@ function App() {
</Col>
</Row>
</Header>
<Content
style={{
padding: 24,
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}>
<Content className='p-6 m-0 min-h-72 bg-white'>
{needToLogin ? <>login...</> : <Outlet />}
</Content>
<Footer></Footer>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
</Layout>
</ErrorBoundary>
</AntApp>
</ConfigProvider>
);
)
}
export default App

@ -1,13 +0,0 @@
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>
);
}

@ -6,9 +6,8 @@ import useAuthStore from '@/stores/Auth'
import useNoticeStore from '@/stores/Notice'
function Login() {
const [validateUserPassword, loginStatus] =
useAuthStore((state) => [state.validateUserPassword, state.loginStatus])
const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount)
const [authenticate, loginStatus, defaultRoute] =
useAuthStore((state) => [state.authenticate, state.loginStatus, state.defaultRoute])
const { t, i18n } = useTranslation()
const { notification } = App.useApp()
@ -17,12 +16,12 @@ function Login() {
useEffect (() => {
if (loginStatus === 302) {
navigate('/reservation/newest')
navigate(defaultRoute)
}
}, [loginStatus])
const onFinish = (values) => {
validateUserPassword(values.username, values.password)
authenticate(values.username, values.password)
.catch(ex => {
console.error(ex)
notification.error({
@ -39,20 +38,19 @@ function Login() {
}
return (
<Row justify='center' align='middle' style={{ minHeight: 500 }}>
<Row justify='center' align='middle' className='min-h-96'>
<Form
name='basic'
name='login'
layout='vertical'
form={form}
size='large'
labelCol={{
span: 8,
}}
wrapperCol={{
span: 16,
}}
style={{
maxWidth: 600,
span: 24,
}}
className='max-w-xl'
initialValues={{
remember: true,
}}
@ -85,12 +83,8 @@ function Login() {
<Input.Password />
</Form.Item>
<Form.Item
wrapperCol={{
offset: 8,
span: 16,
}}
>
<Button type='primary' htmlType='submit' style={{width: '100%'}}>
<Button type='primary' htmlType='submit' className='w-full'>
{t('Login')}
</Button>
</Form.Item>

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

@ -41,15 +41,13 @@ function ChangePassword() {
return (
<>
<Row justify="center" align="middle" style={{ minHeight: 500 }}>
<Row justify="center" align="middle" className='min-h-96'>
<Form
name="basic"
form={form}
layout="vertical"
size="large"
style={{
maxWidth: 600,
}}
className='max-w-xl'
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"

@ -1,103 +1,15 @@
import { useState } from 'react'
import { Row, Col, Space, Button, Table, Select, TreeSelect, Typography, Modal, App, Form, Input } from 'antd'
import SearchForm from '@/components/SearchForm'
import useAccountStore, { fetchRoleList, fetchTravelAgencyByName } from '@/stores/Account'
import useFormStore from '@/stores/Form'
import { isEmpty } from '@/utils/commons'
import { ExclamationCircleFilled } from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Select, Space, Table, Typography, Switch } from 'antd'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import useFormStore from '@/stores/Form'
import useAuthStore from '@/stores/Auth'
import useAccountStore from '@/stores/Account'
import { fetchRoleList } from '@/stores/Account'
import SearchForm from '@/components/SearchForm'
import RequireAuth from '@/components/RequireAuth'
import { PERM_ROLE_NEW } from '@/config'
const { Title } = Typography
const permissionData = [
{
title: '海外供应商',
value: 'oversea-0',
key: 'oversea-0',
children: [
{
title: '所有海外功能',
value: 'oversea-0-0',
key: 'oversea-0-0',
},
],
},
{
title: '机票管理',
value: '0-0',
key: '0-0',
children: [
{
title: '录入机票价格',
value: '0-0-0',
key: '0-0-0',
},
],
},
{
title: '产品管理',
value: '0-1',
key: '0-1',
children: [
{
title: '搜索供应商产品',
value: 'B-1-0',
key: 'B-1-0',
},
{
title: '录入产品价格',
value: '0-1-0',
key: '0-1-0',
},
{
title: '新增产品描述',
value: '0-1-1',
key: '0-1-1',
},
{
title: '复制供应商产品信息',
value: '0-1-2',
key: '0-1-2',
},
],
},
{
title: '账号管理',
value: '2-1',
key: '2-1',
children: [
{
title: '搜索账号',
value: '2-1-01',
key: '2-1-01',
},
{
title: '新增账号',
value: '2-1-11',
key: '2-1-11',
},
{
title: '禁用账号',
value: '2-1-21',
key: '2-1-21',
},
{
title: '重置账号密码',
value: '2-1-31',
key: '2-1-31',
},
{
title: '新增角色',
value: '2-1-41',
key: '2-1-41',
},
],
},
]
function Management() {
const { t } = useTranslation()
@ -111,22 +23,22 @@ function Management() {
title: t('account:realname'),
dataIndex: 'realname',
},
{
title: t('account:travelAgency'),
dataIndex: 'travelAgency',
},
{
title: t('account:email'),
dataIndex: 'email',
},
{
title: t('account:role'),
dataIndex: 'role',
render: roleRender
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'),
@ -141,59 +53,84 @@ function Management() {
)
}
function roleRender(text) {
return (
<Button type='link' onClick={() => setRoleModalOpen(true)}>{text}</Button>
)
}
function actionRender(text, account) {
function actionRender(_, account) {
return (
<Space key='actionRenderSpace' size='middle'>
<Button type='link' key='disable' onClick={() => showDisableConfirm(account)}>{t('account:action.disable')}</Button>
<Switch checkedChildren={t('account:action.enable')} unCheckedChildren={t('account:action.disable')} checked={account.disabled==0} onChange={(checked) => {
showDisableConfirm(account, checked)
}} />
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
</Space>
)
}
const onPermissionChange = (newValue) => {
console.log('onChange ', newValue)
setPermissionValue(newValue)
}
const [permissionValue, setPermissionValue] = useState(['0-0-0'])
const [isAccountModalOpen, setAccountModalOpen] = useState(false)
const [isRoleModalOpen, setRoleModalOpen] = useState(false)
const [dataLoading, setDataLoading] = useState(false)
const [roleAllList, setRoleAllList] = useState([])
const [travelAgencyList, setTravelAgencyList] = useState([])
const [currentTravelAgency, setCurrentTravelAgency] = useState(null)
const [accountForm] = Form.useForm()
const [searchAccountByCriteria, accountList, disableAccount, selectedAccount, saveOrUpdateAccount, selectAccount] =
const [searchAccountByCriteria, accountList, toggleAccountStatus, saveOrUpdateAccount, resetAccountPassword, newEmptyAccount] =
useAccountStore((state) =>
[state.searchAccountByCriteria, state.accountList, state.disableAccount, state.selectedAccount, state.saveOrUpdateAccount, state.selectAccount])
[state.searchAccountByCriteria, state.accountList, state.toggleAccountStatus, state.saveOrUpdateAccount, state.resetAccountPassword, state.newEmptyAccount])
const formValues = useFormStore(state => state.formValues)
const { notification, modal } = App.useApp()
useEffect(() => {
fetchRoleList()
.then((roleList) => {
const roleListMap = roleList.map(r => {
return {
value: r.role_id,
label: r.role_name,
disabled: r.role_id === 1
}
})
roleListMap.unshift({ value: 0, label: '未设置', disabled: true });
setRoleAllList(roleListMap)
})
}, [])
const handelAccountSearch = () => {
setDataLoading(true)
searchAccountByCriteria(formValues)
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
.finally(() => {
setDataLoading(false)
})
}
const onAccountSeleted = async (account) => {
setTravelAgencyList([{
label: account.travelAgencyName,
value: account.travelAgencyId
}])
accountForm.setFieldsValue(account)
selectAccount(account)
console.info(account)
const roleList = await fetchRoleList()
setRoleAllList(roleList.map(r => {
return {
value: r.role_id,
label: r.role_name,
disabled: r.role_id === 1
}
}))
setCurrentTravelAgency(account.travelAgencyId)
setAccountModalOpen(true)
}
const onNewAccount = () => {
const emptyAccount = newEmptyAccount()
accountForm.setFieldsValue(emptyAccount)
setAccountModalOpen(true)
}
const onAccountFinish = (values) => {
console.log(values)
saveOrUpdateAccount(values)
.then(() => {
handelAccountSearch()
})
.catch(ex => {
console.info(ex.message)
notification.error({
message: 'Notification',
description: ex.message,
@ -208,13 +145,47 @@ function Management() {
// form.resetFields()
}
const showDisableConfirm = (account) => {
const handleTravelAgencySearch = (newValue) => {
setDataLoading(true)
fetchTravelAgencyByName(newValue)
.then(result => {
setTravelAgencyList(result.map(r => {
return {
label: r.travel_agency_name,
value: r.travel_agency_id
}
}))
})
.finally(() => {
setDataLoading(false)
})
}
const handleTravelAgencyChange = (newValue) => {
setCurrentTravelAgency(newValue)
}
const showDisableConfirm = (account, status) => {
const confirmTitle = status ? t('account:action.enable.title') : t('account:action.disable.title')
modal.confirm({
title: 'Do you want to disable this account?',
title: confirmTitle,
icon: <ExclamationCircleFilled />,
content: `Username: ${account.username}, Realname: ${account.realname}`,
content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
onOk() {
disableAccount(account.userId)
toggleAccountStatus(account.userId, status)
.then(() => {
handelAccountSearch()
})
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
},
onCancel() {
},
@ -222,12 +193,22 @@ function Management() {
}
const showResetPasswordConfirm = (account) => {
const confirmTitle = t('account:action.resetPassword.tile')
const randomPassword = account.username + '@' + (Math.floor(Math.random() * 900) + 100)
modal.confirm({
title: 'Do you want to reset password?',
title: confirmTitle,
icon: <ExclamationCircleFilled />,
content: `Username: ${account.username}, Realname: ${account.realname}`,
content: t('account:username') + ': ' + account.username + ', ' + t('account:realname') + ': ' + account.realname,
onOk() {
console.log('ResetPassword')
resetAccountPassword(account.userId, randomPassword)
.then(() => {
notification.info({
message: `请复制新密码给 [${account.realname}]`,
description: '新密码:' + randomPassword,
placement: 'top',
duration: 60,
})
})
},
onCancel() {
},
@ -242,29 +223,30 @@ function Management() {
autoFocus: true,
htmlType: 'submit',
}}
title={t('account:management.newAccount')}
title={t('account:detail')}
open={isAccountModalOpen} onOk={() => setAccountModalOpen(false)} onCancel={() => setAccountModalOpen(false)}
destroyOnClose={true}
clearOnDestroy={true}
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
name='AccountForm'
form={accountForm}
layout='vertical'
size='large'
style={{
maxWidth: 600,
}}
onFinish={onAccountFinish}
onFinishFailed={onAccountFailed}
autoComplete='off'
>
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:management.username')}
label={t('account:username')}
name='username'
rules={[
{
@ -276,7 +258,7 @@ function Management() {
<Input />
</Form.Item>
<Form.Item
label={t('account:management.realname')}
label={t('account:realname')}
name='realname'
rules={[
{
@ -288,7 +270,7 @@ function Management() {
<Input />
</Form.Item>
<Form.Item
label={t('account:management.email')}
label={t('account:email')}
name='email'
rules={[
{
@ -300,7 +282,7 @@ function Management() {
<Input />
</Form.Item>
<Form.Item
label={t('account:management.travelAgency')}
label={t('account:travelAgency')}
name='travelAgencyId'
rules={[
{
@ -309,10 +291,20 @@ function Management() {
},
]}
>
<Select options={[{ value: 33032, label: 'test海外地接B' }]}></Select>
<Select
options={travelAgencyList}
value={currentTravelAgency}
onChange={handleTravelAgencyChange}
loading={dataLoading}
showSearch
filterOption={false}
onSearch={handleTravelAgencySearch}
notFoundContent={null}
>
</Select>
</Form.Item>
<Form.Item
label={t('account:management.role')}
label={t('account:roleName')}
name='roleId'
rules={[
{
@ -321,40 +313,33 @@ function Management() {
},
]}
>
<Select options={roleAllList}>
<Select
options={roleAllList}
filterOption={false}
notFoundContent={null}
>
</Select>
</Form.Item>
</Modal>
<Space direction='vertical' style={{ width: '100%' }}>
<Title level={3}>{t('account:management.tile')}</Title>
<Space direction='vertical' className='w-full'>
<Title level={3}>{t('account:accountList')}</Title>
<SearchForm
fieldsConfig={{
shows: ['username', 'realname', 'dates'],
shows: ['username', 'agency'],
fieldProps: {
dates: { label: t('group:ArrivalDate') },
username: { label: t('account:username') + '/' + t('account:realname') },
agency: { label: t('account:travelAgency') },
},
sort: { username: 1, agency: 2},
}}
onSubmit={(err, formValues, filedsVal) => {
console.info(formValues)
setDataLoading(true)
searchAccountByCriteria(formValues)
.catch(ex => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
})
})
.finally(() => {
setDataLoading(false)
})
onSubmit={() => {
handelAccountSearch()
}}
/>
<Row>
<Col span={24}>
<Space>
<Button onClick={() => setAccountModalOpen(true)}>{t('account:management.newAccount')}</Button>
<Button onClick={() => onNewAccount()}>{t('account:newAccount')}</Button>
</Space>
</Col>
</Row>

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

@ -1,115 +1,29 @@
import { useState, useEffect } from 'react'
import { Row, Col, Space, Button, Table, Select, TreeSelect, Typography, Modal, App, Form, Input } from 'antd'
import { ExclamationCircleFilled } from '@ant-design/icons'
import { useTranslation } from 'react-i18next'
import useFormStore from '@/stores/Form'
import useAuthStore from '@/stores/Auth'
import useAccountStore from '@/stores/Account'
import { fetchRoleList } from '@/stores/Account'
import SearchForm from '@/components/SearchForm'
import RequireAuth from '@/components/RequireAuth'
import { PERM_ROLE_NEW } from '@/config'
import useAccountStore, { fetchPermissionList, fetchPermissionListByRoleId, fetchRoleList } from '@/stores/Account'
import { isEmpty } from '@/utils/commons'
import {
SyncOutlined,
} from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Space, Table, Tag, TreeSelect, Typography } from 'antd'
import dayjs from 'dayjs'
import { useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
const { Title } = Typography
const permissionData = [
{
title: '海外供应商',
value: 'oversea-0',
key: 'oversea-0',
children: [
{
title: '所有海外功能',
value: 'oversea-0-0',
key: 'oversea-0-0',
},
],
},
{
title: '机票管理',
value: '0-0',
key: '0-0',
children: [
{
title: '录入机票价格',
value: '0-0-0',
key: '0-0-0',
},
],
},
{
title: '产品管理',
value: '0-1',
key: '0-1',
children: [
{
title: '搜索供应商产品',
value: 'B-1-0',
key: 'B-1-0',
},
{
title: '录入产品价格',
value: '0-1-0',
key: '0-1-0',
},
{
title: '新增产品描述',
value: '0-1-1',
key: '0-1-1',
},
{
title: '复制供应商产品信息',
value: '0-1-2',
key: '0-1-2',
},
],
},
{
title: '账号管理',
value: '2-1',
key: '2-1',
children: [
{
title: '搜索账号',
value: '2-1-01',
key: '2-1-01',
},
{
title: '新增账号',
value: '2-1-11',
key: '2-1-11',
},
{
title: '禁用账号',
value: '2-1-21',
key: '2-1-21',
},
{
title: '重置账号密码',
value: '2-1-31',
key: '2-1-31',
},
{
title: '新增角色',
value: '2-1-41',
key: '2-1-41',
},
],
},
]
function RoleList() {
const { t } = useTranslation()
const roleListColumns = [
{
title: t('account:rolename'),
title: t('account:roleName'),
dataIndex: 'role_name',
render: roleRender
},
{
title: t('account:createdOn'),
dataIndex: 'created_on',
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD HH:mm:ss'))
},
{
title: t('account:action'),
@ -118,57 +32,112 @@ function RoleList() {
},
]
function roleRender(text, role) {
return (
<Button type='link' onClick={() => onRoleSeleted(role)}>{text}</Button>
)
}
function actionRender(text, account) {
return (
<Space key='actionRenderSpace' size='middle'>
<Button type='link' key='disable' onClick={() => showDisableConfirm(account)}>{t('account:action.disable')}</Button>
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
</Space>
)
function actionRender(_, role) {
if (role.role_id == 1) {
return (<Tag icon={<SyncOutlined spin />} color='warning'>不能修改</Tag>)
} else {
return (
<Button type='link' key='edit' onClick={() => onRoleSeleted(role)}>{t('account:action.edit')}</Button>
)
}
}
const onPermissionChange = (newValue) => {
console.log('onChange ', newValue)
setPermissionValue(newValue)
}
useEffect (() => {
function groupByParam(array, param) {
return array.reduce((result, item) => {
(result[item[param]] = result[item[param]] || []).push(item)
return result
}, {})
}
useEffect(() => {
setDataLoading(true)
fetchRoleList()
.then(r => {
setRoleAllList(r)
})
.finally(() => {
setDataLoading(false)
})
const categoryMap = new Map([
['system', '系统管理'],
['oversea', '海外供应商'],
['domestic', '国内供应商'],
['air-ticket', '机票供应商'],
['products', '产品价格'],
['page', '默认页面'],
]);
const permissionTree = []
fetchPermissionList()
.then(r => {
const groupPermissionData = groupByParam(r, 'res_category')
const categoryKeys = Object.keys(groupPermissionData)
categoryKeys.forEach((categoryName) => {
const permissisonList = groupPermissionData[categoryName]
const categoryGroup = {
title: categoryMap.get(categoryName),
value: categoryName,
key: categoryName,
children: permissisonList.map(p => {
return {
disableCheckbox: p.res_id == 1,
title: p.res_name,
value: p.res_id,
key: p.res_id,
}
})
}
permissionTree.push(categoryGroup)
})
setPermissionTreeData(permissionTree)
})
}, [])
const [permissionValue, setPermissionValue] = useState(['0-0-0'])
const [permissionValue, setPermissionValue] = useState([])
const [permissionTreeData, setPermissionTreeData] = useState([])
const [isRoleModalOpen, setRoleModalOpen] = useState(false)
const [dataLoading, setDataLoading] = useState(false)
const [roleAllList, setRoleAllList] = useState([])
const [roleForm] = Form.useForm()
const [saveOrUpdateRole] =
const [saveOrUpdateRole, newEmptyRole] =
useAccountStore((state) =>
[state.saveOrUpdateRole])
[state.saveOrUpdateRole, state.newEmptyRole])
const { notification, modal } = App.useApp()
const { notification } = App.useApp()
const onRoleSeleted = async (role) => {
const onRoleSeleted = (role) => {
fetchPermissionListByRoleId({ role_id: role.role_id })
.then(result => {
role.res_array = result.map(r => r.res_id)
roleForm.setFieldsValue(role)
})
setRoleModalOpen(true)
}
const onNewRole = () => {
const role = newEmptyRole()
roleForm.setFieldsValue(role)
// selectAccount(account)
// console.info(account)
setRoleModalOpen(true)
}
const onRoleFinish = (values) => {
console.log(values)
saveOrUpdateRole(values)
.then(() => {
fetchRoleList()
.then(r => {
setRoleAllList(r)
})
})
.catch(ex => {
console.info(ex.message)
notification.error({
message: 'Notification',
description: ex.message,
@ -179,7 +148,6 @@ function RoleList() {
}
const onRoleFailed = (error) => {
console.log('Failed:', error)
// form.resetFields()
}
@ -191,72 +159,64 @@ function RoleList() {
autoFocus: true,
htmlType: 'submit',
}}
title={t('account:management.newRole')}
title={t('account:detail')}
open={isRoleModalOpen} onOk={() => setRoleModalOpen(false)} onCancel={() => setRoleModalOpen(false)}
destroyOnClose={true}
clearOnDestroy={true}
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
name='RoleForm'
form={roleForm}
layout='vertical'
size='large'
style={{
maxWidth: 600,
}}
onFinish={onRoleFinish}
onFinishFailed={onRoleFailed}
autoComplete='off'
>
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'
>
<Input styles={{display: 'none'}} />
</Form.Item>
<Form.Item
label={t('account:management.roleName')}
name='role_name'
rules={[
{
required: true,
message: t('account:Validation.roleName'),
},
]}
>
<Input />
</Form.Item>
<Form.Item label={t('account:management.permission')}>
<TreeSelect treeData={permissionData} value={permissionValue}
dropdownStyle={{
maxHeight: 600,
overflow: 'auto',
}}
placement='bottomLeft'
showSearch
allowClear
multiple
treeDefaultExpandAll
treeLine={true}
onChange={onPermissionChange}
treeCheckable={true}
showCheckedStrategy={TreeSelect.SHOW_CHILD}
placeholder={'Please select'}
style={{
width: '100%',
}} />
</Form.Item>
<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' style={{ width: '100%' }}>
<Title level={3}>{t('account:management.roleList')}</Title>
<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={() => setRoleModalOpen(true)}>{t('account:management.newRole')}</Button>
<Button onClick={() => onNewRole()}>{t('account:newRole')}</Button>
</RequireAuth>
</Space>
</Col>
@ -268,6 +228,7 @@ function RoleList() {
loading={dataLoading}
rowKey='role_id'
pagination={{
pageSize: 20,
showQuickJumper: true,
showLessItems: true,
showSizeChanger: true,

@ -1,6 +1,4 @@
import { NavLink, useNavigate } from "react-router-dom";
import { useState } from "react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, App, Steps } from "antd";
import { formatDate, isNotEmpty } from "@/utils/commons";
import { AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons";
@ -82,7 +80,7 @@ function Index() {
fieldsConfig={{
shows: ['referenceNo', 'invoiceStatus', 'dates'],
fieldProps: {
referenceNo: { col: 5 },
referenceNo: { col: 7 },
invoiceStatus: { col: 4},
dates: { col: 10 },
},
@ -103,7 +101,7 @@ function Index() {
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={toJS(invoiceList)} />
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={(invoiceList)} />
</Col>
</Row>
</Space>

@ -0,0 +1,247 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { App, Button, Collapse, Table, Space, Divider } from 'antd';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
import { useTranslation } from 'react-i18next';
import useProductsStore, { postProductsQuoteAuditAction, } from '@/stores/Products/Index';
import { cloneDeep, isEmpty } from '@/utils/commons';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
// import PrintContractPDF from './PrintContractPDF';
import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config';
const Header = ({ title, agency, refresh, ...props }) => {
const { travel_agency_id, use_year, audit_state } = useParams();
const { t } = useTranslation();
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const { message, notification } = App.useApp();
const handleAuditItem = (state, row) => {
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => {
if (json.errcode === 0) {
message.success(json.errmsg);
if (typeof refresh === 'function') {
refresh();
}
}
})
.catch((ex) => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
});
});
};
return (
<div className='flex justify-end items-center gap-4 h-full'>
<div className='grow'>
<h2 className='m-0 leading-tight'>
{title}
<Divider type={'vertical'} />
{(use_year || '').replace('all', '')}
</h2>
</div>
{/* <Button size='small'>{t('Copy')}</Button> */}
{/* <Button size='small'>{t('Import')}</Button> */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
<Link className='px-2' to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`}>
{t('Edit')}
</Link>
</RequireAuth>
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' type={'primary'} onClick={() => handleAuditItem('2', agency)}>
{t('products:auditStateAction.Published')}
</Button>
</RequireAuth>
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditItem('2', agency)}>
{t('products:auditStateAction.Approved')}
</Button> */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' type={'primary'} danger ghost onClick={() => handleAuditItem('3', agency)}>
{t('products:auditStateAction.Rejected')}
</Button>
</RequireAuth>
{/* todo: export, 审核完成之后才能导出 */}
<Button size='small'>{t('Print')} PDF</Button>
{/* <PrintContractPDF /> */}
</div>
);
};
const PriceTable = ({ productType, dataSource, refresh }) => {
const { t } = useTranslation('products');
const { travel_agency_id, use_year, audit_state } = useParams();
const isPermitted = useAuthStore(state => state.isPermitted);
const [loading, activeAgency] = useProductsStore((state) => [state.loading, state.activeAgency]);
const [setEditingProduct] = useProductsStore((state) => [state.setEditingProduct]);
const { message, notification } = App.useApp();
const stateMapVal = useProductsAuditStatesMapVal();
const [renderData, setRenderData] = useState(dataSource);
// console.log(dataSource);
const handleAuditPriceItem = (state, row, rowIndex) => {
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => {
if (json.errcode === 0) {
message.success(json.errmsg);
if (typeof refresh === 'function') {
// refresh(); // debug: ,
// const newData = structuredClone(renderData);
const newData = cloneDeep(renderData);
newData.splice(rowIndex, 1, {...row, audit_state_id: state, });
setRenderData(newData);
}
}
})
.catch((ex) => {
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
});
});
};
const rowStyle = (r, tri) => {
const trCls = tri%2 !== 0 ? ' bg-stone-50' : '';
const [infoI, quoteI] = r.rowSpanI;
const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : '';
return [trCls, bigTrCls].join(' ');
};
const columns = [
{ key: 'title', dataIndex: ['info', 'title'], width: '16rem', title: t('Title'), onCell: (r, index) => ({ rowSpan: r.rowSpan, }), className: 'bg-white', render: (text, r) => {
const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
return isPermitted(PERM_PRODUCTS_OFFER_PUT) ? <Link to={`/products/${travel_agency_id}/${use_year}/${audit_state}/edit`} onClick={() => setEditingProduct(r.info)}>{title}</Link> : title;
} },
// ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []),
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
{ key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('GroupSize'),
render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`,
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('UseDates'),
render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''),
},
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') },
{
key: 'state',
title: t('State'),
render: (_, r) => {
const stateCls = ` text-${stateMapVal[`${r.audit_state_id}`]?.color} `;
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
},
},
{
title: '',
key: 'action',
render: (_, r, ri) =>
(Number(r.audit_state_id)) === 0 ? (
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Space>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</Space>
</RequireAuth>
) : null,
},
];
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, }} dataSource={renderData} rowKey={(r) => r.id} />;
};
/**
*
*/
const TypesPanels = (props) => {
const { t } = useTranslation();
const [loading, agencyProducts] = useProductsStore((state) => [state.loading, state.agencyProducts]);
// console.log(agencyProducts);
const productsTypes = useProductsTypes();
const [activeKey, setActiveKey] = useState([]);
const [showTypes, setShowTypes] = useState([]);
useEffect(() => {
// ; , ; ,
const hasDataTypes = Object.keys(agencyProducts);
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => ({
...ele,
extra: t('Table.Total', { total: agencyProducts[ele.value].length }),
children: (
<PriceTable
// loading={loading}
productType={ele.value}
dataSource={agencyProducts[ele.value].reduce(
(r, c, ri) =>
r.concat(
c.quotation.map((q, i) => ({
...q,
weekdays: q.weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`weekdaysShort.${w}`))
.join(', '),
info: c.info,
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...r, [clgc.lgc]: clgc}), {}),
rowSpan: i === 0 ? c.quotation.length : 0,
rowSpanI: [ri, i],
}))
),
[]
)}
refresh={props.refresh}
/>
),
}));
setShowTypes(_show);
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
return () => {};
}, [productsTypes, agencyProducts]);
const onCollapseChange = (_activeKey) => {
setActiveKey(_activeKey);
};
return <Collapse items={showTypes} activeKey={activeKey} onChange={onCollapseChange} />;
};
const Audit = ({ ...props }) => {
const { travel_agency_id, use_year, audit_state } = useParams();
const [loading, activeAgency, getAgencyProducts] = useProductsStore((state) => [state.loading, state.activeAgency, state.getAgencyProducts]);
const handleGetAgencyProducts = () => {
getAgencyProducts({ travel_agency_id, use_year, audit_state });
};
useEffect(() => {
handleGetAgencyProducts();
return () => {};
}, [travel_agency_id]);
return (
<>
<SecondHeaderWrapper header={<Header title={activeAgency.travel_agency_name} agency={activeAgency} refresh={handleGetAgencyProducts} />} loading={loading} >
{/* debug: 0 */}
{/* <PrintContractPDF /> */}
<TypesPanels refresh={handleGetAgencyProducts} />
</SecondHeaderWrapper>
</>
);
};
export default Audit;

File diff suppressed because it is too large Load Diff

@ -0,0 +1,371 @@
import React, { useState } from 'react';
import { Button, Card, Checkbox, Col, DatePicker, Form, Input, Row, Select, Space, Tag, Table, InputNumber } from 'antd';
import { CloseOutlined } from '@ant-design/icons';
import dayjs from 'dayjs';
import { useDatePresets } from '@/hooks/useDatePresets';
const { Option } = Select;
const { RangePicker } = DatePicker;
const BatchImportPrice = ({ onBatchImportData }) => {
const [form] = Form.useForm();
const [tags, setTags] = useState([]);
const [minPeople, setMinPeople] = useState('');
const [maxPeople, setMaxPeople] = useState('');
const [checkedDays, setCheckedDays] = useState([]);
const [tableData, setTableData] = useState([]);
const [sendData, setSendData] = useState(null);
const presets = useDatePresets();
const handleTagClose = (removedTag) => {
setTags(tags.filter(tag => tag !== removedTag));
};
const handleInputConfirm = () => {
if (minPeople && maxPeople) {
const tag = `${minPeople}-${maxPeople}`;
if (tags.indexOf(tag) === -1) {
setTags([...tags, tag]);
}
}
setMinPeople('');
setMaxPeople('');
};
const handleCheckboxChange = (checkedValues) => {
setCheckedDays(checkedValues);
};
const generateTableData = () => {
const values = form.getFieldsValue();
const weekdays = checkedDays.join(',');
let tempSendData = [];
console.log("values",values)
// items
values.items.forEach((item, index) => {
// validPeriods
let tempValidPeriods = []
item.validPeriods?.forEach((period) => {
console.log("period",period)
const validPeriod = period.validPeriod.map(date => date.format('YYYY-MM-DD')).join('~');
tempValidPeriods.push(validPeriod)
// tempSendData tag
});
const priceType = `批量设置价格 ${index + 1} ${item.currency}/${item.type}`
let tempData = []
const unit_name = item.type
const currency = item.currency
tags.forEach((tag) => {
tempValidPeriods.forEach(validPeriod => {
const group_size_min = tag.split('-')[0]
const group_size_max = tag.split('-')[1]
let unit_id = null
const use_dates_start = validPeriod.split('~')[0]
const use_dates_end = validPeriod.split('~')[1]
if (unit_name === "每人") {
unit_id = 0
} else {
unit_id = 1
}
tempData.push({ group_size_min, group_size_max, validPeriod, unit_id, unit_name, use_dates_start, use_dates_end, currency, weekdays, tag, priceType })
});
})
console.log("tempData", tempData)
tempSendData.push(...tempData)
});
//
setSendData([...tempSendData]); // 使 setSendData
const data = [];
values.items.forEach((item, index) => {
item.validPeriods?.forEach((period, idx) => {
const row = {
key: `${index}-${idx}`,
priceType: `批量设置价格 ${index + 1} ${item.currency}/${item.type}`,
validPeriod: period.validPeriod.map(date => date.format('YYYY-MM-DD')).join('~'),
currency: item.currency,
type: item.type,
};
tags.forEach((tag, tagIndex) => {
row[`adultPrice${tagIndex + 1}`] = 0; // Initialize with 0
row[`childrenPrice${tagIndex + 1}`] = 0; // Initialize with 0
});
data.push(row);
});
});
// setSendData([...tempSendData,data]);
setTableData(data);
// onBatchImportData(data); //
};
const handleTableChange = (age_type, value, tag, priceType) => {
if (age_type === 'adult_cost') {
console.log("sendData", sendData)
const updatedSendData = sendData.map((item) => {
console.log("item.priceType === priceType", item.priceType === priceType)
console.log("item.priceType", item.priceType)
console.log("priceType", priceType)
if (item.priceType === priceType && item.tag === tag) {
return {
...item,
adult_cost: value, // adult_cost
};
}
return item; //
});
// sendData
console.log("updatedSendData", updatedSendData)
onBatchImportData(updatedSendData);
setSendData(updatedSendData);
} else {
const updatedSendData = sendData.map((item) => {
if (item.priceType === priceType && item.tag === tag) {
return {
...item,
child_cost: value, // child_cost
};
}
return item; //
});
// sendData
onBatchImportData(updatedSendData);
setSendData(updatedSendData);
}
};
const generatePeopleColumns = () => {
const columns = [];
tags.forEach((tag, index) => {
columns.push({
title: tag,
children: [
{
title: '成人价',
dataIndex: `adultPrice${index + 1}`,
key: `adultPrice${index + 1}`,
render: (text, record, rowIndex) => {
const sameTagRecords = tableData.filter(item => item.priceType === record.priceType);
const firstTagIndex = tableData.findIndex(item => item.priceType === record.priceType && item.validPeriod === sameTagRecords[0].validPeriod);
if (rowIndex === firstTagIndex) {
return {
children: (
<InputNumber
formatter={value => `${value}`}
parser={value => value.replace(/[^\d]/g, '')}
onChange={(value) => handleTableChange('adult_cost', value, tag, record.priceType)}
/>
),
props: {
rowSpan: sameTagRecords.length,
},
};
} else {
return {
props: {
rowSpan: 0,
},
};
}
},
},
{
title: '儿童价',
dataIndex: `childrenPrice${index + 1}`,
key: `childrenPrice${index + 1}`,
render: (text, record, rowIndex) => {
const sameTagRecords = tableData.filter(item => item.priceType === record.priceType);
const firstTagIndex = tableData.findIndex(item => item.priceType === record.priceType && item.validPeriod === sameTagRecords[0].validPeriod);
if (rowIndex === firstTagIndex) {
return {
children: (
<InputNumber
formatter={value => `${value}`}
parser={value => value.replace(/[^\d]/g, '')}
onChange={(value) => handleTableChange('child_cost', value, tag, record.priceType)}
/>
),
props: {
rowSpan: sameTagRecords.length,
},
};
} else {
return {
props: {
rowSpan: 0,
},
};
}
},
}
]
});
});
return columns;
};
const columns = [
{
title: ' ',
dataIndex: 'priceType',
key: 'priceType',
width: "10%",
render: (text, record, index) => {
const obj = {
children: text,
props: {},
};
if (index > 0 && text === tableData[index - 1].priceType) {
obj.props.rowSpan = 0;
} else {
obj.props.rowSpan = tableData.filter(item => item.priceType === text).length;
}
return obj;
},
},
{
title: '有效期\\人等',
dataIndex: 'validPeriod',
key: 'validPeriod',
width: "15%"
},
...generatePeopleColumns(),
];
return (
<div>
<Card
size="small"
title="选择人等、周末"
style={{ marginBottom: 16 }}
>
<Row>
<Col>
<Input.Group compact style={{ marginTop: 10 }}>
<Input
style={{ width: 100, textAlign: 'center' }}
placeholder="Start"
value={minPeople}
onChange={(e) => setMinPeople(e.target.value)}
/>
<Input
style={{ width: 30, borderLeft: 0, pointerEvents: 'none', backgroundColor: '#fff' }}
placeholder="~"
disabled
/>
<Input
style={{ width: 100, textAlign: 'center', borderLeft: 0 }}
placeholder="End"
value={maxPeople}
onChange={(e) => setMaxPeople(e.target.value)}
/>
</Input.Group>
</Col>
<Col>
<Button size="small" type="primary" onClick={handleInputConfirm} style={{ marginLeft: 12, marginTop: 12 }}>
添加人等
</Button>
</Col>
</Row>
<div style={{ marginTop: 16 }}>
{tags.map((tag) => (
<Tag key={tag} closable onClose={() => handleTagClose(tag)}>
{tag}
</Tag>
))}
</div>
<Checkbox.Group
style={{ marginTop: 16 }}
options={['5', '6', '7']}
onChange={handleCheckboxChange}
/>
</Card>
<Form
labelCol={{ span: 6 }}
wrapperCol={{ span: 18 }}
form={form}
name="dynamic_form_complex"
style={{ maxWidth: 600 }}
autoComplete="off"
initialValues={{ items: [{}] }}
>
<Form.List name="items">
{(fields, { add, remove }) => (
<div style={{ display: 'flex', rowGap: 16, flexDirection: 'column' }}>
{fields.map((field, index) => (
<Card
size="small"
title={`批量设置价格 ${index + 1}`}
key={field.key}
extra={<CloseOutlined onClick={() => remove(field.name)} />}
>
<Form.Item label="类型" name={[field.name, 'type']}>
<Select placeholder="选择类型">
<Option value="每人">每人</Option>
<Option value="每团">每团</Option>
</Select>
</Form.Item>
<Form.Item label="币种" name={[field.name, 'currency']}>
<Select placeholder="选择币种">
<Option value="CNY">CNY</Option>
<Option value="USD">USD</Option>
</Select>
</Form.Item>
<Form.Item label="有效期">
<Form.List name={[field.name, 'validPeriods']}>
{(periodFields, periodOpt) => (
<div style={{ display: 'flex', flexDirection: 'column', rowGap: 16 }}>
{periodFields.map((periodField, idx) => (
<Space key={periodField.key}>
<Form.Item noStyle name={[periodField.name, 'validPeriod']}>
<RangePicker allowClear={true} inputReadOnly={true} presets={presets} placeholder={['From', 'Thru']}/>
</Form.Item>
<CloseOutlined onClick={() => periodOpt.remove(periodField.name)} />
</Space>
))}
<Button type="dashed" onClick={() => periodOpt.add()} block>
+ 新增有效期
</Button>
</div>
)}
</Form.List>
</Form.Item>
</Card>
))}
<Button type="dashed" onClick={() => add()} block>
+ 新增价格设置
</Button>
</div>
)}
</Form.List>
</Form>
<Button type="primary" onClick={generateTableData} style={{ marginTop: 20 }}>
生成表格
</Button>
{tableData.length > 0 && (
<div style={{ overflowX: 'auto' }}>
<Table
style={{ marginTop: 20 }}
columns={columns}
dataSource={tableData}
pagination={false}
/>
</div>
)}
</div>
);
};
export default BatchImportPrice;

@ -0,0 +1,160 @@
import { useState, useEffect } from 'react';
import { App, Form, Modal, DatePicker, Divider } from 'antd';
import { isEmpty, objectMapper } from '@/utils/commons';
import { useTranslation } from 'react-i18next';
import SearchInput from '@/components/SearchInput';
import DeptSelector from '@/components/DeptSelector';
import ProductsTypesSelector, { fetchVendorList } from '@/components/ProductsTypesSelector';
import dayjs from 'dayjs';
import arraySupport from 'dayjs/plugin/arraySupport';
import { copyAgencyDataAction } from '@/stores/Products/Index';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
dayjs.extend(arraySupport);
export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const isPermitted = useAuthStore((state) => state.isPermitted);
useEffect(() => {
onFormInstanceReady(form);
}, []);
const onValuesChange = (changeValues, allValues) => {};
return (
<Form layout='horizontal' form={form} name='form_in_modal' initialValues={initialValues} onValuesChange={onValuesChange}>
{action === '#' && <Form.Item name='agency' label={`${t('products:CopyFormMsg.target')}${t('products:Vendor')}`} rules={[{ required: true, message: t('products:CopyFormMsg.requiredVendor') }]}>
<SearchInput
placeholder={t('products:Vendor')}
mode={null}
maxTagCount={0}
fetchOptions={fetchVendorList}
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
/>
</Form.Item>}
<Form.Item name={`products_types`} label={t('products:ProductType')}>
<ProductsTypesSelector maxTagCount={1} mode={'multiple'} placeholder={t('All')} />
</Form.Item>
{action === '#' && <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
<Form.Item name={`dept`} label={t('products:Dept')} rules={[{ required: false, message: t('products:CopyFormMsg.requiredDept') }]}>
<DeptSelector isLeaf={true} />
</Form.Item>
</RequireAuth>}
<Form.Item name={'source_use_year'} label={`${t('products:CopyFormMsg.Source')}${t('products:UseYear')}`} initialValue={dayjs([source.sourceYear, 1, 1])}>
<DatePicker picker='year' allowClear />
</Form.Item>
<Form.Item name={'target_use_year'} label={`${t('products:CopyFormMsg.target')}${t('products:UseYear')}`}>
<DatePicker picker='year' allowClear disabledDate={(current) => current <= dayjs([source.sourceYear, 12, 31])} />
</Form.Item>
</Form>
);
};
const formValuesMapper = (values) => {
const destinationObject = {
'agency': {
key: 'target_agency',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'source_use_year': [{ key: 'source_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }],
'target_use_year': [{ key: 'target_use_year', transform: (arrVal) => (arrVal ? arrVal.format('YYYY') : '') }],
'products_types': {
key: 'products_types',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
'dept': {
key: 'dept',
transform: (value) => {
return Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '';
},
},
};
let dest = {};
const { agency, year, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
}
}
// omit empty
// Object.keys(dest).forEach((key) => (dest[key] == null || dest[key] === '' || dest[key].length === 0) && delete dest[key]);
return dest;
};
/**
*
*/
export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubmit, onCancel, initialValues, loading, copyModalVisible, setCopyModalVisible }) => {
const { t } = useTranslation();
const { notification, message } = App.useApp();
const [formInstance, setFormInstance] = useState();
const [copyLoading, setCopyLoading] = useState(false);
const handleCopyAgency = async (param) => {
param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency;
setCopyLoading(true);
console.log(param);
const toID = param.target_agency;
const success = await copyAgencyDataAction({...param, source_agency: source.sourceAgency.travel_agency_id});
setCopyLoading(false);
success ? message.success(t('Success')) : message.error(t('Failed'));
if (typeof onSubmit === 'function') {
onSubmit(param);
}
// setCopyModalVisible(false);
// navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
};
return (
<Modal
width={600}
open={open}
title={`${t('Copy')}${t('products:#')}${t('products:Offer')}`}
okText='确认'
// cancelText='Cancel'
okButtonProps={{
autoFocus: true,
}}
confirmLoading={copyLoading}
onCancel={() => {
onCancel();
formInstance?.resetFields();
}}
destroyOnClose
onOk={async () => {
try {
const values = await formInstance?.validateFields();
// formInstance?.resetFields();
const dest = formValuesMapper(values);
handleCopyAgency(dest);
} catch (error) {
console.log('Failed:', error);
}
}}>
<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT}>
<div className='py-2'>
{t('products:CopyFormMsg.Source')}: {source.sourceAgency.travel_agency_name}
<Divider type={'vertical'} />
{source.sourceYear}
</div>
</RequireAuth>
<CopyProductsForm action={action}
source={source}
initialValues={initialValues}
onFormInstanceReady={(instance) => {
setFormInstance(instance);
}}
/>
</Modal>
);
};
export default CopyProductsFormModal;

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

@ -0,0 +1,55 @@
import React, { useState } from 'react';
import { DatePicker, Button } from 'antd';
import dayjs from 'dayjs';
import { useDatePresets } from '@/hooks/useDatePresets';
const addValidityWithWeekend = ({ onDateChange }) => {
const dateFormat = 'YYYY/MM/DD';
const { RangePicker } = DatePicker;
const [dateRange, setDateRange] = useState(null);
const [selectedDays, setSelectedDays] = useState([]);
const presets = useDatePresets();
const days = [
'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'
];
const handleChange = (date, dateString) => {
console.log("dateString",dateString)
onDateChange({ dateRange: dateString, selectedDays });
};
const handleDayClick = (day) => {
setSelectedDays((prevSelectedDays) => {
const updatedDays = prevSelectedDays.includes(day)
? prevSelectedDays.filter((d) => d !== day)
: [...prevSelectedDays, day];
onDateChange({ dateRange, selectedDays: updatedDays });
return updatedDays;
});
};
return (
<div>
<h4>Data</h4>
{<RangePicker allowClear={true} inputReadOnly={true} presets={presets} placeholder={['From', 'Thru']} onChange={handleChange}/>}
<h4>Weekdays</h4>
<div>
{days.map((day, index) => (
<Button
key={index}
type={selectedDays.includes(day) ? 'primary' : 'default'}
onClick={() => handleDayClick(day)}
style={{ margin: '5px' }}
>
{day}
</Button>
))}
</div>
</div>
);
};
export default addValidityWithWeekend;

@ -0,0 +1,118 @@
import { useEffect, useState } from 'react';
import { Link, useNavigate } from 'react-router-dom';
import { App, Space, Table, Button, Modal, Divider } from 'antd';
import SearchForm from '@/components/SearchForm';
import dayjs from 'dayjs';
import arraySupport from 'dayjs/plugin/arraySupport';
import { useTranslation } from 'react-i18next';
import useProductsStore, { copyAgencyDataAction } from '@/stores/Products/Index';
import useFormStore from '@/stores/Form';
import { objectMapper } from '@/utils/commons';
import CopyProductsFormModal from './Detail/CopyProducts';
dayjs.extend(arraySupport);
function Index() {
const { notification, message } = App.useApp();
const navigate = useNavigate()
const { t } = useTranslation();
const [loading, agencyList, searchAgency] = useProductsStore((state) => [state.loading, state.agencyList, state.searchAgency]);
const [searchValues, setSearchValues] = useProductsStore((state) => [state.searchValues, state.setSearchValues]);
const formValuesToSub = useFormStore(state => state.formValuesToSub);
const useYear = formValuesToSub.year;
const handleSearchAgency = (formVal = undefined) => {
const { starttime, endtime, ...param } = formVal || formValuesToSub;
const searchParam = objectMapper(param, { agency: 'travel_agency_ids', startdate: 'edit_date1', enddate: 'edit_date2', year: 'use_year' });
setSearchValues(searchParam);
searchAgency(searchParam);
}
const [copyModalVisible, setCopyModalVisible] = useState(false);
const [sourceAgency, setSourceAgency] = useState({});
const [copyAction, setCopyAction] = useState();
const [copyLoading, setCopyLoading] = useState(false);
const handleCopyAgency = async (param) => {
// setCopyLoading(true);
const toID = param.target_agency;
// const success = await copyAgencyDataAction({...param, source_agency: sourceAgency.travel_agency_id});
// setCopyLoading(false);
// success ? message.success('') : message.error('');
setCopyModalVisible(false);
navigate(`/products/${toID}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`);
};
const openCopyModal = (from, action) => {
setSourceAgency(from);
setCopyAction(action);
setCopyModalVisible(true);
};
useEffect(() => {
// handleSearchAgency();
}, []);
const showTotal = (total) => t('Table.Total', { total });
const columns = [
{ title: t('products:Vendor'), key: 'vendor', dataIndex: 'travel_agency_name' },
{ title: t('products:CreatedBy'), key: 'poster_by', dataIndex: 'poster_name' },
{ title: t('products:CreateDate'), key: 'poster_date', dataIndex: 'poster_date' },
{ title: t('products:AuState'), key: 'audit_state', dataIndex: 'audit_state' },
{ title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by_name' },
{ title: t('products:AuditDate'), key: 'audit_date', dataIndex: 'audit_date' },
{
title: '',
key: 'action',
render: (_, r) => (
<Space size={'large'}>
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/edit`}>{t('Edit')}</Link>
<Link to={`/products/${r.travel_agency_id}/${searchValues.use_year || 'all'}/${searchValues.audit_state || 'all'}/audit`}>{t('Audit')}</Link>
<Button type='link' size={'small'} onClick={() => openCopyModal(r, '#')}>{t('Copy')+'-'+t('products:#')}</Button>
<Button type='link' size={'small'} onClick={() => openCopyModal(r, 'o')}>{t('Copy')+'-'+t('products:Offer')}</Button>
</Space>
),
},
];
return (
<Space direction='vertical' style={{ width: '100%' }}>
<SearchForm
fieldsConfig={{
shows: ['agency', 'audit_state', 'dates', 'year'],
fieldProps: {
agency: { col: 4 },
dates: { label: t('products:CreateDate') },
keyword: { label: t('products:Title'), col: 4 },
year: { col: 4, rules: [{ required: true }] },
},
sort: { agency: 1, audit_state: 2, keyword: 100 },
}}
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
audit_state: { value: '', label: t('products:State') },
year: dayjs(), // .add(1, 'year'),
}}
onSubmit={(err, formVal, filedsVal) => {
handleSearchAgency(formVal);
}}
/>
<Table bordered={true} columns={columns} dataSource={agencyList} pagination={{ defaultPageSize: 20, showTotal: showTotal }} loading={loading} rowKey={'travel_agency_id'} />
{/* 复制弹窗 */}
<CopyProductsFormModal
open={copyModalVisible}
action={copyAction}
source={{ sourceAgency, sourceYear: useYear }}
onCancel={() => setCopyModalVisible(false)}
onSubmit={(formVal) => {
handleCopyAgency(formVal);
}}
{...{copyModalVisible, setCopyModalVisible}}
/>
</Space>
);
}
export default Index;

@ -1,19 +1,19 @@
import { useParams } from "react-router-dom"
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App } from 'antd'
import {
FileOutlined
} from '@ant-design/icons';
} from '@ant-design/icons'
import { usingStorage } from '@/hooks/usingStorage'
import useReservationStore from '@/stores/Reservation'
import { useTranslation } from 'react-i18next'
import BackBtn from '@/components/BackBtn'
const { Title, Paragraph } = Typography;
const { TextArea } = Input;
const { Title, Paragraph } = Typography
const { TextArea } = Input
function Detail() {
const { t } = useTranslation();
const { t } = useTranslation()
const confirmationListColumns = [
{
@ -43,9 +43,9 @@ function Detail() {
];
function detailTextRender(text, confirm) {
const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, "\n\n");
const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, '\n\n');
return (
<div style={{whiteSpace: 'pre-line'}}>
<div className='whitespace-pre-line'>
{formattedText}
</div>
);
@ -56,7 +56,7 @@ function Detail() {
<>
{confirm.attachmentList.map(attch => {
return (
<Tag bordered={false} icon={<FileOutlined />}><a href={attch.file_url} target="_blank">{attch.file_name}</a></Tag>
<Tag bordered={false} icon={<FileOutlined />}><a href={attch.file_url} target='_blank'>{attch.file_name}</a></Tag>
)}
)}
</>
@ -65,7 +65,7 @@ function Detail() {
function confirmRender(text, confirm) {
return (
<Button type="link" onClick={() => showConfirmModal(confirm)}>{t('Confirm')}</Button>
<Button type='link' onClick={() => showConfirmModal(confirm)}>{t('Confirm')}</Button>
);
}
@ -96,7 +96,7 @@ function Detail() {
const showConfirmModal = (confirm) => {
setIsModalOpen(true);
const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, "\n\n");
const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, '\n\n');
setConfirmText(formattedText);
selectConfirmation(confirm);
};
@ -140,7 +140,7 @@ function Detail() {
<Title level={4}>{t('group:ConfirmationDetails')}</Title>
<Paragraph>
<blockquote>
<div style={{whiteSpace: 'pre-line'}}>
<div className='whitespace-pre-line'>
{confirmText}
</div>
</blockquote>
@ -155,7 +155,7 @@ function Detail() {
}}
/>
</Modal>
<Space direction="vertical" style={{ width: '100%' }}>
<Space direction='vertical' className='w-full'>
<Row gutter={{ md: 24 }}>
<Col span={20}>
<Title level={4}>{t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate};</Title>
@ -165,17 +165,19 @@ function Detail() {
</Col>
</Row>
<Row gutter={{ md: 24 }}>
<Col span={12} style={{height: '100%'}} >
<iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl} frameBorder="0" style={{ width: '100%', height: '600px' }}></iframe>
<Col span={12} className='w-full'>
<iframe id='msdoc-iframe-reservation' title='msdoc-iframe-reservation'
src={reservationPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Button type='link' target='_blank' href={reservationUrl}>{t('Download')} Itinerary</Button>
</Col>
<Col span={12} style={{height: '100%'}} >
<iframe id="msdoc-iframe-name-card" title="msdoc-iframe-name-card" src={nameCardPreviewUrl} frameBorder="0" style={{ width: '100%', height: '600px' }}></iframe>
<Col span={12} className='w-full'>
<iframe id='msdoc-iframe-name-card' title='msdoc-iframe-name-card'
src={nameCardPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Button type='link' target='_blank' href={nameCardUrl}>{t('Download')} Name Card</Button>
</Col>
</Row>
<Row>
<Col span={24}><Space direction="vertical" style={{ width: '100%' }}>
<Col span={24}><Space direction='vertical' className='w-full'>
<Table
bordered
loading={dataLoading}

@ -21,7 +21,7 @@ function Newest() {
const lastDayjs = dayjs().subtract(1, 'day');
const arrivalDatejs = dayjs(record.arrivalDate);
const requiredHighlight = (arrivalDatejs.isAfter(lastDayjs) && arrivalDatejs.isBefore(after3Dayjs, 'day')) && isEmpty(record.guide);
const linkClassName = requiredHighlight ? 'reservation-highlight' : '';
const linkClassName = requiredHighlight ? 'text-white bg-red-500' : '';
return (
<NavLink className={linkClassName} to={`/reservation/${record.reservationId}`}>{text}</NavLink>
)
@ -30,7 +30,7 @@ function Newest() {
{
title: t('group:ArrivalDate'),
dataIndex: 'arrivalDate',
render: (text, record) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')),
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')),
},
{
title: t('group:Pax'),
@ -43,7 +43,7 @@ function Newest() {
{
title: t('group:ResSendingDate'),
dataIndex: 'reservationDate',
render: (text, record) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')),
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')),
},
{
title: t('group:Guide'),
@ -52,7 +52,7 @@ function Newest() {
},
];
function guideRender(text, reservation) {
function guideRender(_, reservation) {
if (reservation.guide === '') {
return (
<Space size='middle'>
@ -69,13 +69,11 @@ function Newest() {
}
}
function cityGuideRender(text, city) {
function cityGuideRender(_, city) {
return (
<Select
showSearch
style={{
width: 280,
}}
className='w-72'
variant='borderless'
allowClear
placeholder='Select a guide'
@ -84,9 +82,6 @@ function Newest() {
onChange={(guideId) => {
setupCityGuide(city.cityId, guideId);
}}
onSearch={(value) => {
// console.log('search:', value);
}}
filterOption={(input, option) =>
(option?.label ?? '').toLowerCase().includes(input.toLowerCase())
}
@ -177,7 +172,7 @@ function Newest() {
centered
open={isModalOpen} onOk={handleOk} onCancel={handleCancel}
>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='vertical' className='w-full'>
<Row>
<Col span={24}>
<Table
@ -203,15 +198,12 @@ function Newest() {
</Row>
</Space>
</Modal>
<Space direction='vertical' style={{ width: '100%' }}>
<Space direction='vertical' className='w-full'>
<Title level={3}></Title>
<SearchForm
// initialValue={
// {
// }
// }
initialValue={{unconfirmed: true}}
fieldsConfig={{
shows: ['referenceNo', 'dates'],
shows: ['referenceNo', 'dates', 'unconfirmed'],
fieldProps: {
dates: { label: t('group:ArrivalDate') },
},

@ -7,6 +7,8 @@ export default {
colors: {
...colors,
'primary': '#00b96b',
'danger': '#ef4444',
'muted': '#6b7280',
},
extend: {},
},

Loading…
Cancel
Save