Compare commits

...

144 Commits

Author SHA1 Message Date
Lei OT cf47dd44a8 Merge remote-tracking branch 'origin/main' 5 days ago
Lei OT 40ca0bdf8d perf: 滚动条 5 days ago
LiaoYijun 87d1e0d3ae fix: 解决预览 Doc 重复刷新 5 days ago
Lei OT 5694081269 perf: 产品管理: 编辑: 允许不选语种提交 5 days ago
LiaoYijun 2b01fc01ea 2.0.20 1 week ago
LiaoYijun 1e3e9b9942 Merge remote-tracking branch 'origin/火车出票模块' 1 week ago
Ycc 534dd60140 站点更新说明 1 week ago
Ycc 0a6a630441 添加站点英文名 1 week ago
Ycc 7e135f349e 优化账单查询 2 weeks ago
Ycc 894f6e8173 sync 3 weeks ago
Ycc b845280151 车票信息编辑 1 month ago
Ycc 4174f33f08 车票信息编辑 1 month ago
Ycc 093fb9833a 火车计划列表 2 months ago
Ycc eda2b2ac2d 查询火车计划列表 2 months ago
Ycc 7acc8a91d7 初始化火车出票模块 2 months ago
LiaoYijun 062b6e21ee 2.0.19 2 months ago
Lei OT f79b4d4caa perf: 产品管理: 多语言描述限制输入字数2000 2 months ago
LiaoYijun 660bec8594 perf: 增加火车票供应商权限 2 months ago
Lei OT d803964131 2.0.18 3 months ago
Lei OT 80f9bb1b48 fix: 人等排序; fix: 海纳是甲方 3 months ago
LiaoYijun f2f8f0216e 2.0.17 5 months ago
LiaoYijun 1fd49cd587 2.0.16 5 months ago
赵鹏 102774dd64 虚拟订单更换一个新订单,旧虚拟订单用新页面显示。 5 months ago
LiaoYijun 7d4b4d3546 2.0.15 6 months ago
LiaoYijun 0602962a83 fix: enddate 增加本月最后一天和时间 23:59 6 months ago
Lei OT 115f913c7f 2.0.14 7 months ago
Lei OT 813258881f perf: 导出合同: 车费, 增加时段列, 显示价格行的时段信息; 景点的绑定: 仅显示`项目名称-类型`; 包价: 全年的时段不显示, 与车费一样按时段分行 7 months ago
Lei OT 9adf82de12 2.0.13 7 months ago
Lei OT e963322eb0 chore: 更新pagespy 前端sdk 7 months ago
LiaoYijun f2b344cae9 2.0.12 7 months ago
eddie 581d9d1b20 update export doc 7 months ago
eddie 563a744629 包价路线,全年的价格无需展示时间 7 months ago
eddie b7f3f2ae14 导出合同文档备注栏按照序号换行 7 months ago
Jimmy Liow 09755c3665 2.0.11 7 months ago
Jimmy Liow 0bf5d43599 perf: 删除无用的文件 7 months ago
eddie a35b7cf037 移除lodash 7 months ago
eddie 48acdad5b7 移除lodash 7 months ago
eddie 87e7c0acc4 使用1120版本合同导出模版文件 7 months ago
eddie 7b15478640 添加新版本导出模版文件 7 months ago
Eddie 1f85a341f6 Merge branch 'main' of github.com:hainatravel/GHHub 7 months ago
Eddie 78834f8f11 添加新的导出文档的class 类 7 months ago
Ycc df1eae2b86 2.0.10 8 months ago
Ycc fc494b57fc 账单审核状态查询,统计勾选记录的价格,修复城市不完整问题 8 months ago
Jimmy Liow b2df11beb3 2.0.9 8 months ago
Lei OT a2ce2534b6 Merge remote-tracking branch 'origin/main' 8 months ago
Lei OT be3aea599b fix: 报价信显示类型 8 months ago
Jimmy Liow 4b2669227e 2.0.8 8 months ago
Lei OT 503547de78 perf: 所有类型增加【报价信显示】; 默认 `计划和报价信都要显示` 8 months ago
Lei OT 202ba82042 perf: 在【餐费、车费、导游】增加【报价信显示】 8 months ago
Jimmy Liow be0b3d2766 2.0.7 8 months ago
Ycc 0f50b5b482 Merge branch 'main' of github.com:hainatravel/GHHub 8 months ago
Jimmy Liow 2f7e8a9f0f 2.0.6 8 months ago
Jimmy Liow 9a80374e24 feat: 上传发票图片使用 CDN 地址 8 months ago
Ycc aee8f94b40 2.0.6 9 months ago
Ycc 2d3d23e10f 自适应手机端 9 months ago
Ycc e4093fcce7 添加提醒 9 months ago
Ycc e462485057 首次进入页面搜索 9 months ago
Ycc 46235a43ac 2.0.5 9 months ago
Ycc 575e792481 显示审核进度 9 months ago
Ycc 8330244008 机票汇款记录查询界面 9 months ago
Lei OT a7b0c870de 2.0.4 9 months ago
Lei OT aa3b055276 revert: 搜索组件: 产品同步状态 9 months ago
Ycc 4baf742268 添加按团状态和出票状态筛选 9 months ago
Ycc b82c7ef7cb 必填限制和标题修改 9 months ago
Ycc 43fc1e5e2d 打开计划预览 9 months ago
Ycc 324d8c0674 解决modal界面闪动问题 9 months ago
Ycc 9c3cdd6ea6 界面优化 9 months ago
Jimmy Liow 001ce6740d 2.0.3 9 months ago
赵鹏 c29c970c4e 图片上传改为国际地址 9 months ago
Jimmy Liow efd576f487 Merge branch 'main' of github.com:hainatravel/GHHub 9 months ago
Jimmy Liow 65cfb289a2 feat: 增加海外接口地址 9 months ago
Lei OT 7917786e9b perf: 产品管理: `新增产品` 权限 9 months ago
Jimmy Liow 1dd04972c6 feat: 添加“新增产品”权限定义 9 months ago
Jimmy Liow 09d8d99059 perf: 整理 cmd 文件 9 months ago
Ycc cbd7bbc8e1 调整默认查询日期 9 months ago
Ycc e5a0342d7b Merge branch 'main' of github.com:hainatravel/GHHub 9 months ago
Ycc 7296ed954c 下拉选择航空公司 9 months ago
Jimmy Liow 0de97b9ea8 fix: 客服和地接共用产品管理导航 9 months ago
Jimmy Liow 7ccc775791 fix: 缺少 useLocation 9 months ago
Jimmy Liow 9d789723a8 2.0.2 9 months ago
Jimmy Liow bb4eca48f6 perf: 调增产品工具栏按钮及位置 9 months ago
Lei OT fb48ac668f perf: 产品管理: 产品列表树: 名称显示 10 months ago
Ycc 171a5f94c4 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
Ycc 59427a1a3a 添加导出功能,重复添加费用信息 10 months ago
Lei OT ef8046d1ae 2.0.1 10 months ago
Lei OT 024f066075 fix: 产品管理: 编辑页面: 无管理权限的头部参数 10 months ago
Lei OT f37e5302a1 Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT c28d68960e fix: 产品管理: 国内供应商导航 10 months ago
Ycc 0ad7f3a470 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
Jimmy Liow eb410adeb5 2.0.0 10 months ago
Jimmy Liow c88adc4b5e perf: 增加发布小版本批处理 10 months ago
Jimmy Liow ad81a60d55 feat: 增加返回顶部按钮
fix: 合同保存成功后关闭对话框
perf: 优化 main.jsx
10 months ago
Ycc 5aa4d1766d 对齐输入框 10 months ago
Lei OT a7954e1114 2.0.0-rc.9 10 months ago
Ycc bde0e82ece 改为日期控件 10 months ago
Ycc ec2e499abf Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
Ycc 0af62c2e39 必填项标记 10 months ago
Lei OT 8c16e0503e Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT 7c3bfdfefe fix: docx: 人等匹配问题; # 浙江中青旅数据无法导出 10 months ago
Jimmy Liow 6b396633cb feat: 增加获取合同备注异常提示 10 months ago
Lei OT b439abba39 2.0.0-rc.8 10 months ago
Ycc 1407d05f43 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
Ycc c6283f0cc2 新增航班和删除航班功能 10 months ago
Lei OT e66597d4fa fix: 产品管理: 编辑: 语种信息的编辑状态, 保存后更新 10 months ago
Lei OT 5d56479f35 fix: 产品管理: 绑定项目: 搜索的参数travel agency id 10 months ago
Lei OT df1a1f43bf fix: 产品管理: 编辑: 推荐指数, 自由输入 1-1000 10 months ago
Lei OT 3bf491d8ad fix: 产品管理: 编辑: 推荐指数, 自由输入 10 months ago
Lei OT f182ded513 fix: 产品管理: 编辑: 推荐指数 10 months ago
Lei OT 78793d3ddf Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT 4dca7ee290 fix: 产品管: 审核人名称 10 months ago
Jimmy Liow 0cc9a0bb17 2.0.0-rc.7 10 months ago
Jimmy Liow 596dcbd5b0 fix: 增加合同备注表单 10 months ago
Jimmy Liow 6cb61a850a Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
Jimmy Liow 2e535eedfb feat: 完成合同备注功能 10 months ago
Lei OT 903efaead2 perf: docx: 附加项目: `不分人等` 10 months ago
Lei OT 65f2b996aa perf: docx: 人等最大值 10 months ago
Lei OT 592d396d78 perf: 产品编辑: 基本信息, 语种信息的编辑状态 10 months ago
YCC 741e2e17d4 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
Jimmy Liow fc78bd3e58 2.0.0-rc.6 10 months ago
Lei OT ab82ef8d17 Merge remote-tracking branch 'origin/main' 10 months ago
Jimmy Liow 9054538ce3 perf: 增加合同备注界面原型 10 months ago
Lei OT d0af7acae3 perf: 产品管理: 编辑: 表格样式 10 months ago
Lei OT 9ebab45035 perf: 产品编辑: 基本信息, 语种信息的编辑状态 10 months ago
YCC 0dd8cc7a69 调整表格字段位置 10 months ago
Jimmy Liow 7593637786 fix: 解决产品管理没有权限也有导航 10 months ago
YCC 945a1a7a17 解决invoice的图片显示问题 10 months ago
Jimmy Liow 7d4b7bbba1 perf: 使用Message全局提示上传 10 months ago
Jimmy Liow c5343dd4b2 2.0.0-rc.5 10 months ago
Jimmy Liow 4671179614 feat: 增加上传PageSpy离线日志 10 months ago
YCC ff8c072e07 token名称的统一 10 months ago
Jimmy Liow d33cbc625b 2.0.0-rc.4 10 months ago
Jimmy Liow 4c1855d65b feat: 取消令牌时间过期检测 10 months ago
Lei OT 5a4a3f0214 2.0.0-rc.3 10 months ago
Lei OT 93cf0e5c5e Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT 0bb1f24977 fix: 产品管理: 编辑: 保存后绑定项目未显示; 删除绑定项目1; 10 months ago
Jimmy Liow 4951196018 2.0.0-rc.2 10 months ago
Jimmy Liow 3a63b11b3d feat: 预览文档增加随机数 10 months ago
YCC aba7c69a86 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
YCC e3f8262cff 预览文档增加随机数 10 months ago
Lei OT fe58f01f22 perf: 产品管理: 编辑保存后, 不更新树的显示状态 10 months ago
Lei OT 7045af0881 perf: 产品管理: 导出docx: +v0903 10 months ago
Lei OT 1f574acab3 perf: 产品管理: `名称`编辑更新到中文语种`名称` 10 months ago
Lei OT df64cc306a todo: 产品管理: 导出docx: +v0903 10 months ago
Lei OT 866433419c perf: 产品管理: 导出docx 10 months ago

1
.gitignore vendored

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

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

@ -61,11 +61,15 @@ INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有国内功能', '/domestic/all', 'domestic') VALUES ('所有国内功能', '/domestic/all', 'domestic')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有机票功能', '/air-ticket/all', 'air-ticket') VALUES ('所有机票功能', '/air-ticket/all', 'air-ticket')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('所有火车票功能', '/train-ticket/all', 'train-ticket')
-- 价格管理 -- 价格管理
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('管理产品', '/products/*', 'products') VALUES ('管理产品', '/products/*', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('新增产品', '/products/new', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('审核信息', '/products/info/audit', 'products') VALUES ('审核信息', '/products/info/audit', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category]) INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('录入信息', '/products/info/put', 'products') VALUES ('录入信息', '/products/info/put', 'products')

Binary file not shown.

@ -1,7 +1,7 @@
{ {
"name": "global-highlights-hub", "name": "global-highlights-hub",
"private": true, "private": true,
"version": "2.0.0-rc.1", "version": "2.0.20",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -11,8 +11,10 @@
"preview": "vite preview" "preview": "vite preview"
}, },
"dependencies": { "dependencies": {
"@ant-design/icons": "^5.5.1",
"@react-pdf/renderer": "^3.4.0", "@react-pdf/renderer": "^3.4.0",
"antd": "^5.17.2", "antd": "^5.17.2",
"dayjs": "^1.11.13",
"docx": "^8.5.0", "docx": "^8.5.0",
"file-saver": "^2.0.5", "file-saver": "^2.0.5",
"i18next": "^23.11.5", "i18next": "^23.11.5",
@ -23,6 +25,7 @@
"react-i18next": "^14.1.2", "react-i18next": "^14.1.2",
"react-router-dom": "^6.10.0", "react-router-dom": "^6.10.0",
"react-to-pdf": "^1.0.1", "react-to-pdf": "^1.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^4.5.2" "zustand": "^4.5.2"
}, },
"devDependencies": { "devDependencies": {

@ -88,6 +88,7 @@
"Notice": "Notice", "Notice": "Notice",
"Report": "Report", "Report": "Report",
"Airticket": "AirTicket", "Airticket": "AirTicket",
"Trainticket": "TrainTicket",
"Products": "Products" "Products": "Products"
}, },
"Validation": { "Validation": {

@ -88,6 +88,7 @@
"Notice": "通知", "Notice": "通知",
"Report": "质量评分", "Report": "质量评分",
"Airticket": "机票订票", "Airticket": "机票订票",
"Trainticket": "火车订票",
"Products": "产品管理" "Products": "产品管理"
}, },
"Validation": { "Validation": {

@ -1,5 +1,6 @@
{ {
"ProductType": "项目类型", "ProductType": "项目类型",
"ContractRemarks": "合同备注",
"type": { "type": {
"Experience": "综费", "Experience": "综费",
"Car": "车费", "Car": "车费",

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

@ -39,11 +39,12 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
referenceNo: { key: "referenceNo", transform: value => value || "" }, referenceNo: { key: "referenceNo", transform: value => value || "" },
dates: [ dates: [
{ key: "startdate", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") }, { key: "startdate", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "enddate", transform: arrVal => (arrVal ? arrVal[1].format(DATE_FORMAT) : "") }, { key: "enddate", transform: arrVal => (arrVal ? arrVal[1].endOf('month').format(SMALL_DATETIME_FORMAT) : "") },
{ key: "starttime", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") }, { key: "starttime", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "endtime", transform: arrVal => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : "") }, { key: "endtime", transform: arrVal => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : "") },
], ],
invoiceStatus: { key: "invoiceStatus", transform: value => value?.value || value?.key || "", default: "" }, invoiceStatus: { key: "invoiceStatus", transform: value => value?.value || value?.key || "", default: "" },
invoiceCheckStatus: { key: "invoiceCheckStatus", transform: value => value?.value || value?.key || "", default: "" },
audit_state: { key: "audit_state", transform: value => value?.value || value?.key || "", default: "" }, audit_state: { key: "audit_state", transform: value => value?.value || value?.key || "", default: "" },
agency: { agency: {
key: "agency", key: "agency",
@ -71,6 +72,8 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : ""; return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : "";
}, },
}, },
plan_state: { key: "plan_state", transform: value => value?.value || value?.key || "", default: "" },
airticket_state: { key: "airticket_state", transform: value => value?.value || value?.key || "", default: "" },
unconfirmed: { key: "unconfirmed", transform: value => (value ? 1 : 0) }, unconfirmed: { key: "unconfirmed", transform: value => (value ? 1 : 0) },
}; };
let dest = {}; let dest = {};
@ -179,7 +182,7 @@ function getFields(props) {
let baseChildren = []; let baseChildren = [];
baseChildren = [ baseChildren = [
item( item(
"keyword", "keyword", //
99, 99,
<Form.Item name="keyword" {...fieldProps.keyword}> <Form.Item name="keyword" {...fieldProps.keyword}>
<Input allowClear {...fieldComProps.keyword} /> <Input allowClear {...fieldComProps.keyword} />
@ -187,7 +190,7 @@ function getFields(props) {
fieldProps?.keyword?.col || 6 fieldProps?.keyword?.col || 6
), ),
item( item(
"referenceNo", "referenceNo", //
99, 99,
<Form.Item name="referenceNo" label={t("group:RefNo")} {...fieldProps.referenceNo}> <Form.Item name="referenceNo" label={t("group:RefNo")} {...fieldProps.referenceNo}>
<Input placeholder={t("group:RefNo")} allowClear /> <Input placeholder={t("group:RefNo")} allowClear />
@ -195,7 +198,7 @@ function getFields(props) {
fieldProps?.referenceNo?.col || 6 fieldProps?.referenceNo?.col || 6
), ),
item( item(
"PNR", "PNR", //PNR
99, 99,
<Form.Item name="PNR" label="PNR"> <Form.Item name="PNR" label="PNR">
<Input placeholder={t("group:PNR")} allowClear /> <Input placeholder={t("group:PNR")} allowClear />
@ -203,7 +206,7 @@ function getFields(props) {
fieldProps?.PNR?.col || 4 fieldProps?.PNR?.col || 4
), ),
item( item(
"invoiceStatus", "invoiceStatus", //
99, 99,
<Form.Item name={`invoiceStatus`} initialValue={at(props, "initialValue.invoiceStatus")[0] || { value: "0", label: t("invoiceStatus.Status") }}> <Form.Item name={`invoiceStatus`} initialValue={at(props, "initialValue.invoiceStatus")[0] || { value: "0", label: t("invoiceStatus.Status") }}>
<Select <Select
@ -221,7 +224,23 @@ function getFields(props) {
fieldProps?.invoiceStatus?.col || 3 fieldProps?.invoiceStatus?.col || 3
), ),
item( item(
"dates", "invoiceCheckStatus", //
99,
<Form.Item name={`invoiceCheckStatus`} initialValue={at(props, "initialValue.invoiceCheckStatus")[0] || { value: "0", label: "全部"}}>
<Select
labelInValue
options={[
{ value: "0", label: "全部" },
{ value: "1", label: "待提交" },
{ value: "2", label: "已提交待审核" },
{ value: "3", label: "已完成" },
]}
/>
</Form.Item>,
fieldProps?.invoiceCheckStatus?.col || 3
),
item(
"dates", //
99, 99,
<Form.Item name={"dates"} label={t("group:ArrivalDate")} {...fieldProps.dates} initialValue={at(props, "initialValue.dates")[0]}> <Form.Item name={"dates"} label={t("group:ArrivalDate")} {...fieldProps.dates} initialValue={at(props, "initialValue.dates")[0]}>
{/* <DatePickerCharts isform={true} {...fieldProps.dates} form={form} /> */} {/* <DatePickerCharts isform={true} {...fieldProps.dates} form={form} /> */}
@ -230,7 +249,7 @@ function getFields(props) {
fieldProps?.dates?.col || midCol fieldProps?.dates?.col || midCol
), ),
item( item(
"username", "username", //
99, 99,
<Form.Item name="username" label={t("account:username")} {...fieldProps.username}> <Form.Item name="username" label={t("account:username")} {...fieldProps.username}>
<Input placeholder={t("account:username")} allowClear /> <Input placeholder={t("account:username")} allowClear />
@ -241,7 +260,7 @@ function getFields(props) {
* *
*/ */
item( item(
"year", "year", //
99, 99,
<Form.Item name={"year"} label={t("products:UseYear")} {...fieldProps.year} initialValue={at(props, "initialValue.year")[0]}> <Form.Item name={"year"} label={t("products:UseYear")} {...fieldProps.year} initialValue={at(props, "initialValue.year")[0]}>
<DatePicker picker="year" allowClear {...fieldComProps.year} /> <DatePicker picker="year" allowClear {...fieldComProps.year} />
@ -249,7 +268,7 @@ function getFields(props) {
fieldProps?.year?.col || 3 fieldProps?.year?.col || 3
), ),
item( item(
"agency", "agency", //
99, 99,
<Form.Item name="agency" label={t("products:Vendor")} {...fieldProps.agency} initialValue={at(props, "initialValue.agency")[0]}> <Form.Item name="agency" label={t("products:Vendor")} {...fieldProps.agency} initialValue={at(props, "initialValue.agency")[0]}>
<VendorSelector {...fieldComProps.agency} /> <VendorSelector {...fieldComProps.agency} />
@ -257,7 +276,7 @@ function getFields(props) {
fieldProps?.agency?.col || 6 fieldProps?.agency?.col || 6
), ),
item( item(
"audit_state", "audit_state", // : /
99, 99,
<Form.Item name={`audit_state`} initialValue={at(props, "initialValue.audit_state")[0] || { value: "", label: "Status" }}> <Form.Item name={`audit_state`} initialValue={at(props, "initialValue.audit_state")[0] || { value: "", label: "Status" }}>
<AuditStateSelector {...fieldComProps.audit_state} /> <AuditStateSelector {...fieldComProps.audit_state} />
@ -265,7 +284,40 @@ function getFields(props) {
fieldProps?.audit_state?.col || 3 fieldProps?.audit_state?.col || 3
), ),
item( item(
"products_types", "airticket_state", //-1 0 1
99,
<Form.Item name="airticket_state" label="出票状态" initialValue={at(props, "initialValue.airticket_state")[0] || { value: "-1", label: "所有" }}>
<Select
labelInValue
options={[
{ value: "-1", label: "所有" },
{ value: "0", label: "未出票" },
{ value: "1", label: "已出票" },
]}
/>
</Form.Item>,
fieldProps?.airticket_state?.col || 4
),
item(
"plan_state", //-1 0123
99,
<Form.Item name="plan_state" label="计划状态" initialValue={at(props, "initialValue.plan_state")[0] || { value: "-1", label: "所有" }}>
<Select
labelInValue
options={[
{ value: "-1", label: "所有" },
{ value: "0", label: "新计划" },
{ value: "1", label: "已确认" },
{ value: "2", label: "有变更" },
{ value: "3", label: "已取消" },
]}
/>
</Form.Item>,
fieldProps?.plan_state?.col || 4
),
item(
"products_types", //
99, 99,
<Form.Item name={`products_types`} label={t("products:ProductType")} {...fieldProps.products_types} initialValue={at(props, "initialValue.products_types")[0] || undefined}> <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} /> <ProductsTypesSelector maxTagCount={1} {...fieldComProps.products_types} />
@ -273,7 +325,7 @@ function getFields(props) {
fieldProps?.products_types?.col || 6 fieldProps?.products_types?.col || 6
), ),
item( item(
"dept", "dept", //
99, 99,
<Form.Item name={`dept`} label={t("products:Dept")} {...fieldProps.dept} initialValue={at(props, "initialValue.dept")[0] || undefined}> <Form.Item name={`dept`} label={t("products:Dept")} {...fieldProps.dept} initialValue={at(props, "initialValue.dept")[0] || undefined}>
<DeptSelector {...fieldComProps.dept} /> <DeptSelector {...fieldComProps.dept} />
@ -281,7 +333,7 @@ function getFields(props) {
fieldProps?.dept?.col || 6 fieldProps?.dept?.col || 6
), ),
item( item(
"city", "city", //
99, 99,
<Form.Item name={`city`} label={t("products:City")} {...fieldProps.city} initialValue={at(props, "initialValue.city")[0] || undefined}> <Form.Item name={`city`} label={t("products:City")} {...fieldProps.city} initialValue={at(props, "initialValue.city")[0] || undefined}>
<CitySelector {...fieldComProps.city} /> <CitySelector {...fieldComProps.city} />
@ -289,7 +341,7 @@ function getFields(props) {
fieldProps?.city?.col || 4 fieldProps?.city?.col || 4
), ),
item( item(
"unconfirmed", "unconfirmed", //
99, 99,
<Form.Item name={`unconfirmed`} valuePropName="checked" initialValue={at(props, "initialValue.unconfirmed") || false}> <Form.Item name={`unconfirmed`} valuePropName="checked" initialValue={at(props, "initialValue.unconfirmed") || false}>
<Checkbox>{t("group:unconfirmed")}</Checkbox> <Checkbox>{t("group:unconfirmed")}</Checkbox>

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

@ -3,6 +3,7 @@ export const PROJECT_NAME = "GHHub";
// mode: test内部测试使用 // mode: test内部测试使用
export const HT_HOST = import.meta.env.MODE === 'test' ? 'http://120.79.9.217:10024' : import.meta.env.PROD ? 'https://p9axztuwd7x8a7.mycht.cn' : 'http://202.103.68.144:890' export const HT_HOST = import.meta.env.MODE === 'test' ? 'http://120.79.9.217:10024' : import.meta.env.PROD ? 'https://p9axztuwd7x8a7.mycht.cn' : 'http://202.103.68.144:890'
export const OVERSEA_HOST = 'https://ht20-p9axztuwd7x8a7.mycht.cn'
export const DATE_FORMAT = "YYYY-MM-DD"; export const DATE_FORMAT = "YYYY-MM-DD";
export const DATE_FORMAT_MONTH = "YYYY-MM"; export const DATE_FORMAT_MONTH = "YYYY-MM";
@ -33,8 +34,13 @@ export const PERM_DOMESTIC = '/domestic/all'
// category: air-ticket // category: air-ticket
export const PERM_AIR_TICKET = '/air-ticket/all' export const PERM_AIR_TICKET = '/air-ticket/all'
// 火车票供应商
// category: train-ticket
export const PERM_TRAIN_TICKET = '/train-ticket/all'
// 价格管理 // 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理 export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_NEW = '/products/new'; // 新增产品
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核
export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入 export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入
export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核 export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核

@ -105,14 +105,14 @@ export const useProductsTypesFieldsets = (type) => {
const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : []; const infoDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['display_to_c'] : [];
const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : []; const infoRecDisplay = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['recommends_rate'] : [];
const infoTypesMap = { const infoTypesMap = {
'6': [[], []], '6': [[...infoDisplay], []],
'B': [['km'], []], 'B': [['km', ...infoDisplay], []],
'J': [[...infoRecDisplay, 'duration', ], ['description']], 'J': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
'Q': [[...infoRecDisplay, 'duration', ], ['description']], 'Q': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
'D': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']], 'D': [[...infoRecDisplay, 'duration', ...infoDisplay], ['description']],
'7': [[...infoRecDisplay, 'duration', 'open_weekdays'], ['description']], '7': [[...infoRecDisplay, 'duration', 'open_weekdays', ...infoDisplay], ['description']],
'R': [[], ['description']], 'R': [[...infoDisplay], ['description']],
'8': [[], []], '8': [[...infoDisplay], []],
}; };
const thisTypeFieldset = (_type) => { const thisTypeFieldset = (_type) => {
if (isEmpty(_type)) { if (isEmpty(_type)) {
@ -143,7 +143,7 @@ export const useNewProductRecord = () => {
'recommends_rate': 0, 'recommends_rate': 0,
'dept_id': 0, 'dept_id': 0,
'dept_name': '', 'dept_name': '',
'display_to_c': 0, 'display_to_c': '0',
'km': 0, 'km': 0,
'city_id': 0, 'city_id': 0,
'city_name': '', 'city_name': '',
@ -151,6 +151,7 @@ export const useNewProductRecord = () => {
'lastedit_changed': '', 'lastedit_changed': '',
'create_date': '', 'create_date': '',
'created_by': '', 'created_by': '',
'edit_status': 2,
}, },
lgc_details: [ lgc_details: [
{ {
@ -158,6 +159,7 @@ export const useNewProductRecord = () => {
'descriptions': '', 'descriptions': '',
'lgc': 1, 'lgc': 1,
'id': '', 'id': '',
'edit_status': 2,
}, },
], ],
quotation: [ quotation: [

@ -1,5 +1,4 @@
import React from 'react' import { createRoot } from 'react-dom/client'
import ReactDOM from 'react-dom/client'
import { import {
createBrowserRouter, createBrowserRouter,
RouterProvider, RouterProvider,
@ -25,11 +24,19 @@ import NoticeIndex from '@/views/notice/Index'
import NoticeDetail from '@/views/notice/Detail' import NoticeDetail from '@/views/notice/Detail'
import InvoiceIndex from '@/views/invoice/Index' import InvoiceIndex from '@/views/invoice/Index'
import InvoiceDetail from '@/views/invoice/Detail' import InvoiceDetail from '@/views/invoice/Detail'
import InvoiceHistory from '@/views/invoice/History'
import InvoicePaid from '@/views/invoice/Paid' import InvoicePaid from '@/views/invoice/Paid'
import InvoicePaidDetail from '@/views/invoice/PaidDetail' import InvoicePaidDetail from '@/views/invoice/PaidDetail'
import Airticket from '@/views/airticket/Index' import Airticket from '@/views/airticket/Index'
import AirticketPlan from '@/views/airticket/Plan' import AirticketPlan from '@/views/airticket/Plan'
import AirticketInvoice from '@/views/airticket/Invoice' import AirticketInvoice from '@/views/airticket/Invoice'
import AirticketInvoicePaid from '@/views/airticket/InvoicePaid'
import Trainticket from '@/views/trainticket/index'
import TrainticketPlan from '@/views/trainticket/plan'
import TrainticketInvoice from '@/views/trainticket/invoice'
import TrainticketInvoicePaid from '@/views/trainticket/invoicePaid'
import { ThemeContext } from '@/stores/ThemeContext' import { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage' import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from './stores/Auth' import useAuthStore from './stores/Auth'
@ -38,12 +45,10 @@ import { isNotEmpty } from '@/utils/commons'
import ProductsManage from '@/views/products/Manage'; import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail'; import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit'; 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 { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
import './i18n' import './i18n'
const { createRoot } = ReactDOM
const initRouter = async () => { const initRouter = async () => {
return createBrowserRouter([ return createBrowserRouter([
{ {
@ -66,11 +71,19 @@ const initRouter = async () => {
{ path: 'notice/:CCP_BLID', element: <NoticeDetail />}, { path: 'notice/:CCP_BLID', element: <NoticeDetail />},
{ path: 'invoice',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></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/detail/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
{ path: 'invoice/history/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceHistory /></RequireAuth>},
{ path: 'invoice/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>}, { path: 'invoice/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></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',element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
{ path: 'airticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>}, { path: 'airticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
{ path: 'airticket/invoice',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoice /></RequireAuth>}, { path: 'airticket/invoice',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoice /></RequireAuth>},
{ path: 'airticket/invoicepaid',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoicePaid /></RequireAuth>},
{ path: 'trainticket',element: <RequireAuth subject={PERM_TRAIN_TICKET} result={true}><Trainticket /></RequireAuth>},
{ path: 'trainticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketPlan /></RequireAuth>},
{ path: 'trainticket/invoice',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoice /></RequireAuth>},
{ path: 'trainticket/invoicepaid',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoicePaid /></RequireAuth>},
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>}, { path: "products",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/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
@ -100,8 +113,11 @@ const initAppliction = async () => {
} }
const router = await initRouter() const router = await initRouter()
const root = document.getElementById('root')
if (!root) throw new Error('No root element found')
createRoot(document.getElementById('root')).render( createRoot(root).render(
//<React.StrictMode> //<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}> <ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider <RouterProvider

@ -1,26 +1,36 @@
import { loadScript } from '@/utils/commons'; import { loadScript } from '@/utils/commons';
import { PROJECT_NAME } from '@/config'; import { PROJECT_NAME, BUILD_VERSION } from '@/config';
export const loadPageSpy = (title) => { export const loadPageSpy = (title) => {
if (import.meta.env.DEV || window.$pageSpy) return if (import.meta.env.DEV || window.$pageSpy) return
const PageSpyConfig = { api: 'page-spy.mycht.cn', project: PROJECT_NAME, title: title, autoRender: false };
const PageSpySrc = [ const PageSpySrc = [
'https://page-spy.mycht.cn/page-spy/index.min.js', 'https://page-spy.mycht.cn/page-spy/index.min.js'+`?${BUILD_VERSION}`,
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js', 'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js'+`?${BUILD_VERSION}`,
'https://page-spy.mycht.cn/plugin/rrweb/index.min.js', 'https://page-spy.mycht.cn/plugin/rrweb/index.min.js'+`?${BUILD_VERSION}`,
]; ];
Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => { Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
// //
PageSpy.registerPlugin(new DataHarborPlugin({ maximum: 2 * 1024 * 1024 })); window.$harbor = new DataHarborPlugin();
// PageSpy window.$rrweb = new RRWebPlugin();
window.$pageSpy = new PageSpy({ api: 'page-spy.mycht.cn', project: PROJECT_NAME, title: title, autoRender: false }); [window.$harbor, window.$rrweb].forEach(p => {
PageSpy.registerPlugin(p)
})
window.$pageSpy = new PageSpy(PageSpyConfig);
}); });
}; };
export const uploadPageSpyLog = () => { export const uploadPageSpyLog = async () => {
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload'); // window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
await window.$harbor.upload() // { clearCache: true, remark: '' }
alert('Success')
} else {
alert('Failure')
}
} }
export const PageSpyLog = () => { export const PageSpyLog = () => {

@ -12,8 +12,9 @@ const airTicketStore = create((set, get) => ({
setGuestList: guestList => set({ guestList }), setGuestList: guestList => set({ guestList }),
setVEIFlightBill: vEIFlightBill => set({ vEIFlightBill }), setVEIFlightBill: vEIFlightBill => set({ vEIFlightBill }),
setVeiPlanChangeTxt: veiPlanChangeTxt => set({ veiPlanChangeTxt }), setVeiPlanChangeTxt: veiPlanChangeTxt => set({ veiPlanChangeTxt }),
setAirPortList: airPortList => set({ airPortList }),
async getPlanList(vei_sn, GRI_Name, TimeStart, TimeEnd) { async getPlanList(vei_sn, GRI_Name, TimeStart, TimeEnd, plan_state, airticket_state) {
const { setLoading, setPlanList } = get(); const { setLoading, setPlanList } = get();
setLoading(true); setLoading(true);
const searchParams = { const searchParams = {
@ -21,6 +22,8 @@ const airTicketStore = create((set, get) => ({
FlightDate1: TimeStart, FlightDate1: TimeStart,
FlightDate2: TimeEnd, FlightDate2: TimeEnd,
GRI_Name: GRI_Name, GRI_Name: GRI_Name,
FlightStatus: plan_state,
TicketIssued: airticket_state,
}; };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightPlan`, searchParams); const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightPlan`, searchParams);
@ -38,6 +41,8 @@ const airTicketStore = create((set, get) => ({
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightPlanDetail`, searchParams); const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetFlightPlanDetail`, searchParams);
const _result = errcode !== 0 ? [] : result; const _result = errcode !== 0 ? [] : result;
setPlanDetail(_result); setPlanDetail(_result);
//return _result.filter(item => isNotEmpty(item.GRI_No));
return "dsadsd";
}, },
async postFlightDetail(CLF_SN, GRI_SN, VEI_SN, original_values, info_object) { async postFlightDetail(CLF_SN, GRI_SN, VEI_SN, original_values, info_object) {
@ -51,6 +56,7 @@ const airTicketStore = create((set, get) => ({
for (const [key, value] of Object.entries(info_object)) { for (const [key, value] of Object.entries(info_object)) {
formData.set(key, value); //再用新值覆盖 formData.set(key, value); //再用新值覆盖
} }
formData.set("StartDate", dayjs(info_object.StartDate).format(DATE_FORMAT)); //再用新值覆盖
//是否出票的值true、false变为1或0 //是否出票的值true、false变为1或0
formData.set("TicketIssued", info_object.TicketIssued ? 1 : 0); formData.set("TicketIssued", info_object.TicketIssued ? 1 : 0);
const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_info"; const postUrl = HT_HOST + "/Service_BaseInfoWeb/edit_or_new_flight_info";
@ -62,6 +68,15 @@ const airTicketStore = create((set, get) => ({
} }
}); });
}, },
//删除航班信息
async delete_flight_info(CLF_SN) {
const searchParams = {
CLF_SN: CLF_SN,
};
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/Delete_flight_info`, searchParams);
const _result = errcode !== 0 ? [] : result;
return _result;
},
async getGuestList(coli_sn) { async getGuestList(coli_sn) {
const { setGuestList } = get(); const { setGuestList } = get();
@ -154,15 +169,58 @@ const airTicketStore = create((set, get) => ({
}); });
}, },
//通知顾问查看机票信息 //通知顾问查看机票信息
async ticketIssuedNotifications(CLF_SN, OPI_SN) { async ticketIssuedNotifications(LMI_SN, CLF_SN, OPI_SN, FlightMemo_messages) {
const searchParams = { const searchParams = {
CLF_SN: CLF_SN, CLF_SN: CLF_SN,
OPI_SN: OPI_SN, OPI_SN: OPI_SN,
LMI_SN: LMI_SN,
FlightMemo_messages: FlightMemo_messages,
}; };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/TicketIssuedNotifications`, searchParams); const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/TicketIssuedNotifications`, searchParams);
const _result = errcode !== 0 ? [] : result; const _result = errcode !== 0 ? [] : result;
return _result; return _result;
}, },
//获取机场列表
async getAirPortList() {
const { setAirPortList } = get();
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/GetAirPortInfo`);
const _result = errcode !== 0 ? [] : result;
setAirPortList(_result);
},
airLineList: [
{ label: "CA-国航", value: "国航" },
{ label: "MU-东方航空", value: "东方航空" },
{ label: "FM-上海航空", value: "上海航空" },
{ label: "CZ-南方航空", value: "南方航空" },
{ label: "HO-吉祥航空", value: "吉祥航空" },
{ label: "HU-海南航空", value: "海南航空" },
{ label: "ZH-深圳航空", value: "深圳航空" },
{ label: "MF-厦门航空", value: "厦门航空" },
{ label: "3U-四川航空", value: "四川航空" },
{ label: "SC-山东航空", value: "山东航空" },
{ label: "JD-首都航空", value: "首都航空" },
{ label: "BK-奥凯航空", value: "奥凯航空" },
{ label: "GS-天津航空", value: "天津航空" },
{ label: "CN-大新华", value: "大新华" },
{ label: "KN-中联航", value: "中联航" },
{ label: "TV-西藏航空", value: "西藏航空" },
{ label: "8L-祥鹏航空", value: "祥鹏航空" },
{ label: "KY-昆明航空", value: "昆明航空" },
{ label: "EU-成都航空", value: "成都航空" },
{ label: "G5-华夏航空", value: "华夏航空" },
{ label: "NS-河北航空", value: "河北航空" },
{ label: "QW-青岛航空", value: "青岛航空" },
{ label: "Y8-扬子江", value: "扬子江" },
{ label: "PN-西部航空", value: "西部航空" },
{ label: "DZ-东海航空", value: "东海航空" },
{ label: "GT-桂林航空", value: "桂林航空" },
{ label: "9H-长安航空", value: "长安航空" },
{ label: "GY-多彩航空", value: "多彩航空" },
{ label: "DR-瑞丽航空", value: "瑞丽航空" },
{ label: "GJ-长龙航空", value: "长龙航空" },
{ label: "GX-广西北部", value: "广西北部" },
],
})); }));
export default airTicketStore; export default airTicketStore;

@ -37,6 +37,7 @@ export const fetchPermissionListByUserId = async (userId) => {
return errcode !== 0 ? {} : result return errcode !== 0 ? {} : result
} }
// 取消令牌时间过期检测,待删除
async function fetchLastRequet() { async function fetchLastRequet() {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`) const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`)
return errcode !== 0 ? {} : result return errcode !== 0 ? {} : result
@ -44,7 +45,6 @@ async function fetchLastRequet() {
const initialState = { const initialState = {
tokenInterval: null, tokenInterval: null,
tokenTimeout: import.meta.env.PROD ? true : false,
loginStatus: 0, loginStatus: 0,
defaltRoute: '', defaltRoute: '',
currentUser: { currentUser: {
@ -62,7 +62,7 @@ const useAuthStore = create(devtools((set, get) => ({
...initialState, ...initialState,
initAuth: async () => { initAuth: async () => {
const { startTokenInterval, loadUserPermission } = get() const { loadUserPermission } = get()
const { setStorage, loginToken } = usingStorage() const { setStorage, loginToken } = usingStorage()
// Dev 模式使用 localStorage会有 token 失效情况,需要手动删除 // Dev 模式使用 localStorage会有 token 失效情况,需要手动删除
@ -87,8 +87,7 @@ const useAuthStore = create(devtools((set, get) => ({
} }
})) }))
startTokenInterval() loadPageSpy(`${userJson.real_name}-${userJson.VName}`)
loadPageSpy(userJson.real_name)
}, },
authenticate: async (usr, pwd) => { authenticate: async (usr, pwd) => {
@ -102,7 +101,6 @@ const useAuthStore = create(devtools((set, get) => ({
await initAuth() await initAuth()
set(() => ({ set(() => ({
tokenTimeout: false,
loginStatus: 302 loginStatus: 302
})) }))
}, },
@ -139,27 +137,6 @@ const useAuthStore = create(devtools((set, get) => ({
})) }))
}, },
startTokenInterval: () => {
const { logout } = get()
async function checkTokenTimeout() {
// TODOToken 失效后要跳转到登录页面
const { LastReqDate } = await fetchLastRequet()
const lastReqDateTime = new Date(LastReqDate).getTime()
const now = new Date()
const diffTime = now.getTime() - lastReqDateTime
const diffHours = diffTime/1000/60/60
if (diffHours > 1) {
logout()
}
}
const interval = setInterval(() => checkTokenTimeout(), 1000*60*10)
set(() => ({
tokenInterval: interval
}))
},
// TODO: 迁移到 Account.js // TODO: 迁移到 Account.js
changeUserPassword: (password, newPassword) => { changeUserPassword: (password, newPassword) => {
const { userId } = usingStorage() const { userId } = usingStorage()

@ -66,7 +66,7 @@ export const delProductExtrasAction = async (body) => {
* @param {object} param { id, travel_agency_id, use_year } * @param {object} param { id, travel_agency_id, use_year }
*/ */
export const getAgencyProductExtrasAction = async (param) => { export const getAgencyProductExtrasAction = async (param) => {
const _param = { ...param, use_year: (param.use_year || '').replace('all', '') }; const _param = { ...param, use_year: String(param.use_year || '').replace('all', '') };
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param); const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_extras`, _param);
return errcode !== 0 ? [] : result; return errcode !== 0 ? [] : result;
}; };
@ -137,6 +137,32 @@ export const deleteQuotationAction = async (id) => {
return { errcode, result, success: errcode === 0 }; return { errcode, result, success: errcode === 0 };
} }
/**
* 获取合同备注
*/
export const fetchRemarkList = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_get`, params)
return { errcode, result, success: errcode === 0 }
}
/**
* 获取合同备注
*/
export const postRemarkList = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_add`, params)
return { errcode, result, success: errcode === 0 }
}
const defaultRemarkList = [
{id: 0, "product_type_id": "6","Memo": ""},
{id: 0, "product_type_id": "B","Memo": ""},
{id: 0, "product_type_id": "J","Memo": ""},
{id: 0, "product_type_id": "Q","Memo": ""},
{id: 0, "product_type_id": "7","Memo": ""},
{id: 0, "product_type_id": "R","Memo": ""},
{id: 0, "product_type_id": "D","Memo": ""}
]
const initialState = { const initialState = {
loading: false, loading: false,
searchValues: {}, // 客服首页: 搜索条件 searchValues: {}, // 客服首页: 搜索条件
@ -148,7 +174,8 @@ const initialState = {
quotationList: [], // 编辑页: 当前产品报价列表 quotationList: [], // 编辑页: 当前产品报价列表
editing: false, editing: false,
switchParams: {}, // 头部切换参数 switchParams: {}, // 头部切换参数
}; }
export const useProductsStore = create( export const useProductsStore = create(
devtools((set, get) => ({ devtools((set, get) => ({
// 初始化状态 // 初始化状态
@ -193,6 +220,45 @@ export const useProductsStore = create(
reset: () => set(initialState), reset: () => set(initialState),
getRemarkList: async() => {
const {switchParams} = get()
const { result, success } = await fetchRemarkList({
travel_agency_id: switchParams.travel_agency_id, use_year: switchParams.use_year
})
if (success) {
const mapRemarkList = defaultRemarkList.map(remark => {
const filterResult = result.filter(r => r.product_type_id === remark.product_type_id)
if (filterResult.length > 0) return filterResult[0]
else return remark
})
return Promise.resolve(mapRemarkList)
} else {
return Promise.resolve('获取合同备注失败')
}
},
saveOrUpdateRemark: async(remarkList) => {
const {switchParams} = get()
const mapRemarkList = remarkList.map(remark => {
return {
id: remark.id,
travel_agency_id: switchParams.travel_agency_id,
use_year: switchParams.use_year,
product_type_id: remark.product_type_id,
Memo: remark.Memo,
}
})
const { result, success } = await postRemarkList(mapRemarkList)
if (success) {
return Promise.resolve(result)
} else {
return Promise.resolve('保存合同备注失败')
}
},
newEmptyQuotation: () => ({ newEmptyQuotation: () => ({
id: null, id: null,
adult_cost: 0, adult_cost: 0,

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

@ -1,27 +1,28 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'; import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react'
import { Layout, Menu, ConfigProvider, theme, Dropdown, Space, Row, Col, Badge, Typography, Modal, Input, Button, App as AntApp } from 'antd'; import { Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp } from 'antd'
import { DownOutlined } from '@ant-design/icons'; import { DownOutlined } from '@ant-design/icons'
import 'antd/dist/reset.css'; import 'antd/dist/reset.css'
import AppLogo from '@/assets/logo-gh.png'; import AppLogo from '@/assets/logo-gh.png'
import { isEmpty } from '@/utils/commons'; import { isEmpty } from '@/utils/commons'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import zhLocale from 'antd/locale/zh_CN'; import zhLocale from 'antd/locale/zh_CN'
import enLocale from 'antd/locale/en_US'; import enLocale from 'antd/locale/en_US'
import 'dayjs/locale/zh-cn'; import 'dayjs/locale/zh-cn'
import ErrorBoundary from '@/components/ErrorBoundary'; import { BugOutlined } from "@ant-design/icons"
import { BUILD_VERSION, } from '@/config'; import ErrorBoundary from '@/components/ErrorBoundary'
import useNoticeStore from '@/stores/Notice'; import { BUILD_VERSION, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT } from '@/config'
import useAuthStore from '@/stores/Auth'; import useNoticeStore from '@/stores/Notice'
import { useThemeContext } from '@/stores/ThemeContext'; import useAuthStore from '@/stores/Auth'
import { usingStorage } from '@/hooks/usingStorage'; import { useThemeContext } from '@/stores/ThemeContext'
import { useDefaultLgc } from '@/i18n/LanguageSwitcher'; import { usingStorage } from '@/hooks/usingStorage'
import { useDefaultLgc } from '@/i18n/LanguageSwitcher'
import { appendRequestParams } from '@/utils/request' import { appendRequestParams } from '@/utils/request'
import { uploadPageSpyLog } from '@/pageSpy';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT } from '@/config'; import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET } from '@/config'
const { Header, Content, Footer } = Layout; const { Header, Content, Footer } = Layout
const { Title } = Typography;
function App() { function App() {
@ -29,13 +30,13 @@ function App() {
const { colorPrimary } = useThemeContext() const { colorPrimary } = useThemeContext()
const [password, setPassword] = useState('') const [isPermitted, currentUser] = useAuthStore(
(state) => [state.isPermitted, state.currentUser])
const [authenticate, tokenTimeout, isPermitted, currentUser] = useAuthStore(
(state) => [state.authenticate, state.tokenTimeout, state.isPermitted, state.currentUser])
const { loginToken } = usingStorage() const { loginToken } = usingStorage()
const [messageApi, contextHolder] = message.useMessage()
const noticeUnRead = useNoticeStore((state) => state.noticeUnRead) const noticeUnRead = useNoticeStore((state) => state.noticeUnRead)
const href = useHref() const href = useHref()
const navigate = useNavigate() const navigate = useNavigate()
@ -48,15 +49,6 @@ function App() {
} }
}, [href]) }, [href])
const onSubmit = () => {
authenticate(currentUser?.username, password)
.catch(ex => {
console.error(ex)
alert(t('Validation.LoginFailed'))
})
setPassword('')
}
const splitPath = href.split('/') const splitPath = href.split('/')
let defaultPath = 'notice' let defaultPath = 'notice'
@ -64,13 +56,26 @@ function App() {
defaultPath = splitPath[1] defaultPath = splitPath[1]
} }
const { language } = useDefaultLgc(); const { language } = useDefaultLgc()
const [antdLng, setAntdLng] = useState(enLocale); const [antdLng, setAntdLng] = useState(enLocale)
useEffect(() => { useEffect(() => {
setAntdLng(i18n.language === 'en' ? enLocale : zhLocale); setAntdLng(i18n.language === 'en' ? enLocale : zhLocale)
appendRequestParams('lgc', language); appendRequestParams('lgc', language)
}, [i18n.language]) }, [i18n.language])
const uploadLog = () => {
if (window.$pageSpy) {
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload')
messageApi.info('Success')
} else {
messageApi.error('Failure')
}
}
//
const isProductPermitted = isPermitted(PERM_PRODUCTS_MANAGEMENT) || isPermitted(PERM_PRODUCTS_INFO_PUT)
const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/edit'
return ( return (
<ConfigProvider locale={antdLng} <ConfigProvider locale={antdLng}
theme={{ theme={{
@ -82,27 +87,18 @@ function App() {
algorithm: theme.defaultAlgorithm, algorithm: theme.defaultAlgorithm,
}}> }}>
<AntApp> <AntApp>
<ErrorBoundary> <FloatButton.Group
<Modal shape='square'
centered style={{
closable={false} insetInlineEnd: 94,
maskClosable={false} }}
footer={null}
open={tokenTimeout}
> >
<Title level={3}>{t('LoginTimeout')}</Title> <FloatButton icon={<BugOutlined />} onClick={() => uploadPageSpyLog()} />
<div>{t('LoginTimeoutTip')}</div> <FloatButton.BackTop />
<Space direction='horizontal'> </FloatButton.Group>
<Input.Password value={password} {contextHolder}
onChange={(e) => setPassword(e.target.value)} <ErrorBoundary>
onPressEnter={onSubmit} <Layout className='min-h-screen h-dvh'>
addonBefore={currentUser?.username} />
<Button
onClick={onSubmit}
>{t('Submit')}</Button></Space>
</Modal>
<Layout className='min-h-screen'>
<Header className='sticky top-0 z-10 w-full'> <Header className='sticky top-0 z-10 w-full'>
<Row gutter={{ md: 24 }} justify='end' align='middle'> <Row gutter={{ md: 24 }} justify='end' align='middle'>
<Col span={15}> <Col span={15}>
@ -119,7 +115,8 @@ function App() {
isPermitted(PERM_OVERSEA) ? { key: 'feedback', label: <Link to='/feedback'>{t('menu.Feedback')}</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_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_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> } : { key: 'products', label: <Link to='/products/edit'>{t('menu.Products')}</Link> }, isPermitted(PERM_TRAIN_TICKET) ? { key: 'trainticket', label: <Link to='/trainticket'>{t('menu.Trainticket')}</Link> } : null,
isProductPermitted ? { key: 'products', label: <Link to={productLink}>{t('menu.Products')}</Link> } : null,
{ {
key: 'notice', key: 'notice',
label: ( label: (
@ -162,7 +159,7 @@ function App() {
</Col> </Col>
</Row> </Row>
</Header> </Header>
<Content className='p-6 m-0 min-h-72 bg-white'> <Content className='p-6 m-0 min-h-72 bg-white overflow-auto'>
{needToLogin ? <>login...</> : <Outlet />} {needToLogin ? <>login...</> : <Outlet />}
</Content> </Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer> <Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>

@ -1,15 +1,14 @@
import { Outlet } from 'react-router-dom' import { Outlet } from "react-router-dom";
import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from 'antd' import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from "antd";
import 'antd/dist/reset.css' import "antd/dist/reset.css";
import AppLogo from '@/assets/logo-gh.png' import AppLogo from "@/assets/logo-gh.png";
import { useThemeContext } from '@/stores/ThemeContext' import { useThemeContext } from "@/stores/ThemeContext";
import { BUILD_VERSION, } from '@/config'; import { BUILD_VERSION } from "@/config";
const { Header, Content, Footer } = Layout const { Header, Content, Footer } = Layout;
function Standlone() { function Standlone() {
const { colorPrimary } = useThemeContext();
const { colorPrimary } = useThemeContext()
return ( return (
<ConfigProvider <ConfigProvider
@ -20,23 +19,19 @@ function Standlone() {
algorithm: theme.defaultAlgorithm, algorithm: theme.defaultAlgorithm,
}}> }}>
<AntApp> <AntApp>
<Layout className='min-h-screen'> <Layout className="min-h-screen">
<Header className='sticky top-0 z-10 w-full'> <Header className="sticky top-0 z-10 w-full">
<Row gutter={{ md: 24 }} justify='center'> <img src={AppLogo} className="float-left h-9 my-4 mr-6 ml-0 bg-white/30" alt="App logo" />
<Col span={2}> <p className="text-white text-center">Global Highlights Hub</p>
<img src={AppLogo} className='float-left h-9 my-4 mr-6 ml-0 bg-white/30' alt='App logo' />
</Col>
<Col span={5}><h1 className='text-white text-center'>Global Highlights Hub</h1></Col>
</Row>
</Header> </Header>
<Content className='p-6 m-0 min-h-72 bg-white'> <Content className="p-6 m-0 min-h-72 bg-white">
<Outlet /> <Outlet />
</Content> </Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer> <Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
</Layout> </Layout>
</AntApp> </AntApp>
</ConfigProvider> </ConfigProvider>
) );
} }
export default Standlone export default Standlone;

@ -128,6 +128,11 @@ function Management() {
const onAccountFinish = (values) => { const onAccountFinish = (values) => {
saveOrUpdateAccount(values) saveOrUpdateAccount(values)
.then(() => { .then(() => {
notification.info({
message: 'Notification',
description: '账号保存成功',
placement: 'top',
})
setAccountModalOpen(false) setAccountModalOpen(false)
handelAccountSearch() handelAccountSearch()
}) })

@ -1,6 +1,6 @@
import { Descriptions, Col, Row } from 'antd'; import { Descriptions, Col, Row } from 'antd'
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next'
import useAuthStore from '@/stores/Auth'; import useAuthStore from '@/stores/Auth'
function Profile() { function Profile() {
@ -12,7 +12,7 @@ function Profile() {
<Col span={12} offset={6}> <Col span={12} offset={6}>
<Descriptions title={t('userProfile')} layout="vertical" column={2}> <Descriptions title={t('userProfile')} layout="vertical" column={2}>
<Descriptions.Item label={t("Username")}>{currentUser?.username}</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("Realname")}>{currentUser?.realname}</Descriptions.Item>
<Descriptions.Item label={t("Email")}>{currentUser?.emailAddress}</Descriptions.Item> <Descriptions.Item label={t("Email")}>{currentUser?.emailAddress}</Descriptions.Item>
<Descriptions.Item label={t("Company")}>{currentUser?.travelAgencyName}</Descriptions.Item> <Descriptions.Item label={t("Company")}>{currentUser?.travelAgencyName}</Descriptions.Item>
</Descriptions> </Descriptions>
@ -21,4 +21,4 @@ function Profile() {
); );
} }
export default Profile; export default Profile

@ -68,6 +68,7 @@ function RoleList() {
['oversea', '海外供应商'], ['oversea', '海外供应商'],
['domestic', '国内供应商'], ['domestic', '国内供应商'],
['air-ticket', '机票供应商'], ['air-ticket', '机票供应商'],
['train-ticket', '火车票供应商'],
['products', '产品价格'], ['products', '产品价格'],
['page', '默认页面'], ['page', '默认页面'],
]); ]);

@ -5,7 +5,8 @@ import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons"; import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm"; import SearchForm from "@/components/SearchForm";
import { DATE_FORMAT } from "@/config";
import { TableExportBtn } from "@/components/Data";
import airTicketStore from "@/stores/Airticket"; import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage"; import { usingStorage } from "@/hooks/usingStorage";
@ -64,13 +65,13 @@ const planListColumns = [
render: text => formatColonTime(text), render: text => formatColonTime(text),
}, },
{ {
title: "是否出票", title: "出票处理",
key: "TicketIssued", key: "TicketIssued",
dataIndex: "TicketIssued", dataIndex: "TicketIssued",
render: (text, record) => record.TicketIssuedName, render: (text, record) => record.TicketIssuedName,
}, },
{ {
title: "状态", title: "计划状态",
key: "FlightStatus", key: "FlightStatus",
dataIndex: "FlightStatus", dataIndex: "FlightStatus",
render: (text, record) => record.FlightStatusName, render: (text, record) => record.FlightStatusName,
@ -89,7 +90,9 @@ const Airticket = props => {
const [getPlanList, planList, loading] = airTicketStore(state => [state.getPlanList, state.planList, state.loading]); const [getPlanList, planList, loading] = airTicketStore(state => [state.getPlanList, state.planList, state.loading]);
const showTotal = total => `合计 ${total} `; const showTotal = total => `合计 ${total} `;
useEffect(() => {}, []); useEffect(() => {
!planList && getPlanList(travelAgencyId, "", dayjs().startOf("M").format(DATE_FORMAT), dayjs().add(3, "M").endOf("M").format(DATE_FORMAT), "-1", "-1");
}, []);
return ( return (
<Space direction="vertical" style={{ width: "100%" }}> <Space direction="vertical" style={{ width: "100%" }}>
@ -97,30 +100,36 @@ const Airticket = props => {
<Col md={20} lg={20} xxl={20}> <Col md={20} lg={20} xxl={20}>
<SearchForm <SearchForm
initialValue={{ initialValue={{
dates: [dayjs().startOf("M"), dayjs().endOf("M")], dates: [dayjs().startOf("M"), dayjs().add(3, "M").endOf("M")],
}} }}
fieldsConfig={{ fieldsConfig={{
shows: ["referenceNo", "dates"], shows: ["referenceNo", "dates", "airticket_state", "plan_state"],
fieldProps: { fieldProps: {
referenceNo: { label: "搜索计划" }, referenceNo: { label: "搜索计划" },
dates: { label: "出发日期", col: 8 }, dates: { label: "出发日期", col: 8 },
}, },
}} }}
onSubmit={(err, formVal, filedsVal) => { onSubmit={(err, formVal, filedsVal) => {
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime); getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
}} }}
/> />
</Col> </Col>
<Col md={4} lg={4} xxl={4}> <Col md={4} lg={4} xxl={4}>
<Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoice`)}> <Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoice`)}>
报账 报账
</Button> </Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/airticket/invoicepaid`)}>
汇款记录
</Button>
</Space>
</Col> </Col>
</Row> </Row>
<Row gutter={16}> <Row gutter={16}>
<Col md={24} lg={24} xxl={24}> <Col md={24} lg={24} xxl={24}>
<Table bordered={true} rowKey="id" columns={planListColumns} dataSource={planList} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} /> <Table bordered={true} rowKey="id" columns={planListColumns} dataSource={planList} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
<TableExportBtn btnTxt="导出计划" label={`机票计划`} {...{ columns: planListColumns, dataSource: planList }} />
</Col> </Col>
<Col md={24} lg={24} xxl={24}></Col> <Col md={24} lg={24} xxl={24}></Col>
</Row> </Row>

@ -1,11 +1,12 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, Checkbox, Table, Button, App } from "antd"; import { Grid, Divider, Layout, Steps, Statistic, Col, Row, Space, Checkbox, Table, Button, App, Typography } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, FrownTwoTone, LikeTwoTone } from "@ant-design/icons"; import { PhoneOutlined, CustomerServiceOutlined, FrownTwoTone, LikeTwoTone } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom"; import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons"; import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs"; import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm"; import SearchForm from "@/components/SearchForm";
import BackBtn from "@/components/BackBtn"; import BackBtn from "@/components/BackBtn";
import { TableExportBtn } from "@/components/Data";
import airTicketStore from "@/stores/Airticket"; import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage"; import { usingStorage } from "@/hooks/usingStorage";
@ -23,22 +24,17 @@ const Invoice = props => {
title: "团名", title: "团名",
key: "GRI_No", key: "GRI_No",
dataIndex: "GRI_No", dataIndex: "GRI_No",
render: (text, record) => `${record.GRI_No} ${record.WL}`, render: (text, record) => (
<Typography.Text title={record.Memo}>
{record.GRI_No} {record.WL}
</Typography.Text>
),
}, },
{ {
title: "费用类型", title: "状态",
key: "CostType", key: "CostType",
dataIndex: "CostType", dataIndex: "CostType",
}, },
{
title: "手续费",
children: [
{
title: vEIFlightBill && vEIFlightBill.reduce((acc, curr) => acc + curr.ServiceFee, 0),
dataIndex: "ServiceFee",
},
],
},
{ {
title: "出发日期", title: "出发日期",
key: "StartDate", key: "StartDate",
@ -50,10 +46,16 @@ const Invoice = props => {
}, },
}, },
{ {
title: "城市", title: "出发",
key: "FromCity", key: "FromCity",
dataIndex: "FromCity", dataIndex: "FromCity",
render: (text, record) => (record.CostType == "出票" ? `${record.FromCity} - ${record.ToCity}` : "-"), render: (text, record) => (record.CostType == "出票" ? `${record.FromCity}` : "-"),
},
{
title: "抵达",
key: "ToCity",
dataIndex: "ToCity",
render: (text, record) => (record.CostType == "出票" ? `${record.ToCity}` : "-"),
}, },
{ {
title: "航班", title: "航班",
@ -90,17 +92,21 @@ const Invoice = props => {
], ],
key: "Cost", key: "Cost",
}, },
{
title: "服务费",
children: [
{
title: vEIFlightBill && vEIFlightBill.reduce((acc, curr) => acc + curr.ServiceFee, 0),
dataIndex: "ServiceFee",
},
],
},
{ {
title: "折扣", title: "折扣",
key: "Discount", key: "Discount",
dataIndex: "Discount", dataIndex: "Discount",
render: (text, record) => (record.CostType == "出票" ? text : "-"), render: (text, record) => (record.CostType == "出票" ? text : "-"),
}, },
{
title: "备注",
key: "Memo",
dataIndex: "Memo",
},
{ {
title: "审核状态", title: "审核状态",
children: [ children: [
@ -117,10 +123,28 @@ const Invoice = props => {
待提交 待提交
</Checkbox> </Checkbox>
) : ( ) : (
record.CheckStatusName <Steps
size="small"
current={record.CheckStatus - 1}
items={[
{
title: "提交账单",
},
{
title: "顾问审核",
},
{
title: "财务处理",
},
{
title: "账单支付",
},
]}
/>
), ),
}, },
], ],
sorter: (a, b) => { sorter: (a, b) => {
return b.CheckStatus - a.CheckStatus; return b.CheckStatus - a.CheckStatus;
}, },
@ -132,7 +156,7 @@ const Invoice = props => {
}; };
// checkbox // checkbox
const handleCheckboxChange = (event, data) => { const handleCheckboxChange = (event, data) => {
const value = { CLC_SN: data.CLC_SN, WL: data.WL, OPI_SN: data.OPI_SN, OPI_Email: data.OPI_Email, GRI_SN: data.GRI_SN, GRI_Name: data.GRI_Name }; const value = { CLC_SN: data.CLC_SN, WL: data.WL, OPI_SN: data.OPI_SN, OPI_Email: data.OPI_Email, GRI_SN: data.GRI_SN, GRI_Name: data.GRI_Name, Cost: data.Cost, ServiceFee: data.ServiceFee };
if (event.target.checked) { if (event.target.checked) {
setSelectedValues([...selectedValues, value]); // setSelectedValues([...selectedValues, value]); //
} else { } else {
@ -166,7 +190,13 @@ const Invoice = props => {
const checkALL = () => { const checkALL = () => {
if (isEmpty(vEIFlightBill)) return; if (isEmpty(vEIFlightBill)) return;
const allChecked = selectedValues.length === vEIFlightBill.filter(item => item.CheckStatus < 2).length; const allChecked = selectedValues.length === vEIFlightBill.filter(item => item.CheckStatus < 2).length;
setSelectedValues(allChecked ? [] : vEIFlightBill.filter(item => item.CheckStatus < 2).map(item => ({ CLC_SN: item.CLC_SN, OPI_SN: item.OPI_SN }))); // setSelectedValues(
allChecked
? []
: vEIFlightBill
.filter(item => item.CheckStatus < 2)
.map(item => ({ CLC_SN: item.CLC_SN, WL: item.WL, OPI_SN: item.OPI_SN, OPI_Email: item.OPI_Email, GRI_SN: item.GRI_SN, GRI_Name: item.GRI_Name, Cost: item.Cost, ServiceFee: item.ServiceFee }))
); //
}; };
useEffect(() => {}, []); useEffect(() => {}, []);
@ -180,14 +210,14 @@ const Invoice = props => {
dates: [dayjs().startOf("M"), dayjs().endOf("M")], dates: [dayjs().startOf("M"), dayjs().endOf("M")],
}} }}
fieldsConfig={{ fieldsConfig={{
shows: ["referenceNo", "dates", "invoiceStatus"], shows: ["referenceNo", "dates", "invoiceCheckStatus"],
fieldProps: { fieldProps: {
referenceNo: { label: "搜索计划" }, referenceNo: { label: "搜索计划" },
dates: { label: "出发日期", col: 8 }, dates: { label: "出发日期", col: 8 },
}, },
}} }}
onSubmit={(err, formVal, filedsVal) => { onSubmit={(err, formVal, filedsVal) => {
getVEIFlightBill(travelAgencyId, formVal.referenceNo, formVal.invoiceStatus, formVal.startdate, formVal.endtime); getVEIFlightBill(travelAgencyId, formVal.referenceNo, formVal.invoiceCheckStatus, formVal.startdate, formVal.endtime);
}} }}
/> />
</Col> </Col>
@ -199,12 +229,20 @@ const Invoice = props => {
<Row> <Row>
<Col md={24} lg={24} xxl={24}> <Col md={24} lg={24} xxl={24}>
<Table bordered={true} rowKey="CLC_SN" columns={vEIFlightBillColumns} dataSource={vEIFlightBill} loading={loading} pagination={{ defaultPageSize: 100, showTotal: showTotal }} /> <Table bordered={true} rowKey="CLC_SN" columns={vEIFlightBillColumns} dataSource={vEIFlightBill} loading={loading} pagination={{ defaultPageSize: 100, showTotal: showTotal }} />
<TableExportBtn btnTxt="导出账单" label={`机票账单`} {...{ columns: vEIFlightBillColumns, dataSource: vEIFlightBill }} />
</Col> </Col>
<Col md={24} lg={24} xxl={24}></Col> <Col md={24} lg={24} xxl={24}></Col>
</Row> </Row>
<Row> <Row>
<Col md={24} lg={22} xxl={22}></Col> <Col md={24} lg={18} xxl={18}></Col>
<Col md={24} lg={2} xxl={2}>
<Statistic title="已选机票价格" value={selectedValues.reduce((acc, curr) => acc + curr.Cost, 0)} />
</Col>
<Col md={24} lg={2} xxl={2}>
<Statistic title="已选服务费" value={selectedValues.reduce((acc, curr) => acc + curr.ServiceFee, 0)} />
</Col>
<Col md={24} lg={2} xxl={2}> <Col md={24} lg={2} xxl={2}>
<Button type="primary" size="large" onClick={postInvoice} disabled={selectedValues.length == 0}> <Button type="primary" size="large" onClick={postInvoice} disabled={selectedValues.length == 0}>
提交账单 提交账单

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

@ -1,10 +1,10 @@
import { useState, useEffect } from "react"; import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Modal, Form, Input, Col, Row, Space, Collapse, Table, Button, Select, App, Popconfirm, Switch } from "antd"; import { Checkbox, Divider, DatePicker, Modal, Form, Input, Col, Row, Space, Collapse, Table, Button, Select, App, Popconfirm, Switch, Radio, List } from "antd";
import { PhoneOutlined, FrownTwoTone, LikeTwoTone, ArrowUpOutlined, ArrowDownOutlined } from "@ant-design/icons"; import { PhoneOutlined, FrownTwoTone, LikeTwoTone, ArrowUpOutlined, ArrowDownOutlined, PlusOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom"; import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons"; import { isEmpty, formatColonTime } from "@/utils/commons";
import { OFFICEWEBVIEWERURL } from "@/config"; import { OFFICEWEBVIEWERURL } from "@/config";
import dayjs from "dayjs";
import airTicketStore from "@/stores/Airticket"; import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage"; import { usingStorage } from "@/hooks/usingStorage";
import BackBtn from "@/components/BackBtn"; import BackBtn from "@/components/BackBtn";
@ -12,7 +12,24 @@ import BackBtn from "@/components/BackBtn";
const AirticketPlan = props => { const AirticketPlan = props => {
const { coli_sn, gri_sn } = useParams(); const { coli_sn, gri_sn } = useParams();
const { travelAgencyId, loginToken, userId } = usingStorage(); const { travelAgencyId, loginToken, userId } = usingStorage();
const [getPlanDetail, planDetail, getGuestList, guestList, loading, postFlightDetail, postFlightCost, deleteFlightCost, getVeiPlanChange, veiPlanChangeTxt, postVeiFlightPlanConfirm, ticketIssuedNotifications] = airTicketStore(state => [ const [
getPlanDetail,
planDetail,
getGuestList,
guestList,
loading,
postFlightDetail,
postFlightCost,
deleteFlightCost,
getVeiPlanChange,
veiPlanChangeTxt,
postVeiFlightPlanConfirm,
ticketIssuedNotifications,
delete_flight_info,
getAirPortList,
airPortList,
airLineList,
] = airTicketStore(state => [
state.getPlanDetail, state.getPlanDetail,
state.planDetail, state.planDetail,
state.getGuestList, state.getGuestList,
@ -25,6 +42,10 @@ const AirticketPlan = props => {
state.veiPlanChangeTxt, state.veiPlanChangeTxt,
state.postVeiFlightPlanConfirm, state.postVeiFlightPlanConfirm,
state.ticketIssuedNotifications, state.ticketIssuedNotifications,
state.delete_flight_info,
state.getAirPortList,
state.airPortList,
state.airLineList,
]); ]);
const reservationUrl = `https://p9axztuwd7x8a7.mycht.cn/Service_BaseInfoWeb/FlightPlanDocx?GRI_SN=${gri_sn}&VEI_SN=${travelAgencyId}&token=${loginToken}`; const reservationUrl = `https://p9axztuwd7x8a7.mycht.cn/Service_BaseInfoWeb/FlightPlanDocx?GRI_SN=${gri_sn}&VEI_SN=${travelAgencyId}&token=${loginToken}`;
const reservationPreviewUrl = OFFICEWEBVIEWERURL + encodeURIComponent(reservationUrl); const reservationPreviewUrl = OFFICEWEBVIEWERURL + encodeURIComponent(reservationUrl);
@ -32,7 +53,7 @@ const AirticketPlan = props => {
const { notification } = App.useApp(); const { notification } = App.useApp();
//console.log(reservationPreviewUrl); //console.log(reservationPreviewUrl);
// //
const guestList_select = () => { const guestList_select = () => {
return ( return (
guestList && guestList &&
@ -42,21 +63,36 @@ const AirticketPlan = props => {
); );
}; };
const guestList_OnChange = value => { const guestList_OnChange = e => {
ticket_form.setFieldsValue({ Memo: `${value}` }); ticket_form.setFieldsValue({ Memo: `${e.target.value}` });
};
//
const airPortList_select = () => {
return (
airPortList &&
airPortList.map(item => {
return { label: `${item.AirPort_Code} - ${item.AirPort_Name}`, value: item.AirPort_Name };
})
);
}; };
// //
const costListColumns = [ const costListColumns = [
{ {
title: "费用类型", title: "客人信息/备注",
key: "Memo",
dataIndex: "Memo",
},
{
title: "状态",
key: "CostType", key: "CostType",
dataIndex: "CostType", dataIndex: "CostType",
}, },
{ {
title: "手续费", title: "票号",
key: "ServiceFee", key: "TicketNo",
dataIndex: "ServiceFee", dataIndex: "TicketNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
}, },
{ {
title: "PNR", title: "PNR",
@ -64,12 +100,7 @@ const AirticketPlan = props => {
dataIndex: "PNR", dataIndex: "PNR",
render: (text, record) => (record.CostType == "出票" ? text : "-"), render: (text, record) => (record.CostType == "出票" ? text : "-"),
}, },
{
title: "票号",
key: "TicketNo",
dataIndex: "TicketNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{ {
title: "机票类型", title: "机票类型",
key: "FlightType", key: "FlightType",
@ -82,17 +113,18 @@ const AirticketPlan = props => {
dataIndex: "Cost", dataIndex: "Cost",
render: (text, record) => (record.CostType == "出票" ? text : "-"), render: (text, record) => (record.CostType == "出票" ? text : "-"),
}, },
{
title: "服务费",
key: "ServiceFee",
dataIndex: "ServiceFee",
},
{ {
title: "折扣", title: "折扣",
key: "Discount", key: "Discount",
dataIndex: "Discount", dataIndex: "Discount",
render: (text, record) => (record.CostType == "出票" ? text : "-"), render: (text, record) => (record.CostType == "出票" ? text : "-"),
}, },
{
title: "备注",
key: "Memo",
dataIndex: "Memo",
},
{ {
title: "编辑", title: "编辑",
key: "CLC_SN", key: "CLC_SN",
@ -115,13 +147,11 @@ const AirticketPlan = props => {
const Airticket_form = props => { const Airticket_form = props => {
const airInfo = props.airInfo; const airInfo = props.airInfo;
const [airinfo_form] = Form.useForm();
return ( return (
<> <>
<Row>
<Col md={4} lg={4} xxl={4}></Col>
<Col md={16} lg={16} xxl={16}>
<Divider orientation="left">航班信息</Divider>
<Form <Form
form={airinfo_form}
name={"ticket_form_" + airInfo.id} name={"ticket_form_" + airInfo.id}
labelCol={{ labelCol={{
span: 6, span: 6,
@ -129,7 +159,7 @@ const AirticketPlan = props => {
wrapperCol={{ wrapperCol={{
span: 16, span: 16,
}} }}
initialValues={airInfo} initialValues={{ ...airInfo, StartDate: dayjs(airInfo.StartDate) }}
onFinish={values => { onFinish={values => {
postFlightDetail(airInfo.CLF_SN, airInfo.GRI_SN, airInfo.VEI_SN, airInfo, values) postFlightDetail(airInfo.CLF_SN, airInfo.GRI_SN, airInfo.VEI_SN, airInfo, values)
.then(() => { .then(() => {
@ -152,90 +182,116 @@ const AirticketPlan = props => {
}); });
}} }}
autoComplete="off"> autoComplete="off">
<Form.Item label="城市"> <Divider orientation="left">航班信息</Divider>
<Space> <Row gutter={16}>
<Form.Item name="FromCity" noStyle> <Col md={24} lg={20} xxl={20}>
<Input placeholder="出发" prefix={<ArrowUpOutlined />} /> <Form.Item label="出发日期、航班、城市、时间" required>
</Form.Item>
<Form.Item name="ToCity" noStyle>
<Input placeholder="抵达" prefix={<ArrowDownOutlined />} />
</Form.Item>
</Space>
</Form.Item>
<Form.Item label="出发日期、航司、航班">
<Space> <Space>
<Form.Item name="StartDate" noStyle> <Form.Item name="StartDate" noStyle rules={[{ required: true, message: "请输入出发日期!" }]}>
<Input placeholder="出发日期" /> <DatePicker
</Form.Item> style={{
<Form.Item name="FlightCompany" noStyle> minWidth: 160,
<Input placeholder="航空公司" /> }}
/>
</Form.Item> </Form.Item>
<Form.Item name="FlightNo" noStyle> <Form.Item name="FlightNo" noStyle rules={[{ required: true, message: "请输入航班号!" }]}>
<Input placeholder="航班号" /> <Input placeholder="航班号" />
</Form.Item> </Form.Item>
</Space> <Form.Item name="FromCity" noStyle rules={[{ required: true, message: "请输入出发城市!" }]}>
<Input placeholder="出发" />
</Form.Item> </Form.Item>
<Form.Item label="出发"> <Form.Item name="FlightStart" noStyle rules={[{ required: true, message: "请输入出发时间!" }]}>
<Space> <Input placeholder="出发时间" />
<Form.Item name="FromAirport" noStyle>
<Input placeholder="机场" />
</Form.Item> </Form.Item>
<Form.Item name="FromTerminal" noStyle> -
<Input placeholder="航站楼" /> <Form.Item name="ToCity" noStyle rules={[{ required: true, message: "请输入抵达城市!" }]}>
<Input placeholder="抵达" />
</Form.Item> </Form.Item>
<Form.Item name="FlightStart" noStyle> <Form.Item name="FlightEnd" noStyle rules={[{ required: true, message: "请输入抵达时间!" }]}>
<Input placeholder="出发时间" /> <Input placeholder="抵达时间" />
</Form.Item> </Form.Item>
</Space> </Space>
</Form.Item> </Form.Item>
<Form.Item label="抵达"> <Form.Item label="机场、航站楼、仓位、行李重量" required>
<Space> <Space>
<Form.Item name="ToAirport" noStyle> <Form.Item name="FromAirport" noStyle rules={[{ required: true, message: "请输入出发机场!" }]}>
<Input placeholder="机场" /> <Select
showSearch
placeholder="出发机场"
style={{
minWidth: 160,
}}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
options={airPortList_select()}
/>
</Form.Item> </Form.Item>
<Form.Item name="ToTerminal" noStyle> <Form.Item name="FromTerminal" noStyle rules={[{ required: true, message: "请输入出发航站楼!" }]}>
<Input placeholder="航站楼" /> <Input placeholder="航站楼" />
</Form.Item> </Form.Item>
<Form.Item name="FlightEnd" noStyle> -
<Input placeholder="抵达时间" /> <Form.Item name="ToAirport" noStyle rules={[{ required: true, message: "请输入抵达机场!" }]}>
<Select
showSearch
placeholder="抵达机场"
style={{
minWidth: 160,
}}
filterOption={(input, option) => (option?.label ?? "").toLowerCase().includes(input.toLowerCase())}
options={airPortList_select()}
/>
</Form.Item> </Form.Item>
</Space> <Form.Item name="ToTerminal" noStyle rules={[{ required: true, message: "请输入抵达航站楼!" }]}>
<Input placeholder="航站楼" />
</Form.Item> </Form.Item>
<Form.Item name="FlightCabin" noStyle rules={[{ required: true, message: "请输入仓位!" }]}>
<Form.Item label="仓位和行李">
<Space>
<Form.Item name="FlightCabin" noStyle>
<Input placeholder="仓位" /> <Input placeholder="仓位" />
</Form.Item> </Form.Item>
<Form.Item name="Baggage" noStyle> <Form.Item name="Baggage" noStyle>
<Input placeholder="行李说明" /> <Input placeholder="行李说明 20KG" />
</Form.Item> </Form.Item>
</Space> </Space>
</Form.Item> </Form.Item>
<Form.Item label="备注" name="FlightMemo"> </Col>
<Input.TextArea rows={5} /> <Col md={24} lg={4} xxl={4}>
</Form.Item> <Space direction="vertical">
<Form.Item label="已出票" name="TicketIssued"> <Form.Item name="TicketIssued">
<Switch checkedChildren="是" unCheckedChildren="否" /> <Switch checkedChildren="已处理" unCheckedChildren="未处理" />
</Form.Item> </Form.Item>
<Form.Item
wrapperCol={{
offset: 10,
span: 16,
}}>
<Space>
<Button type="primary" htmlType="submit"> <Button type="primary" htmlType="submit">
1. 保存机票信息 1. 保存机票信息
</Button> </Button>
</Space>
</Col>
</Row>
<Divider orientation="left">出票信息</Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Table bordered={true} rowKey="CLC_SN" columns={costListColumns} dataSource={airInfo.Flightcost_AsJOSN} loading={loading} pagination={false} />
</Col>
<Col md={24} lg={4} xxl={4}>
<Space direction="vertical">
<Button type="primary" onClick={() => showModal(airInfo)}> <Button type="primary" onClick={() => showModal(airInfo)}>
2. 添加出票信息或费用 2. 添加出票信息
</Button> </Button>
</Space>
</Col>
</Row>
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label="提醒信息" name="FlightMemo_messages">
<Input placeholder="没有提醒请留空,信息会抄送给上下站地接" />
</Form.Item>
<Form.Item label="已发提醒" name="FlightMemo">
<Input.TextArea rows={4} readOnly disabled />
</Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
ticketIssuedNotifications(airInfo.CLF_SN, airInfo.OPI_SN) ticketIssuedNotifications(userId,airInfo.CLF_SN, airInfo.OPI_SN,airinfo_form.getFieldValue('FlightMemo_messages'))
.then(() => { .then(() => {
notification.success({ notification.success({
message: `成功`, message: `成功`,
@ -244,6 +300,7 @@ const AirticketPlan = props => {
duration: 4, duration: 4,
icon: <LikeTwoTone />, icon: <LikeTwoTone />,
}); });
airinfo_form.setFieldValue('FlightMemo_messages','')
}) })
.catch(() => { .catch(() => {
notification.error({ notification.error({
@ -257,18 +314,9 @@ const AirticketPlan = props => {
}}> }}>
3. 通知顾问 3. 通知顾问
</Button> </Button>
</Space>
</Form.Item>
</Form>
</Col>
<Col md={4} lg={4} xxl={4}></Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24}>
<Divider orientation="left">费用列表</Divider>
<Table bordered={true} rowKey="CLC_SN" columns={costListColumns} dataSource={airInfo.Flightcost_AsJOSN} loading={loading} pagination={false} />
</Col> </Col>
</Row> </Row>
</Form>
</> </>
); );
}; };
@ -279,7 +327,21 @@ const AirticketPlan = props => {
return { return {
key: item.id, key: item.id,
label: `${item.StartDate} ${item.FlightNo}(${item.FromAirport}${item.FlightStart}-${item.ToAirport}${item.FlightEnd})(${item.FlightCabin})`, label: `${item.StartDate} ${item.FlightNo}(${item.FromAirport}${item.FlightStart}-${item.ToAirport}${item.FlightEnd})(${item.FlightCabin})`,
extra: `${item.GRI_No}-${item.WL}`, extra: (
<Popconfirm
title="请确认要删除航班记录"
description=""
onConfirm={() => {
delete_flight_info(item.CLF_SN); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" size="small" disabled={item.Flightcost_AsJOSN.length == 0 ? false : true}>
删除
</Button>
</Popconfirm>
),
children: <Airticket_form airInfo={item} />, children: <Airticket_form airInfo={item} />,
}; };
}) })
@ -287,20 +349,25 @@ const AirticketPlan = props => {
}; };
// begin // begin
const [isModalOpen, setIsModalOpen] = useState(false); const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOpen_confirmInfo, setisModalOpen_confirmInfo] = useState(false); const [isModalOpen_confirmInfo, setisModalOpen_confirmInfo] = useState(false);
const [isTicketType, setisTicketType] = useState(true); const [isTicketType, setisTicketType] = useState(true);
const [isAddNew, setisAddNew] = useState(true); //
const [ticket_form] = Form.useForm(); const [ticket_form] = Form.useForm();
const [confirmInfo_form] = Form.useForm(); const [confirmInfo_form] = Form.useForm();
const showModal = ticket => { const showModal = ticket => {
setIsModalOpen(true); setIsModalOpen(true);
ticket_form.resetFields();
if (isEmpty(ticket.CostType)) ticket.CostType = "出票"; if (isEmpty(ticket.CostType)) ticket.CostType = "出票";
ticket.CostType == "出票" ? setisTicketType(true) : setisTicketType(false); // ticket.CostType == "出票" ? setisTicketType(true) : setisTicketType(false); //
isEmpty(ticket.CLC_SN) ? setisAddNew(true) : setisAddNew(false); //
ticket_form.setFieldsValue(ticket); ticket_form.setFieldsValue(ticket);
if (isEmpty(ticket.Memo)) ticket_form.setFieldsValue({ Memo: "" });
}; };
const handleOk = () => { const handleOk = (close_modal = true) => {
ticket_form ticket_form
.validateFields() .validateFields()
.then(values => { .then(values => {
@ -326,8 +393,7 @@ const AirticketPlan = props => {
icon: <FrownTwoTone />, icon: <FrownTwoTone />,
}); });
}); });
ticket_form.resetFields(); if (close_modal) setIsModalOpen(false);
setIsModalOpen(false);
}) })
.catch(info => { .catch(info => {
console.log("Validate Failed:", info); console.log("Validate Failed:", info);
@ -370,120 +436,6 @@ const AirticketPlan = props => {
} }
}; };
const TicketModal = () => {
return (
<>
<Modal title="费用信息" open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
<Form
form={ticket_form}
labelCol={{
span: 5,
}}>
<Form.Item label="费用类型" name="CostType">
<Select
style={{
width: 160,
}}
onChange={onChangeType}
options={[
{
value: "出票",
label: "出票",
},
{
value: "改签",
label: "改签",
},
{
value: "退票",
label: "退票",
},
{
value: "其它",
label: "其它",
},
]}
/>
</Form.Item>
<Form.Item label="手续费/费用" name="ServiceFee" rules={[{ required: true }]}>
<Input
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="PNR" name="PNR">
<Input />
</Form.Item>
<Form.Item label="票号" name="TicketNo">
<Input />
</Form.Item>
<Form.Item label="机票类型" name="FlightType">
<Select
style={{
width: 160,
}}
options={[
{
value: "成人",
label: "成人",
},
{
value: "儿童",
label: "儿童",
},
{
value: "婴儿",
label: "婴儿",
},
]}
/>
</Form.Item>
<Form.Item label="机票价格" name="Cost">
<Input
placeholder="含基建和税"
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label="折扣" name="Discount">
<Input
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label="选择客人" name="MEI_Name66">
<Select onChange={value => guestList_OnChange(value)} options={guestList_select()} />
</Form.Item>
</>
)}
<Form.Item label="备注" name="Memo">
<Input.TextArea rows={4} />
</Form.Item>
<Form.Item name="CLF_SN" hidden>
<input />{" "}
</Form.Item>
<Form.Item name="GRI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="VEI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="CLC_SN" hidden>
<input />
</Form.Item>
</Form>
</Modal>
</>
);
};
// //
const showModal_confirmInfo = ConfirmInfo => { const showModal_confirmInfo = ConfirmInfo => {
setisModalOpen_confirmInfo(true); setisModalOpen_confirmInfo(true);
@ -526,24 +478,6 @@ const AirticketPlan = props => {
console.log("Validate Failed:", info); console.log("Validate Failed:", info);
}); });
}; };
//
const ConfirmInfoModal = () => {
return (
<>
<Modal title="变更" open={isModalOpen_confirmInfo} onOk={handleOk_confirmInfo} onCancel={handleCancel_confirmInfo}>
<Form
form={confirmInfo_form}
labelCol={{
span: 5,
}}>
<Form.Item label="确认信息" name="ConfirmInfo" rules={[{ required: true }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
</>
);
};
// end // end
@ -551,6 +485,7 @@ const AirticketPlan = props => {
getPlanDetail(travelAgencyId, gri_sn); // getPlanDetail(travelAgencyId, gri_sn); //
getGuestList(coli_sn); // getGuestList(coli_sn); //
getVeiPlanChange(travelAgencyId, gri_sn); // getVeiPlanChange(travelAgencyId, gri_sn); //
getAirPortList(); //
}, []); }, []);
return ( return (
@ -564,7 +499,7 @@ const AirticketPlan = props => {
<Row> <Row>
<Col md={24} lg={24} xxl={24} style={{ height: "100%" }}> <Col md={24} lg={24} xxl={24} style={{ height: "100%" }}>
<iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl} style={{ width: "100%", height: "600px" }}></iframe> <iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl + "&v=" + Math.random()} style={{ width: "100%", height: "600px" }}></iframe>
<Button type="link" target="_blank" href={reservationUrl}> <Button type="link" target="_blank" href={reservationUrl}>
下载 下载
</Button> </Button>
@ -572,16 +507,35 @@ const AirticketPlan = props => {
</Row> </Row>
<Row> <Row>
<Divider orientation="left">出票信息</Divider> <Divider orientation="center">{planDetail ? `${planDetail[0].GRI_No} - ${planDetail[0].WL}` : ""}</Divider>
<Col md={24} lg={24} xxl={24}> <Col md={24} lg={24} xxl={24}>
<Collapse items={detail_items()} /> <Collapse items={detail_items()} />
</Col> </Col>
<Col md={24} lg={24} xxl={24}>
<br />
<p style={{ textAlign: "right" }}>
<Popconfirm
title="请确认要增加航班记录"
description=""
onConfirm={() => {
postFlightDetail("", gri_sn, travelAgencyId, { FlightNo: "新的记录", FlightStatus: 1 }, []); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" icon={<PlusOutlined />}>
新增航班记录
</Button>
</Popconfirm>
</p>
</Col>
</Row> </Row>
<Row> <Row>
<Divider orientation="left">计划变更</Divider> <Divider orientation="left">计划变更</Divider>
<Col md={24} lg={12} xxl={12}> <Col md={24} lg={12} xxl={12}>
<Space direction="vertical" style={{ width: "90%" }}> <Space direction="vertical" style={{ width: "90%" }}>
<Input.TextArea rows={4} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ChangeText} /> <Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ChangeText} />
<Button <Button
type="primary" type="primary"
onClick={() => { onClick={() => {
@ -597,8 +551,170 @@ const AirticketPlan = props => {
</Col> </Col>
</Row> </Row>
<TicketModal /> <Modal title="变更" open={isModalOpen_confirmInfo} onOk={handleOk_confirmInfo} onCancel={handleCancel_confirmInfo}>
<ConfirmInfoModal /> <Form
form={confirmInfo_form}
labelCol={{
span: 5,
}}>
<Form.Item label="确认信息" name="ConfirmInfo" rules={[{ required: true }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="费用信息"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
okText="保存"
cancelText="关闭"
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
{isAddNew ? (
<>
<Button type="primary" onClick={() => handleOk(false)}>
添加并继续新增
</Button>{" "}
<Button type="primary" onClick={() => handleOk(true)}>
添加并关闭
</Button>
</>
) : (
<OkBtn />
)}
</>
)}>
<Form
form={ticket_form}
labelCol={{
span: 5,
}}>
<Form.Item label="状态" name="CostType">
<Select
style={{
width: 160,
}}
onChange={onChangeType}
options={[
{
value: "出票",
label: "出票",
},
{
value: "改签",
label: "改签",
},
{
value: "退票",
label: "退票",
},
]}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="PNR" name="PNR" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label="票号" name="TicketNo" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label="机票类型" name="FlightType" rules={[{ required: true }]}>
<Select
style={{
width: 160,
}}
options={[
{
value: "成人",
label: "成人",
},
{
value: "儿童",
label: "儿童",
},
{
value: "婴儿",
label: "婴儿",
},
]}
/>
</Form.Item>
<Form.Item label="机票价格" name="Cost" rules={[{ required: true }]}>
<Input
placeholder="含基建和税"
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
</>
)}
<Form.Item label="服务费" name="ServiceFee" rules={[{ required: true }]}>
<Input
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="折扣" name="Discount" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
placeholder="如 0.9"
/>
</Form.Item>
<Form.Item label="选择客人" name="MEI_Name66">
<Radio.Group
onChange={e => guestList_OnChange(e)}
style={{
minWidth: 320,
}}>
<List
bordered
dataSource={guestList_select()}
renderItem={item => (
<List.Item>
<Radio value={item.value}>{item.label}</Radio>
</List.Item>
)}></List>
</Radio.Group>
{/* <Select onChange={value => guestList_OnChange(value)} options={guestList_select()} placeholder="如果列表里面没有客人信息,请手动录到备注里" /> */}
</Form.Item>
</>
)}
<Form.Item label="客人信息/备注" name="Memo">
<Input.TextArea rows={4} disabled={isTicketType} />
</Form.Item>
<Form.Item name="CLF_SN" hidden>
<input />
</Form.Item>
<Form.Item name="GRI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="VEI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="CLC_SN" hidden>
<input />
</Form.Item>
</Form>
</Modal>
</Space> </Space>
); );
}; };

@ -12,7 +12,7 @@ const { Title, Text, Paragraph } = Typography;
function Detail() { function Detail() {
const navigate = useNavigate(); const navigate = useNavigate();
const { GRI_SN, RefNo, CII_SN } = useParams(); const { GRI_SN, RefNo, CII_SN } = useParams();
const {travelAgencyId, token} = usingStorage(); const {travelAgencyId, loginToken} = usingStorage();
const desc = ['none', 'Unacceptable', 'Poor', 'Fair', 'Very Good', 'Excellent']; const desc = ['none', 'Unacceptable', 'Poor', 'Fair', 'Very Good', 'Excellent'];
const { notification } = App.useApp(); const { notification } = App.useApp();
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -168,7 +168,7 @@ function Detail() {
name='ghhfile' name='ghhfile'
// accept="image/*" // accept="image/*"
multiple={true} multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${token}`} action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${loginToken}`}
fileList={fileList} fileList={fileList}
listType='picture-card' listType='picture-card'
onChange={handleChange} onChange={handleChange}

@ -11,7 +11,7 @@ const { Title, Text, Paragraph } = Typography;
function Detail() { function Detail() {
const navigate = useNavigate(); const navigate = useNavigate();
const { GRI_SN,RefNo } = useParams(); const { GRI_SN,RefNo } = useParams();
const {travelAgencyId, token} = usingStorage(); const {travelAgencyId, loginToken} = usingStorage();
const desc = ["none", "Unacceptable", "Poor", "Fair", "Very Good", "Excellent"]; const desc = ["none", "Unacceptable", "Poor", "Fair", "Very Good", "Excellent"];
const { notification } = App.useApp(); const { notification } = App.useApp();
const [form] = Form.useForm(); const [form] = Form.useForm();
@ -166,7 +166,7 @@ function Detail() {
name="ghhfile" name="ghhfile"
// accept="image/*" // accept="image/*"
multiple={true} multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${token}`} action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GRI_SN}&VEI_SN=${travelAgencyId}&token=${loginToken}`}
fileList={fileList} fileList={fileList}
listType="picture-card" listType="picture-card"
onChange={handleChange} onChange={handleChange}

@ -1,13 +1,13 @@
import { useParams, useNavigate } from "react-router-dom"; import { useParams, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { Row, Col, Space, Button, Typography, Card, Form, Upload, Input, Divider, DatePicker, Select, App, Descriptions, Image } from "antd"; import { Row, Col, Space, Button, Typography, Card, Form, Upload, Input, Divider, DatePicker, Select, App, Descriptions, Image } from "antd";
import { PlusOutlined } from "@ant-design/icons"; import { PlusOutlined,AuditOutlined } from "@ant-design/icons";
import { isNotEmpty } from "@/utils/commons"; import { isNotEmpty } from "@/utils/commons";
import * as config from "@/config"; import * as config from "@/config";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { fetchInvoiceDetail, postEditInvoiceDetail, postAddInvoice } from '@/stores/Invoice'; import { fetchInvoiceDetail, postEditInvoiceDetail, postAddInvoice } from "@/stores/Invoice";
import { removeFeedbackImages } from '@/stores/Feedback'; import { removeFeedbackImages } from "@/stores/Feedback";
import BackBtn from '@/components/BackBtn'; import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage"; import { usingStorage } from "@/hooks/usingStorage";
const { Title, Text } = Typography; const { Title, Text } = Typography;
@ -15,7 +15,7 @@ const { Title,Text } = Typography;
function Detail() { function Detail() {
const navigate = useNavigate(); const navigate = useNavigate();
const { GMDSN, GSN } = useParams(); const { GMDSN, GSN } = useParams();
const {userId, travelAgencyId, token} = usingStorage(); const { userId, travelAgencyId, loginToken } = usingStorage();
const [form] = Form.useForm(); const [form] = Form.useForm();
const [dataLoading, setDataLoading] = useState(false); const [dataLoading, setDataLoading] = useState(false);
const [edited, setEdited] = useState(true); // const [edited, setEdited] = useState(true); //
@ -99,7 +99,7 @@ function Detail() {
}); });
} }
const fileList = (invoicekImages); const fileList = invoicekImages;
// //
let arrimg = []; let arrimg = [];
if (isNotEmpty(fileList)) { if (isNotEmpty(fileList)) {
@ -193,18 +193,18 @@ function Detail() {
} }
} }
const invoiceStatus = (FKState) => { const invoiceStatus = FKState => {
switch (FKState - 1) { switch (FKState - 1) {
case 1: case 1:
return 'Submitted'; return "Submitted";
case 2: case 2:
return 'Travel Advisor'; return "Travel Advisor";
case 3: case 3:
return 'Finance Dept'; return "Finance Dept";
case 4: case 4:
return 'Paid'; return "Paid";
default: default:
return ''; return "";
} }
}; };
@ -218,13 +218,13 @@ function Detail() {
<Row key={data.GMD_SN} gutter={16}> <Row key={data.GMD_SN} gutter={16}>
<Col span={4}></Col> <Col span={4}></Col>
<Col span={16}> <Col span={16}>
<Card type="inner" title={"Invoice " + ++index}> <Card type="inner" title={"Invoice " + (index + 1)}>
<Row gutter={16}> <Row gutter={16}>
<Col span={12}> <Col span={12}>
<Descriptions column={1}> <Descriptions column={1}>
<Descriptions.Item label="Amount">{data.GMD_Cost}</Descriptions.Item> <Descriptions.Item label="Amount">{data.GMD_Cost}</Descriptions.Item>
<Descriptions.Item label="Currency">{data.GMD_Currency}</Descriptions.Item> <Descriptions.Item label="Currency">{data.GMD_Currency}</Descriptions.Item>
<Descriptions.Item label="Due Dat">{data.GMD_PayDate}</Descriptions.Item> <Descriptions.Item label="Due Date">{data.GMD_PayDate}</Descriptions.Item>
<Descriptions.Item label="Status">{invoiceStatus(data.FKState)}</Descriptions.Item> <Descriptions.Item label="Status">{invoiceStatus(data.FKState)}</Descriptions.Item>
</Descriptions> </Descriptions>
</Col> </Col>
@ -232,13 +232,15 @@ function Detail() {
<Image.PreviewGroup> <Image.PreviewGroup>
{invoicePicList[index] && {invoicePicList[index] &&
invoicePicList[index].map(item => { invoicePicList[index].map(item => {
if (item.url) {
return <Image key={item.uid} width={90} src={item.url} />; return <Image key={item.uid} width={90} src={item.url} />;
}
})} })}
</Image.PreviewGroup> </Image.PreviewGroup>
</Col> </Col>
</Row> </Row>
</Card> </Card>
{addButton(index++ == invoiceZDDetail.length)} {addButton(index + 1 == invoiceZDDetail.length)}
</Col> </Col>
<Col span={4}></Col> <Col span={4}></Col>
</Row> </Row>
@ -251,7 +253,7 @@ function Detail() {
<Col span={16}> <Col span={16}>
<Card <Card
type="inner" type="inner"
title={"Invoice " + ++index} title={"Invoice " + (index + 1)}
extra={ extra={
<Button type="link" onClick={() => setEdited(false)}> <Button type="link" onClick={() => setEdited(false)}>
Edit Edit
@ -283,17 +285,21 @@ function Detail() {
]}> ]}>
<Select placeholder="Select Currency type" onChange={onCurrencyChange} options={bindCurrency()}></Select> <Select placeholder="Select Currency type" onChange={onCurrencyChange} options={bindCurrency()}></Select>
</Form.Item> </Form.Item>
<Form.Item name="info_date" label="Due Month" <Form.Item
name="info_date"
label="Due Month"
rules={[ rules={[
{ {
required: true, required: true,
message: "please select Due Month!", message: "please select Due Month!",
}, },
]} ]}>
>
<DatePicker picker="month" /> <DatePicker picker="month" />
</Form.Item> </Form.Item>
<Text type="secondary">Payment is arranged during the last week of each month. If the invoice is issued after the 20th, please select the following month for payment. For urgent payments, please contact the travel advisor. </Text> <Text type="secondary">
Payment is arranged during the last week of each month. If the invoice is issued after the 20th, please select the following month for payment. For urgent payments, please contact the
travel advisor.{" "}
</Text>
<Form.Item name="info_gmdsn" hidden={true}> <Form.Item name="info_gmdsn" hidden={true}>
<input /> <input />
</Form.Item> </Form.Item>
@ -309,13 +315,12 @@ function Detail() {
<Upload <Upload
name="ghhfile" name="ghhfile"
multiple={true} multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GSN}&VEI_SN=${travelAgencyId}&FilePathName=invoice&token=${token}`} action={config.OVERSEA_HOST + `/service-fileServer/FileUpload?GRI_SN=${GSN}&VEI_SN=${travelAgencyId}&FilePathName=invoice&token=${loginToken}`}
fileList={fileList} fileList={fileList}
listType="picture-card" listType="picture-card"
onChange={handleChange} onChange={handleChange}
onRemove={handRemove} onRemove={handRemove}
accept=".jpg,.png,.peg,.bmp" accept=".jpg,.png,.peg,.bmp">
>
<div> <div>
<PlusOutlined /> <PlusOutlined />
<div style={{ marginTop: 8 }}>Upload Invoice</div> <div style={{ marginTop: 8 }}>Upload Invoice</div>
@ -353,6 +358,9 @@ function Detail() {
</Col> </Col>
<Col span={4}> <Col span={4}>
<BackBtn /> <BackBtn />
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/history/0/338787`)}>
Billing Records
</Button>
</Col> </Col>
</Row> </Row>
<Title level={5}></Title> <Title level={5}></Title>
@ -362,4 +370,4 @@ function Detail() {
); );
} }
export default (Detail); export default Detail;

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

@ -72,7 +72,7 @@ function Index() {
return ( return (
<Space direction="vertical" style={{ width: "100%" }}> <Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}> <Row gutter={16}>
<Col flex="auto"> <Col md={16} sm={16} xs={24} >
<SearchForm <SearchForm
initialValue={{ initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')], dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
@ -90,13 +90,15 @@ function Index() {
}} }}
/> />
</Col> </Col>
<Col md={24} lg={4} xxl={4}> <Col md={8} sm={8} xs={24} >
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/338787`)}> <Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/395074`)}>
Misc. Invoice Misc. Invoice
</Button> </Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/paid`)}> <Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/paid`)}>
Bank statement Bank statement
</Button> </Button>
</Space>
</Col> </Col>
</Row> </Row>
<Row> <Row>

@ -43,11 +43,11 @@ function Detail() {
backTo={false} backTo={false}
header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} handleNewProduct={() => setAddProductVisible(true)} />}> header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} handleNewProduct={() => setAddProductVisible(true)} />}>
<> <>
<Flex gap={10}> <Flex gap={10} className='h-full'>
{/* onNodeSelect={handleNodeSelect} */} {/* onNodeSelect={handleNodeSelect} */}
<ProductsTree className='basis-80 sticky top-16 overflow-y-auto shrink-0' style={{ height: 'calc(100vh - 150px)' }} /> <ProductsTree className='basis-80 sticky top-0 overflow-y-auto shrink-0' style1={{ height: 'calc(100vh - 150px)' }} />
<Divider type={'vertical'} className='mx-1 h-auto' /> <Divider type={'vertical'} className='mx-1 h-auto' />
<div className=' flex-auto '> <div className=' flex-auto overflow-auto '>
<ProductInfo /> <ProductInfo />
</div> </div>
</Flex> </Flex>

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

@ -10,12 +10,15 @@ import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT } from '@/config'; import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
import { useProductsTypesMapVal } from '@/hooks/useProductsSets'; import { useProductsTypesMapVal } from '@/hooks/useProductsSets';
import { usingStorage } from '@/hooks/usingStorage'; import { usingStorage } from '@/hooks/usingStorage';
import useProductsStore from '@/stores/Products/Index';
const NewAddonModal = ({ onPick, ...props }) => { const NewAddonModal = ({ onPick, ...props }) => {
const { travel_agency_id, use_year } = useParams(); // const { travel_agency_id, use_year } = useParams();
const { t } = useTranslation(); const { t } = useTranslation();
const { notification, message } = App.useApp(); const { notification, message } = App.useApp();
const [{ travel_agency_id, use_year }] = useProductsStore((state) => [state.switchParams]);
const productsTypesMapVal = useProductsTypesMapVal(); const productsTypesMapVal = useProductsTypesMapVal();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -29,7 +32,7 @@ const NewAddonModal = ({ onPick, ...props }) => {
setSearchLoading(true); setSearchLoading(true);
setSearchResult([]); setSearchResult([]);
const search_year = year || use_year; const search_year = year || use_year;
const result = await searchPublishedProductsAction({ ...param, use_year: search_year, travel_agency_id, }); const result = await searchPublishedProductsAction({ ...param, use_year: search_year, travel_agency_id });
setSearchResult(result); setSearchResult(result);
setSearchLoading(false); setSearchLoading(false);
}; };
@ -110,8 +113,9 @@ const Extras = ({ productId, onChange, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const { notification, message } = App.useApp(); const { notification, message } = App.useApp();
const { travel_agency_id, use_year } = useParams(); // const { travel_agency_id, use_year } = useParams();
const { travelAgencyId } = usingStorage(); const { travelAgencyId } = usingStorage();
const [{travel_agency_id, use_year}] = useProductsStore((state) => [state.switchParams]);
const [extrasData, setExtrasData] = useState([]); const [extrasData, setExtrasData] = useState([]);
@ -132,7 +136,7 @@ const Extras = ({ productId, onChange, ...props }) => {
} }
const handleDelAddon = async (item) => { const handleDelAddon = async (item) => {
const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, extras: [item.id] }); const delSuccess = await delProductExtrasAction({ travel_agency_id, id: productId, del_extras_ids: [item.id] });
delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`); delSuccess ? message.success(`${t('Success')}`) : message.error(`${t('Failed')}`);
await handleGetAgencyProductExtras(); await handleGetAgencyProductExtras();
}; };
@ -150,7 +154,7 @@ const Extras = ({ productId, onChange, ...props }) => {
dataIndex: 'operation', dataIndex: 'operation',
width: '4rem', width: '4rem',
render: (_, r) => ( render: (_, r) => (
<Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r)} okText={t('Yes')} > <Popconfirm title={t('sureDelete')} onConfirm={(e) => handleDelAddon(r.info)} okText={t('Yes')} >
<Button size='small' type='link' danger> <Button size='small' type='link' danger>
{t('Delete')} {t('Delete')}
</Button> </Button>

@ -1,34 +1,46 @@
import { useEffect, useState } from 'react'; import { useEffect, useState } from "react";
import { useParams, Link, useNavigate, useLocation } from 'react-router-dom'; import { useParams, Link, useNavigate, useLocation } from "react-router-dom";
import { App, Button, Divider, Popconfirm, Select } from 'antd'; import { App, Button, Divider, Popconfirm, Select } from "antd";
import { ReloadOutlined } from '@ant-design/icons'; import { ReloadOutlined } from "@ant-design/icons";
import { useProductsAuditStatesMapVal } from '@/hooks/useProductsSets'; import { useProductsAuditStatesMapVal } from "@/hooks/useProductsSets";
import { useTranslation } from 'react-i18next'; import { useTranslation } from "react-i18next";
import useProductsStore, { postAgencyProductsAuditAction, postAgencyAuditAction, getAgencyAllExtrasAction } from '@/stores/Products/Index'; import useProductsStore, {
import { isEmpty, objectMapper } from '@/utils/commons'; postAgencyProductsAuditAction,
import useAuthStore from '@/stores/Auth'; postAgencyAuditAction,
import RequireAuth from '@/components/RequireAuth'; getAgencyAllExtrasAction,
} from "@/stores/Products/Index";
import { isEmpty, objectMapper } from "@/utils/commons";
import useAuthStore from "@/stores/Auth";
import RequireAuth from "@/components/RequireAuth";
// import PrintContractPDF from './PrintContractPDF'; // import PrintContractPDF from './PrintContractPDF';
import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from '@/config'; import { PERM_PRODUCTS_OFFER_AUDIT, PERM_PRODUCTS_OFFER_PUT } from "@/config";
import dayjs from 'dayjs'; import dayjs from "dayjs";
import VendorSelector from '@/components/VendorSelector'; import VendorSelector from "@/components/VendorSelector";
import AuditStateSelector from '@/components/AuditStateSelector'; import AuditStateSelector from "@/components/AuditStateSelector";
import NewProductModal from './NewProductModal'; import { usingStorage } from "@/hooks/usingStorage";
import AgencyContract from './../Print/AgencyContract'; import AgencyContract from "../Print/AgencyContract";
// import AgencyContract from "../Print/AgencyContract_v0903";
import { saveAs } from "file-saver"; import { saveAs } from "file-saver";
import { Packer } from "docx"; import { Packer } from "docx";
const Header = ({ refresh, ...props }) => { const Header = ({ refresh, ...props }) => {
const location = useLocation(); const location = useLocation();
const isEditPage = location.pathname.includes('edit'); const isEditPage = location.pathname.includes("edit");
const showEditA = !location.pathname.includes('edit'); const showEditA = !location.pathname.includes("edit");
const showAuditA = !location.pathname.includes('audit'); const showAuditA = !location.pathname.includes("audit");
const { travel_agency_id, use_year, audit_state } = useParams(); const { travel_agency_id, use_year, audit_state } = useParams();
const { travelAgencyId } = usingStorage();
const { t } = useTranslation(); const { t } = useTranslation();
const isPermitted = useAuthStore((state) => state.isPermitted); const isPermitted = useAuthStore((state) => state.isPermitted);
const [activeAgency, setActiveAgency] = useProductsStore((state) => [state.activeAgency, state.setActiveAgency]); const [activeAgency, setActiveAgency] = useProductsStore((state) => [
const [switchParams, setSwitchParams] = useProductsStore((state) => [state.switchParams, state.setSwitchParams]); state.activeAgency,
state.setActiveAgency,
]);
const [switchParams, setSwitchParams] = useProductsStore((state) => [
state.switchParams,
state.setSwitchParams,
]);
// const [activeAgencyState] = useProductsStore((state) => [state.activeAgencyState]); // const [activeAgencyState] = useProductsStore((state) => [state.activeAgencyState]);
const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]); const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
const stateMapVal = useProductsAuditStatesMapVal(); const stateMapVal = useProductsAuditStatesMapVal();
@ -37,26 +49,49 @@ const Header = ({ refresh, ...props }) => {
const yearOptions = []; const yearOptions = [];
const currentYear = switchParams.use_year || dayjs().year(); const currentYear = switchParams.use_year || dayjs().year();
const baseYear = use_year ? Number(use_year === 'all' ? currentYear : use_year) : currentYear; const baseYear = use_year
? Number(use_year === "all" ? currentYear : use_year)
: currentYear;
for (let i = currentYear - 5; i <= baseYear + 5; i++) { for (let i = currentYear - 5; i <= baseYear + 5; i++) {
yearOptions.push({ label: i, value: i }); yearOptions.push({ label: i, value: i });
} }
const [param, setParam] = useState({ pick_year: baseYear, pick_agency: travel_agency_id }); const { getRemarkList } = useProductsStore((selector) => ({
getRemarkList: selector.getRemarkList,
}));
const [param, setParam] = useState({
pick_year: baseYear,
pick_agency: travel_agency_id,
});
const [pickYear, setPickYear] = useState(baseYear); const [pickYear, setPickYear] = useState(baseYear);
const [pickAgency, setPickAgency] = useState({ value: activeAgency.travel_agency_id, label: activeAgency.travel_agency_name }); const [pickAgency, setPickAgency] = useState({
value: activeAgency.travel_agency_id,
label: activeAgency.travel_agency_name,
});
const [pickAuditState, setPickAuditState] = useState(); const [pickAuditState, setPickAuditState] = useState();
useEffect(() => { useEffect(() => {
const _param = objectMapper(param, { pick_year: 'use_year', pick_agency: 'travel_agency_id', pick_state: 'audit_state' }); const _param = objectMapper(param, {
setSwitchParams({ ..._param }); pick_year: "use_year",
pick_agency: "travel_agency_id",
pick_state: "audit_state",
});
setSwitchParams({
..._param,
travel_agency_id: _param?.travel_agency_id || travelAgencyId,
});
refresh(param); refresh(param);
return () => {}; return () => {};
}, [param]); }, [param]);
const emptyPickState = { value: '', label: t('products:State') }; const emptyPickState = { value: "", label: t("products:State") };
useEffect(() => { useEffect(() => {
const baseState = audit_state ? (audit_state === 'all' ? emptyPickState : stateMapVal[`${audit_state}`]) : emptyPickState; const baseState = audit_state
? audit_state === "all"
? emptyPickState
: stateMapVal[`${audit_state}`]
: emptyPickState;
if (isEmpty(pickAuditState)) { if (isEmpty(pickAuditState)) {
setPickAuditState(baseState); setPickAuditState(baseState);
} }
@ -83,20 +118,23 @@ const Header = ({ refresh, ...props }) => {
// const s = Object.keys(agencyProducts).map((typeKey) => { // const s = Object.keys(agencyProducts).map((typeKey) => {
// }); // });
postAgencyProductsAuditAction(state, { travel_agency_id: activeAgency.travel_agency_id, use_year: switchParams.use_year }) postAgencyProductsAuditAction(state, {
travel_agency_id: activeAgency.travel_agency_id,
use_year: switchParams.use_year,
})
.then((json) => { .then((json) => {
if (json.errcode === 0) { if (json.errcode === 0) {
message.success(json.errmsg); message.success(json.errmsg);
if (typeof refresh === 'function') { if (typeof refresh === "function") {
refresh(param); refresh(param);
} }
} }
}) })
.catch((ex) => { .catch((ex) => {
notification.error({ notification.error({
message: 'Notification', message: "Notification",
description: ex.message, description: ex.message,
placement: 'top', placement: "top",
duration: 4, duration: 4,
}); });
}); });
@ -106,112 +144,168 @@ const Header = ({ refresh, ...props }) => {
postAgencyAuditAction(activeAgency.travel_agency_id, switchParams.use_year) postAgencyAuditAction(activeAgency.travel_agency_id, switchParams.use_year)
.then((json) => { .then((json) => {
if (json.errcode === 0) { if (json.errcode === 0) {
message.success(t('Success')); message.success(t("Success"));
if (typeof refresh === 'function') { if (typeof refresh === "function") {
refresh(param); refresh(param);
const auditPagePath = isPermitted(PERM_PRODUCTS_OFFER_AUDIT) const auditPagePath = isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${switchParams.use_year}/all/audit` ? `/products/${activeAgency.travel_agency_id}/${switchParams.use_year}/all/audit`
: isPermitted(PERM_PRODUCTS_OFFER_PUT) : isPermitted(PERM_PRODUCTS_OFFER_PUT)
? `/products/audit` ? `/products/audit`
: ''; : "";
navigate(auditPagePath); navigate(auditPagePath);
} }
} }
}) })
.catch((ex) => { .catch((ex) => {
notification.error({ notification.error({
message: 'Notification', message: "Notification",
description: ex.message, description: ex.message,
placement: 'top', placement: "top",
duration: 4, duration: 4,
}); });
}); });
}; };
const handleDownload = async () => { const handleDownload = async () => {
// await refresh(); // await refresh();
const agencyExtras = await getAgencyAllExtrasAction(switchParams); const agencyExtras = await getAgencyAllExtrasAction(switchParams);
const remarks = await getRemarkList()
const documentCreator = new AgencyContract(); const documentCreator = new AgencyContract();
const doc = documentCreator.create([switchParams, activeAgency, agencyProducts, agencyExtras]); const doc = documentCreator.create([
switchParams,
activeAgency,
agencyProducts,
agencyExtras,
remarks
]);
const _d = dayjs().format('YYYYMMDD_HH.mm.ss.SSS'); // Date.now().toString(32) const _d = dayjs().format("YYYYMMDD_HH.mm.ss.SSS"); // Date.now().toString(32)
Packer.toBlob(doc).then(blob => { Packer.toBlob(doc).then((blob) => {
saveAs(blob, `${activeAgency.travel_agency_name}${pickYear}年地接合同-${_d}.docx`); saveAs(
blob,
`${activeAgency.travel_agency_name}${pickYear}年地接合同-${_d}.docx`
);
}); });
}; };
return ( return (
<div className='flex justify-end items-center gap-4 h-full'> <div className="flex justify-end items-center gap-4 h-full">
<div className='grow'> <div className="grow">
<h2 className='m-0 leading-tight'> <h2 className="m-0 leading-tight">
{isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? ( {isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? (
<VendorSelector <VendorSelector
value={{ label: activeAgency.travel_agency_name, value: activeAgency.travel_agency_id }} value={{
label: activeAgency.travel_agency_name,
value: activeAgency.travel_agency_id,
}}
onChange={handleAgencyChange} onChange={handleAgencyChange}
allowClear={false} allowClear={false}
mode={null} mode={null}
className='w-72' className="w-72"
size='large' size="large"
variant={'borderless'} variant={"borderless"}
/> />
) : ( ) : (
activeAgency.travel_agency_name activeAgency.travel_agency_name
)} )}
<Divider type={'vertical'} /> <Divider type={"vertical"} />
<Select options={yearOptions} variant={'borderless'} className='w-24' size='large' value={pickYear} onChange={handleYearChange} /> <Select
<Divider type={'vertical'} /> options={yearOptions}
<AuditStateSelector variant={'borderless'} className='w-32' size='large' value={pickAuditState} onChange={handleAuditStateChange} /> variant={"borderless"}
className="w-24"
size="large"
value={pickYear}
onChange={handleYearChange}
/>
<Divider type={"vertical"} />
<AuditStateSelector
variant={"borderless"}
className="w-32"
size="large"
value={pickAuditState}
onChange={handleAuditStateChange}
/>
{/* <Divider type={'vertical'} /> {/* <Divider type={'vertical'} />
{(use_year || '').replace('all', '')} */} {(use_year || '').replace('all', '')} */}
<Button onClick={() => refresh(param)} type='text' className='text-primary round-none' icon={<ReloadOutlined />} /> <Button
onClick={() => refresh(param)}
type="text"
className="text-primary round-none"
icon={<ReloadOutlined />}
/>
</h2> </h2>
</div> </div>
{/* todo: export, 审核完成之后才能导出 */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size="small" onClick={handleDownload}>
{t("Export")} .docx
</Button>
{/* <PrintContractPDF /> */}
</RequireAuth>
{/* {activeAgencyState === 0 && ( */}
<>
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
<Popconfirm
title={t("products:sureSubmitAudit")}
onConfirm={handleSubmitForAudit}
okText={t("Yes")}
placement={"bottomLeft"}
>
<Button size="small" type={"primary"}>
{t("Submit")}
{t("Audit")}
</Button>
</Popconfirm>
</RequireAuth>
</>
{/* )} */}
{showEditA && ( {showEditA && (
<Link className='px-2' to={isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/edit` : `/products/edit`}> <Link
{t('Edit')} className="px-2"
to={
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/edit`
: `/products/edit`
}
>
{t("Edit")}
</Link> </Link>
)} )}
{showAuditA && ( {showAuditA && (
<Link className='px-2' to={isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/audit` : `/products/audit`}> <Link
{t('products:AuditRes')} className="px-2"
to={
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/audit`
: `/products/audit`
}
>
{t("products:AuditRes")}
</Link> </Link>
)} )}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}> <RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' type={'primary'} onClick={() => handleAuditAgency('1')}> <Button
{t('products:auditStateAction.Published')} size="small"
type={"primary"}
onClick={() => handleAuditAgency("1")}
>
{t("products:auditStateAction.Published")}
</Button> </Button>
</RequireAuth> </RequireAuth>
{/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditAgency('2')}> {/* <Button size='small' type={'primary'} ghost onClick={() => handleAuditAgency('2')}>
{t('products:auditStateAction.Approved')} {t('products:auditStateAction.Approved')}
</Button> */} </Button> */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}> <RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' type={'primary'} danger ghost onClick={() => handleAuditAgency('3')}> <Button
{t('products:auditStateAction.Rejected')} size="small"
type={"primary"}
danger
ghost
onClick={() => handleAuditAgency("3")}
>
{t("products:auditStateAction.Rejected")}
</Button> </Button>
</RequireAuth> </RequireAuth>
{/* todo: export, 审核完成之后才能导出 */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' onClick={handleDownload}>{t('Export')} .docx</Button>
{/* <PrintContractPDF /> */}
</RequireAuth>
{/* 编辑 */}
<Divider type='vertical' />
{isEditPage && (
<NewProductModal />
)}
{/* {activeAgencyState === 0 && ( */}
<>
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}>
<Popconfirm title={t('products:sureSubmitAudit')} onConfirm={handleSubmitForAudit} okText={t('Yes')} placement={'bottomLeft'}>
<Button size='small' type={'primary'} >
{t('Submit')}
{t('Audit')}
</Button>
</Popconfirm>
</RequireAuth>
</>
{/* )} */}
</div> </div>
); );
}; };

@ -8,7 +8,7 @@ import useProductsStore from '@/stores/Products/Index';
import { useNewProductRecord, useProductsTypesMapVal } from '@/hooks/useProductsSets'; import { useNewProductRecord, useProductsTypesMapVal } from '@/hooks/useProductsSets';
import { useDefaultLgc } from '@/i18n/LanguageSwitcher'; import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
import RequireAuth from '@/components/RequireAuth'; import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_OFFER_PUT } from '@/config'; import { PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_NEW } from '@/config';
export const NewProductsForm = ({ initialValues, onFormInstanceReady, ...props }) => { export const NewProductsForm = ({ initialValues, onFormInstanceReady, ...props }) => {
const { t } = useTranslation('products'); const { t } = useTranslation('products');
@ -91,7 +91,7 @@ export const NewProductModal = ({ initialValues }) => {
}; };
return ( return (
<> <>
<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT}> <RequireAuth subject={PERM_PRODUCTS_NEW}>
<Button size='small' type={'primary'} onClick={() => setOpen(true)}> <Button size='small' type={'primary'} onClick={() => setOpen(true)}>
{t('New')} {t('New')}
{t('products:#')} {t('products:#')}

@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { useProductsTypesMapVal, useNewProductRecord } from '@/hooks/useProductsSets'; import { useProductsTypesMapVal, useNewProductRecord } from '@/hooks/useProductsSets';
import useProductsStore, { postProductsSaveAction } from '@/stores/Products/Index'; import useProductsStore, { postProductsSaveAction } from '@/stores/Products/Index';
import useAuthStore from '@/stores/Auth'; import useAuthStore from '@/stores/Auth';
import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'; import { PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT, PERM_PRODUCTS_INFO_PUT, PERM_PRODUCTS_NEW } from '@/config';
import { isEmpty, pick } from '@/utils/commons'; import { isEmpty, pick } from '@/utils/commons';
import ProductInfoForm from './ProductInfoForm'; import ProductInfoForm from './ProductInfoForm';
import { usingStorage } from '@/hooks/usingStorage'; import { usingStorage } from '@/hooks/usingStorage';
@ -31,7 +31,7 @@ const ProductInfo = ({ ...props }) => {
const hasHT = (editingProduct?.info?.htid || 0) > 0; const hasHT = (editingProduct?.info?.htid || 0) > 0;
// const hasAuditPer = isPermitted(PERM_PRODUCTS_OFFER_AUDIT); // const hasAuditPer = isPermitted(PERM_PRODUCTS_OFFER_AUDIT);
const hasEditPer = isPermitted(PERM_PRODUCTS_OFFER_PUT); const hasEditPer = isPermitted(PERM_PRODUCTS_INFO_PUT) || isPermitted(PERM_PRODUCTS_NEW); // || isPermitted(PERM_PRODUCTS_OFFER_PUT);
setEditablePerm(topPerm || hasEditPer); setEditablePerm(topPerm || hasEditPer);
// setEditable(topPerm || (hasAuditPer ? true : (!hasHT && hasEditPer))); // setEditable(topPerm || (hasAuditPer ? true : (!hasHT && hasEditPer)));
@ -40,14 +40,35 @@ const ProductInfo = ({ ...props }) => {
setInfoEditable(topPerm || (!hasHT && hasEditPer)); setInfoEditable(topPerm || (!hasHT && hasEditPer));
const _priceEditable = [-1, 3].includes(activeAgency?.audit_state_id) || isEmpty(editingProduct?.info?.id); const _priceEditable = [-1, 3].includes(activeAgency?.audit_state_id) || isEmpty(editingProduct?.info?.id);
// setPriceEditable(topPerm || (_priceEditable && hasEditPer)); const hasPricePer = isPermitted(PERM_PRODUCTS_OFFER_PUT);
setPriceEditable(true); // debug: 0 // setPriceEditable(topPerm || (_priceEditable && hasPricePer));
setPriceEditable(topPerm || (hasPricePer));
// setPriceEditable(true); // debug: 0
const showExtras = topPerm && hasHT; // !isEmpty(editingProduct) && const showExtras = topPerm && hasHT; // !isEmpty(editingProduct) &&
setExtrasVisible(showExtras); setExtrasVisible(showExtras);
setLgcEdits({});
setInfoEditStatus('');
return () => {}; return () => {};
}, [activeAgency, editingProduct]); }, [activeAgency, editingProduct]);
const [infoEditStatus, setInfoEditStatus] = useState('');
const [lgcEdits, setLgcEdits] = useState({});
const onValuesChange = (changedValues, forms) => {
// console.log('onValuesChange', changedValues);
if ('product_title' in changedValues) {
setInfoEditStatus('2');
setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}});
}
if ('lgc_details_mapped' in changedValues) {
const lgc = Object.keys(changedValues.lgc_details_mapped)[0];
setLgcEdits({...lgcEdits, [lgc]: {'edit_status': '2'}});
} else {
setInfoEditStatus('2');
}
};
const onSave = async (err, values, forms) => { const onSave = async (err, values, forms) => {
values.travel_agency_id = activeAgency.travel_agency_id; values.travel_agency_id = activeAgency.travel_agency_id;
const copyNewProduct = structuredClone(newProductRecord); const copyNewProduct = structuredClone(newProductRecord);
@ -58,6 +79,7 @@ const ProductInfo = ({ ...props }) => {
'travel_agency_id': activeAgency.travel_agency_id, 'travel_agency_id': activeAgency.travel_agency_id,
// "travel_agency_name": "", // "travel_agency_name": "",
// "lastedit_changed": "", // "lastedit_changed": "",
"edit_status": infoEditStatus || editingProduct.info.edit_status,
}; };
const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title', const copyFields = pick(editingProduct.info, ['product_type_id']); // 'title',
const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, title: editingProduct.info.product_title, ...values.info, ...copyFields, ...poster }; const readyToSubInfo = { ...copyNewProduct.info, ...editingProduct.info, title: editingProduct.info.product_title, ...values.info, ...copyFields, ...poster };
@ -65,9 +87,15 @@ const ProductInfo = ({ ...props }) => {
/** lgc_details */ /** lgc_details */
const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: c }), {}); const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
const mergedLgc = { ...prevLgcDetailsMapped, ...values.lgc_details_mapped }; const mergedLgc = { ...prevLgcDetailsMapped, ...values.lgc_details_mapped, };
for (const lgcKey in lgcEdits) {
if (Object.prototype.hasOwnProperty.call(lgcEdits, lgcKey)) {
const element = lgcEdits[lgcKey];
mergedLgc[lgcKey].edit_status = element?.edit_status || values.lgc_details_mapped[lgcKey]?.edit_status || '2';
}
}
// console.log(values); // console.log('before save', '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
// return false; // debug: 0 // return false; // debug: 0
/** 提交保存 */ /** 提交保存 */
setLoading(true); setLoading(true);
@ -89,6 +117,7 @@ const ProductInfo = ({ ...props }) => {
success ? message.success(t('Success')) : message.error(t('Failed')); success ? message.success(t('Success')) : message.error(t('Failed'));
// //
// result.quotation = isEmpty(result.quotation) ? editingProduct.quotation : result.quotation; // result.quotation = isEmpty(result.quotation) ? editingProduct.quotation : result.quotation;
result.info.htid = editingProduct?.info?.htid;
appendNewProduct(result); appendNewProduct(result);
setEditingProduct(result); setEditingProduct(result);
}; };
@ -110,7 +139,7 @@ const ProductInfo = ({ ...props }) => {
) : ( ) : (
<> <>
<h2>{t('products:EditComponents.info')}</h2> <h2>{t('products:EditComponents.info')}</h2>
<ProductInfoForm {...{ editablePerm, infoEditable, priceEditable }} initialValues={editingProduct?.info} onSubmit={onSave} /> <ProductInfoForm {...{ editablePerm, infoEditable, priceEditable, onValuesChange }} initialValues={editingProduct?.info} onSubmit={onSave} />
<Divider className='my-1' /> <Divider className='my-1' />
{extrasVisible && <Extras productId={editingProduct?.info?.id} />} {extrasVisible && <Extras productId={editingProduct?.info?.id} />}
</> </>

@ -29,6 +29,8 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const filedsets = useProductsTypesFieldsets(editingProduct?.info?.product_type_id); const filedsets = useProductsTypesFieldsets(editingProduct?.info?.product_type_id);
const shows = filedsets[0]; const shows = filedsets[0];
const [pickEditedInfo, setPickEditedInfo] = useState({}); //
// const [editable, setEditable] = useState(true); // const [editable, setEditable] = useState(true);
const [formEditable, setFormEditable] = useState(true); const [formEditable, setFormEditable] = useState(true);
const [showSave, setShowSave] = useState(true); const [showSave, setShowSave] = useState(true);
@ -39,6 +41,8 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const lgc_details_mapped = (editingProduct?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {}); const lgc_details_mapped = (editingProduct?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
form.setFieldValue('lgc_details_mapped', lgc_details_mapped); form.setFieldValue('lgc_details_mapped', lgc_details_mapped);
form.setFieldValue('quotation', editingProduct?.quotation); form.setFieldValue('quotation', editingProduct?.quotation);
form.setFieldValue('display_to_c', editingProduct.info?.display_to_c || '0');
setPickEditedInfo({ ...pickEditedInfo, product_title: editingProduct?.info?.product_title });
setFormEditable(infoEditable || priceEditable); setFormEditable(infoEditable || priceEditable);
@ -46,7 +50,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
setShowSave(infoEditable || priceEditable); setShowSave(infoEditable || priceEditable);
// setEditable(editable0); // setEditable(editable0);
return () => {}; return () => {};
}, [editingProduct?.info?.id, editablePerm, infoEditable, priceEditable]); }, [editingProduct, editablePerm, infoEditable, priceEditable]);
const onFinish = (values) => { const onFinish = (values) => {
console.log('Received values of form, origin form value: \n', values); console.log('Received values of form, origin form value: \n', values);
@ -78,8 +82,12 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const onIValuesChange = (changedValues, allValues) => { const onIValuesChange = (changedValues, allValues) => {
const dest = formValuesMapper(allValues); const dest = formValuesMapper(allValues);
// console.log('form onValuesChange', Object.keys(changedValues), changedValues); // console.log('form onValuesChange', Object.keys(changedValues), changedValues);
if ('product_title' in changedValues) {
const editTitle = (changedValues.product_title);
setPickEditedInfo({ ...pickEditedInfo, product_title: editTitle });
}
if (typeof onValuesChange === 'function') { if (typeof onValuesChange === 'function') {
onValuesChange(dest); onValuesChange(changedValues, dest);
} }
}; };
const onFieldsChange = (hangedFields, allFields) => { const onFieldsChange = (hangedFields, allFields) => {
@ -130,7 +138,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
}, },
}), }),
]}> ]}>
<ProductInfoLgc editable={infoEditable} formInstance={form} /> <ProductInfoLgc editable={infoEditable} formInstance={form} pickEditedInfo={pickEditedInfo} />
</Form.Item> </Form.Item>
<Form.Item name='quotation'> <Form.Item name='quotation'>
@ -243,19 +251,20 @@ function getFields(props) {
99, 99,
<Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}> <Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}>
{/* <Input placeholder={t('RecommendsRate')} allowClear /> */} {/* <Input placeholder={t('RecommendsRate')} allowClear /> */}
<Select <InputNumber {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} />
{/* <Select
{...styleProps} {...styleProps}
{...editableProps('recommends_rate')} {...editableProps('recommends_rate')}
style={{ width: '100%' }} style={{ width: '100%' }}
labelInValue={false} labelInValue={false}
options={[ options={[
{ value: '1', label: 'Top 1' }, { value: 1, label: 'Top 1' },
{ value: '2', label: 'Top 2' }, { value: 2, label: 'Top 2' },
{ value: '3', label: 'Top 3' }, { value: 3, label: 'Top 3' },
{ value: '4', label: '4' }, { value: 4, label: '4' },
{ value: '5', label: '5' }, { value: 5, label: '5' },
]} ]}
/> /> */}
</Form.Item>, </Form.Item>,
fieldProps?.recommends_rate?.col || midCol fieldProps?.recommends_rate?.col || midCol
), ),
@ -288,7 +297,7 @@ function getFields(props) {
labelInValue={false} labelInValue={false}
options={[ options={[
{ value: '153001', label: '在计划显示,不在报价信显示' }, { value: '153001', label: '在计划显示,不在报价信显示' },
{ value: 0, label: '计划和报价信都要显示' }, { value: '0', label: '计划和报价信都要显示' },
{ value: '153001, 153002', label: '计划和报价信都不用显示' }, { value: '153001, 153002', label: '计划和报价信都不用显示' },
]} ]}
{...styleProps} {...styleProps}
@ -369,7 +378,7 @@ const formValuesMapper = (values) => {
], ],
'dept': { key: 'dept_id', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') }, 'dept': { key: 'dept_id', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') },
'open_weekdays': { key: 'open_weekdays', transform: (value) => (Array.isArray(value) ? value.join(',') : value) }, 'open_weekdays': { key: 'open_weekdays', transform: (value) => (Array.isArray(value) ? value.join(',') : value) },
'recommends_rate': { key: 'recommends_rate', transform: (value) => (typeof value === 'string' ? value : value?.value || value?.key || '') }, // 'recommends_rate': { key: 'recommends_rate', transform: (value) => ((typeof value === 'string' || typeof value === 'number') ? value : value?.value || value?.key || '') },
// 'lgc_details': [ // 'lgc_details': [
// { // {
// key: 'lgc_details', // key: 'lgc_details',
@ -391,7 +400,7 @@ const formValuesMapper = (values) => {
key: 'lgc_details', key: 'lgc_details',
transform: (value) => { transform: (value) => {
const valueArr = Object.values(value) const valueArr = Object.values(value)
.filter((_v) => !isEmpty(_v)) .filter((_v) => !isEmpty(_v.lgc))
.map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' })); .map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' }));
return valueArr; return valueArr;
}, },
@ -400,7 +409,7 @@ const formValuesMapper = (values) => {
key: 'lgc_details_mapped', key: 'lgc_details_mapped',
transform: (value) => { transform: (value) => {
const valueArr = Object.values(value) const valueArr = Object.values(value)
.filter((_v) => !isEmpty(_v)) .filter((_v) => !isEmpty(_v.lgc))
.map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' })); .map((e) => ({ title: '', ...e, descriptions: e.descriptions || '' }));
return valueArr.reduce((r, c) => ({ ...r, [c.lgc]: c }), {}); return valueArr.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
}, },

@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next';
import { useDefaultLgc } from '@/i18n/LanguageSwitcher'; import { useDefaultLgc } from '@/i18n/LanguageSwitcher';
import { cloneDeep, isEmpty, isNotEmpty } from '@/utils/commons'; import { cloneDeep, isEmpty, isNotEmpty } from '@/utils/commons';
const ProductInfoLgc = ({ editable, formInstance, ...props }) => { const ProductInfoLgc = ({ editable, formInstance, pickEditedInfo, ...props }) => {
const { t } = useTranslation('products'); const { t } = useTranslation('products');
const { language: languageHT } = useDefaultLgc(); const { language: languageHT } = useDefaultLgc();
const HTLanguageSetsMapVal = useHTLanguageSetsMapVal(); const HTLanguageSetsMapVal = useHTLanguageSetsMapVal();
@ -15,6 +15,13 @@ const ProductInfoLgc = ({ editable, formInstance, ...props }) => {
const [activeKey, setActiveKey] = useState(); const [activeKey, setActiveKey] = useState();
const [items, setItems] = useState([]); const [items, setItems] = useState([]);
useEffect(() => {
formInstance.setFieldValue(['lgc_details_mapped', '2', 'title'], pickEditedInfo.product_title);
return () => {};
}, [pickEditedInfo.product_title]);
useEffect(() => { useEffect(() => {
const existsLgc = (editingProduct?.lgc_details || []).map((ele, li) => ({ const existsLgc = (editingProduct?.lgc_details || []).map((ele, li) => ({
...ele, ...ele,
@ -39,7 +46,7 @@ const ProductInfoLgc = ({ editable, formInstance, ...props }) => {
<Form.Item name={['lgc_details_mapped', `${ele.lgc}`, 'descriptions']} label={t('products:Description')} initialValue={ele.descriptions} tooltip={t('FormTooltip.Description')}> <Form.Item name={['lgc_details_mapped', `${ele.lgc}`, 'descriptions']} label={t('products:Description')} initialValue={ele.descriptions} tooltip={t('FormTooltip.Description')}>
<Input.TextArea <Input.TextArea
className={'!text-slate-600'} className={'!text-slate-600'}
rows={3} rows={3} maxLength={2000} showCount
allowClear allowClear
// onChange={(e) => handleChange('description', e.target.value)} // onChange={(e) => handleChange('description', e.target.value)}
// disabled={ignoreEditable ? false : (!isEmpty(ele.descriptions) || !editable)} // disabled={ignoreEditable ? false : (!isEmpty(ele.descriptions) || !editable)}
@ -91,7 +98,7 @@ const ProductInfoLgc = ({ editable, formInstance, ...props }) => {
<Input allowClear placeholder={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)} /> <Input allowClear placeholder={t(`FormTooltip.NewTitle.${editingProduct?.info?.product_type_id}`)} />
</Form.Item> </Form.Item>
<Form.Item name={['lgc_details_mapped', `${lgcItem.value}`, 'descriptions']} preserve={false} label={t('products:Description')} tooltip={t('FormTooltip.Description')}> <Form.Item name={['lgc_details_mapped', `${lgcItem.value}`, 'descriptions']} preserve={false} label={t('products:Description')} tooltip={t('FormTooltip.Description')}>
<Input.TextArea rows={3} allowClear /> <Input.TextArea rows={3} maxLength={2000} showCount allowClear />
</Form.Item> </Form.Item>
<Form.Item hidden name={['lgc_details_mapped', `${lgcItem.value}`, 'lgc']} preserve={false} initialValue={lgcItem.value}> <Form.Item hidden name={['lgc_details_mapped', `${lgcItem.value}`, 'lgc']} preserve={false} initialValue={lgcItem.value}>
<Input /> <Input />

@ -139,7 +139,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false) const [isQuotationModalOpen, setQuotationModalOpen] = useState(false)
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false) const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false)
const { modal, notification } = App.useApp(); const { modal, notification } = App.useApp()
const [quotationForm] = Form.useForm() const [quotationForm] = Form.useForm()
const [batchSetupForm] = Form.useForm() const [batchSetupForm] = Form.useForm()
@ -215,7 +215,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
{ {
title: t('products:group_size'), title: t('products:group_size'),
dataIndex: 'group_size', dataIndex: 'group_size',
width: '4rem', width: '6rem',
render: (_, record) => `${record.group_size_min}-${record.group_size_max}`, render: (_, record) => `${record.group_size_min}-${record.group_size_max}`,
}, },
@ -247,7 +247,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
return ( return (
<> <>
<h2>{t('products:EditComponents.Quotation')}</h2> <h2>{t('products:EditComponents.Quotation')}</h2>
<Table <Table size='small'
bordered bordered
dataSource={quotationList} dataSource={quotationList}
columns={quotationColumns} columns={quotationColumns}

@ -1,10 +1,13 @@
import { createContext, useEffect, useState } from 'react'; import { createContext, useEffect, useState } from 'react';
import { Tree, Input } from 'antd'; import { useLocation } from 'react-router-dom';
import { Tree, Input, Divider } from 'antd';
import { CaretDownOutlined } from '@ant-design/icons'; import { CaretDownOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import useProductsStore from '@/stores/Products/Index'; import useProductsStore from '@/stores/Products/Index';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets'; import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import { groupBy, sortBy } from '@/utils/commons'; import { groupBy, sortBy } from '@/utils/commons';
import NewProductModal from './NewProductModal';
import ContractRemarksModal from './ContractRemarksModal'
const flattenTreeFun = (tree) => { const flattenTreeFun = (tree) => {
let flatList = []; let flatList = [];
@ -40,7 +43,10 @@ const getParentKey = (key, tree) => {
const ProductsTree = ({ onNodeSelect, ...props }) => { const ProductsTree = ({ onNodeSelect, ...props }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const location = useLocation();
const isEditPage = location.pathname.includes('edit');
const [agencyProducts, editingProduct, setEditingProduct] = useProductsStore((state) => [state.agencyProducts, state.editingProduct, state.setEditingProduct]); const [agencyProducts, editingProduct, setEditingProduct] = useProductsStore((state) => [state.agencyProducts, state.editingProduct, state.setEditingProduct]);
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const productsTypes = useProductsTypes(); const productsTypes = useProductsTypes();
const [treeData, setTreeData] = useState([]); const [treeData, setTreeData] = useState([]);
@ -49,6 +55,11 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
const [expandedKeys, setExpandedKeys] = useState([]); const [expandedKeys, setExpandedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true); const [autoExpandParent, setAutoExpandParent] = useState(true);
useEffect(() => {
setExpandedKeys(productsTypes.map((item) => item.key)); //
return () => {}
}, [productsTypes, activeAgency]);
useEffect(() => { useEffect(() => {
// ; // ;
// const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || ''; // const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
@ -70,7 +81,7 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}); const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {});
return { return {
// title: product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || '', // title: product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || '',
title: `${product.info.city_name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || ''), title: `${product.info.city_name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || ''),
// key: `${ele.value}-${product.info.id}`, // key: `${ele.value}-${product.info.id}`,
key: product.info.id, key: product.info.id,
_raw: product, _raw: product,
@ -89,7 +100,7 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
setRawTreeData(_show); setRawTreeData(_show);
setFlattenTreeData(flattenTreeFun(_show)); setFlattenTreeData(flattenTreeFun(_show));
setExpandedKeys(productsTypes.map((item) => item.key)); // // setExpandedKeys(productsTypes.map((item) => item.key)); //
// setActiveKey(isEmpty(_show) ? [] : [_show[0].key]); // setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
return () => {}; return () => {};
@ -144,6 +155,12 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
return ( return (
<div className={`${props.className} relative`} style={props.style}> <div className={`${props.className} relative`} style={props.style}>
<Input.Search placeholder='Search' onChange={onSearch} allowClear className='sticky top-1 z-20 mb-3' /> <Input.Search placeholder='Search' onChange={onSearch} allowClear className='sticky top-1 z-20 mb-3' />
{/* 编辑 */}
{isEditPage && (
<NewProductModal />
)}
<Divider type='vertical' />
<ContractRemarksModal />
<Tree <Tree
blockNode blockNode
showLine defaultExpandAll expandAction={'doubleClick'} showLine defaultExpandAll expandAction={'doubleClick'}

@ -74,7 +74,7 @@ function Index() {
return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>; return <span className={stateCls}>{stateMapVal[`${r.audit_state_id}`]?.label}</span>;
}, },
}, },
{ title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by_name' }, { title: t('products:AuditedBy'), key: 'audited_by', dataIndex: 'audited_by' },
{ title: t('products:AuditDate'), key: 'audit_date', dataIndex: 'audit_date' }, { title: t('products:AuditDate'), key: 'audit_date', dataIndex: 'audit_date' },
{ {
title: '', title: '',

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -43,7 +43,7 @@ function Detail() {
]; ];
function detailTextRender(_, confirm) { function detailTextRender(_, confirm) {
const formattedText = confirm.PCI_ConfirmText;//.replace(/\;/g, '\n\n'); const formattedText = confirm.PCI_ConfirmText;
return ( return (
<div className='whitespace-pre-line'> <div className='whitespace-pre-line'>
{formattedText} {formattedText}
@ -76,6 +76,8 @@ function Detail() {
const [confirmText, setConfirmText] = useState(''); const [confirmText, setConfirmText] = useState('');
const [newConfirmText, setNewConfirmText] = useState(''); const [newConfirmText, setNewConfirmText] = useState('');
const [dataLoading, setDataLoading] = useState(false); const [dataLoading, setDataLoading] = useState(false);
const [reservationPreviewUrl, setReservationPreviewUrl] = useState('');
const [nameCardPreviewUrl, setNameCardPreviewUrl] = useState('');
const { notification } = App.useApp(); const { notification } = App.useApp();
const { reservationId } = useParams(); const { reservationId } = useParams();
@ -84,17 +86,14 @@ function Detail() {
const [getReservationDetail, reservationDetail, confirmationList, selectConfirmation, submitConfirmation] = const [getReservationDetail, reservationDetail, confirmationList, selectConfirmation, submitConfirmation] =
useReservationStore((state) => useReservationStore((state) =>
[state.getReservationDetail, state.reservationDetail, state.confirmationList, state.selectConfirmation, state.submitConfirmation]) [state.getReservationDetail, state.reservationDetail, state.confirmationList, state.selectConfirmation, state.submitConfirmation])
const randomString = new Date().getTime()
const officeWebViewerUrl = const officeWebViewerUrl =
'https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src='; 'https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src=';
// https://www.chinahighlights.com/public/reservationW220420009.doc // https://www.chinahighlights.com/public/reservationW220420009.doc
const reservationUrl = const reservationUrl =
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=1`; `https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=1&v=${randomString}`
const nameCardUrl = const nameCardUrl =
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=2`; `https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=2&v=${randomString}`
const reservationPreviewUrl = officeWebViewerUrl + encodeURIComponent(reservationUrl);
const nameCardPreviewUrl = officeWebViewerUrl + encodeURIComponent(nameCardUrl);
const showConfirmModal = (confirm) => { const showConfirmModal = (confirm) => {
setIsModalOpen(true); setIsModalOpen(true);
@ -118,6 +117,10 @@ function Detail() {
useEffect(() => { useEffect(() => {
setDataLoading(true); setDataLoading(true);
setReservationPreviewUrl(officeWebViewerUrl + encodeURIComponent(reservationUrl))
setNameCardPreviewUrl(officeWebViewerUrl + encodeURIComponent(nameCardUrl))
getReservationDetail(reservationId) getReservationDetail(reservationId)
.catch(ex => { .catch(ex => {
notification.error({ notification.error({

@ -0,0 +1,140 @@
import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, List, Table, Button, Typography } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined, AuditOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
import { DATE_FORMAT } from "@/config";
import { TableExportBtn } from "@/components/Data";
import trainTicketStore from "@/stores/Trainticket";
import { usingStorage } from "@/hooks/usingStorage";
const planListColumns = [
{
title: "团名",
key: "GRI_No",
dataIndex: "GRI_No",
// sorter: (a, b) => b.GRI_No - a.GRI_No,
render: (text, record) => (
<Typography.Text title={record.Memo}>
{record.GRI_No} {record.WL}
</Typography.Text>
),
},
{
title: "人数",
dataIndex: "PersonNum",
key: "PersonNum",
},
{
title: "出发",
key: "FromAirport",
dataIndex: "FromAirport",
},
{
title: "抵达",
key: "ToAirport",
dataIndex: "ToAirport",
},
{
title: "车次",
key: "FlightNo",
dataIndex: "FlightNo",
},
{
title: "出发日期",
key: "StartDate",
dataIndex: "StartDate",
sorter: (a, b) => {
const dateA = new Date(a.StartDate);
const dateB = new Date(b.StartDate);
return dateB.getTime() - dateA.getTime();
},
},
{
title: "开始时间",
key: "FlightStart",
dataIndex: "FlightStart",
render: text => formatColonTime(text),
},
{
title: "抵达时间",
key: "FlightEnd",
dataIndex: "FlightEnd",
render: text => formatColonTime(text),
},
{
title: "出票处理",
key: "TicketIssued",
dataIndex: "TicketIssued",
render: (text, record) => record.TicketIssuedName,
},
{
title: "计划状态",
key: "FlightStatus",
dataIndex: "FlightStatus",
render: (text, record) => record.FlightStatusName,
},
{
title: "操作",
key: "FlightInfo",
dataIndex: "FlightInfo",
render: (text, record) => <NavLink to={`/trainticket/plan/${record.COLI_SN}/${record.GRI_SN}`}>{"编辑"}</NavLink>,
},
];
const Trainticket = props => {
const navigate = useNavigate();
const { travelAgencyId } = usingStorage();
const [getPlanList, planList, loading] = trainTicketStore(state => [state.getPlanList, state.planList, state.loading]);
const showTotal = total => `合计 ${total} `;
useEffect(() => {
!planList && getPlanList(travelAgencyId, "", dayjs().startOf("M").format(DATE_FORMAT), dayjs().endOf("M").format(DATE_FORMAT), "-1", "-1");
}, []);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row>
<Col md={20} lg={20} xxl={20}>
<SearchForm
initialValue={{
dates: [dayjs().startOf("M"), dayjs().endOf("M")],
}}
fieldsConfig={{
shows: ["referenceNo", "dates", "airticket_state", "plan_state"],
fieldProps: {
referenceNo: { label: "搜索计划" },
dates: { label: "出发日期", col: 8 },
},
}}
onSubmit={(err, formVal, filedsVal) => {
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
}}
/>
</Col>
<Col md={4} lg={4} xxl={4}>
<Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/trainticket/invoice`)}>
报账
</Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/trainticket/invoicepaid`)}>
汇款记录
</Button>
</Space>
</Col>
</Row>
<Row gutter={16}>
<Col md={24} lg={24} xxl={24}>
<Table bordered={true} rowKey="id" columns={planListColumns} dataSource={planList} loading={loading} pagination={{ defaultPageSize: 20, showTotal: showTotal }} />
<TableExportBtn btnTxt="导出计划" label={`出票计划`} {...{ columns: planListColumns, dataSource: planList }} />
</Col>
<Col md={24} lg={24} xxl={24}></Col>
</Row>
</Space>
);
};
export default Trainticket;

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

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

@ -0,0 +1,706 @@
import { useState, useEffect } from "react";
import { Checkbox, Divider, DatePicker, Modal, Form, Input, Col, Row, Space, Collapse, Table, Button, Select, App, Popconfirm, Switch, Radio, List } from "antd";
import { PhoneOutlined, FrownTwoTone, LikeTwoTone, ArrowUpOutlined, ArrowDownOutlined, PlusOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@/utils/commons";
import { OFFICEWEBVIEWERURL } from "@/config";
import dayjs from "dayjs";
import trainTicketStore from "@/stores/Trainticket";
import { usingStorage } from "@/hooks/usingStorage";
import BackBtn from "@/components/BackBtn";
import { station_names } from "@/views/trainticket/station_name";
const TrainticketPlan = props => {
const { coli_sn, gri_sn } = useParams();
const { travelAgencyId, loginToken, userId } = usingStorage();
const [
getPlanDetail,
planDetail,
getGuestList,
guestList,
loading,
postFlightDetail,
postFlightCost,
deleteFlightCost,
getVeiPlanChange,
veiPlanChangeTxt,
postVeiFlightPlanConfirm,
ticketIssuedNotifications,
delete_flight_info,
seatTable,
] = trainTicketStore(state => [
state.getPlanDetail,
state.planDetail,
state.getGuestList,
state.guestList,
state.loading,
state.postFlightDetail,
state.postFlightCost,
state.deleteFlightCost,
state.getVeiPlanChange,
state.veiPlanChangeTxt,
state.postVeiFlightPlanConfirm,
state.ticketIssuedNotifications,
state.delete_flight_info,
state.seatTable,
]);
const reservationUrl = `https://p9axztuwd7x8a7.mycht.cn/Service_BaseInfoWeb/FlightPlanDocx?GRI_SN=${gri_sn}&VEI_SN=${travelAgencyId}&token=${loginToken}`;
const reservationPreviewUrl = OFFICEWEBVIEWERURL + encodeURIComponent(reservationUrl);
const [form] = Form.useForm();
const { notification } = App.useApp();
//
const guestList_select = () => {
return (
guestList &&
guestList.map(item => {
return { label: `${item.MEI_Name} , ${item.MEI_PassportNo}`, value: `${item.MEI_Name} , ${item.MEI_PassportNo} , ${item.MEI_Country} , ${item.MEI_Gender} , ${item.MEI_age} , ${item.MEI_Birthday}` };
})
);
};
const station_name_select = rawStr => {
const records = rawStr.split("@").filter(item => item); // 1. @
return records.map(record => {
// |8
const [
abbreviation, // bjb
name, //
code, // VAP
pinyin, // beijingbei
shortPinyin, // bjb
sequence, // 0
areaCode, // 0357
city, //
] = record.split("|");
return {
value: name + "-" + pinyin,
label: name,
};
});
};
const guestList_OnChange = e => {
ticket_form.setFieldsValue({ Memo: `${e.target.value}` });
};
//
const seatTableList_select = () => {
return (
seatTable &&
seatTable.map(item => {
return { label: `${item.name}`, value: `${item.name}` };
})
);
};
//
const costListColumns = [
{
title: "客人信息/备注",
key: "Memo",
dataIndex: "Memo",
},
{
title: "费用类型",
key: "CostType",
dataIndex: "CostType",
},
{
title: "车厢",
key: "Cabin",
dataIndex: "Cabin",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "座位号",
key: "SeatNo",
dataIndex: "SeatNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "座位类型",
key: "SeatClass",
dataIndex: "SeatClass",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "车票价格",
key: "Cost",
dataIndex: "Cost",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: "服务费",
key: "ServiceFee",
dataIndex: "ServiceFee",
},
{
title: "编辑",
key: "CLC_SN",
dataIndex: "CLC_SN",
render: (text, record) =>
record.CheckStatus <= 2 ? (
<Space>
<a onClick={() => showModal(record)}>编辑</a>
<Popconfirm title="删除" description="请确认是否删除?" onConfirm={() => handleDelete(record.CLC_SN)} okText="是" cancelText="否">
<Button danger type="link">
删除
</Button>
</Popconfirm>
</Space>
) : (
record.CheckStatusName
),
},
];
const Trainticket_form = props => {
const trainInfo = props.airInfo;
const [traininfo_form] = Form.useForm();
return (
<>
<Form
form={traininfo_form}
name={"ticket_form_" + trainInfo.id}
labelCol={{
span: 6,
}}
wrapperCol={{
span: 16,
}}
initialValues={{ ...trainInfo, StartDate: dayjs(trainInfo.StartDate) }}
onFinish={values => {
postFlightDetail(trainInfo.CLF_SN, trainInfo.GRI_SN, trainInfo.VEI_SN, trainInfo, values)
.then(() => {
notification.success({
message: `成功`,
description: "车票信息保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
}}
autoComplete="off">
<Divider orientation="left">火车信息</Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label="出发日期、车次、取票号" required>
<Space>
<Form.Item name="StartDate" noStyle rules={[{ required: true, message: "请输入出发日期!" }]}>
<DatePicker
placeholder="出发日期"
style={{
minWidth: 160,
}}
/>
</Form.Item>
<Form.Item name="FlightNo" noStyle rules={[{ required: true, message: "请输入车次!" }]}>
<Input placeholder="车次" />
</Form.Item>
<Form.Item name="TicketNo" noStyle rules={[{ required: true, message: "请输入取票号!" }]}>
<Input placeholder="取票号" maxLength={9} />
</Form.Item>
</Space>
</Form.Item>
<Form.Item label="出发站、抵达站" required>
<Space>
<Form.Item name="FromAirport" noStyle rules={[{ required: true, message: "请输入出发站!" }]}>
{/* <Input placeholder="出发" /> */}
<Select
placeholder="出发"
showSearch
style={{
minWidth: 160,
}}
options={station_name_select(station_names)}
/>
</Form.Item>
<Form.Item name="FlightStart" noStyle rules={[{ required: true, message: "请输入出发时间!" }]}>
<Input placeholder="出发时间" />
</Form.Item>
-
<Form.Item name="ToAirport" noStyle rules={[{ required: true, message: "请输入抵达站!" }]}>
{/* <Input placeholder="抵达" /> */}
<Select
placeholder="抵达"
showSearch
style={{
minWidth: 160,
}}
options={station_name_select(station_names)}
/>
</Form.Item>
<Form.Item name="FlightEnd" noStyle rules={[{ required: true, message: "请输入抵达时间!" }]}>
<Input placeholder="抵达时间" />
</Form.Item>
</Space>
</Form.Item>
<Form.Item name="ServiceType" hidden initialValue="2"></Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Space direction="vertical">
<Form.Item name="TicketIssued">
<Switch checkedChildren="已处理" unCheckedChildren="未处理" />
</Form.Item>
<Button type="primary" htmlType="submit">
1. 保存车票信息
</Button>
</Space>
</Col>
</Row>
<Divider orientation="left">出票信息</Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Table bordered={true} rowKey="CLC_SN" columns={costListColumns} dataSource={trainInfo.Flightcost_AsJOSN} loading={loading} pagination={false} />
</Col>
<Col md={24} lg={4} xxl={4}>
<Space direction="vertical">
<Button type="primary" onClick={() => showModal(trainInfo)}>
2. 添加出票信息
</Button>
</Space>
</Col>
</Row>
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label="上下站提醒信息" name="FlightMemo_messages">
<Input placeholder="没有提醒请留空,信息会抄送给上下站地接" />
</Form.Item>
<Form.Item label="已发提醒" name="FlightMemo">
<Input.TextArea rows={4} readOnly disabled />
</Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button
type="primary"
onClick={() => {
ticketIssuedNotifications(userId, trainInfo.CLF_SN, trainInfo.OPI_SN, traininfo_form.getFieldValue("FlightMemo_messages"))
.then(() => {
notification.success({
message: `成功`,
description: "提醒信息已发出!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
traininfo_form.setFieldValue("FlightMemo_messages", "");
})
.catch(() => {
notification.error({
message: `错误`,
description: "提醒失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
}}>
3. 通知顾问
</Button>
</Col>
</Row>
</Form>
</>
);
};
const detail_items = () => {
return planDetail
? planDetail.map(item => {
return {
key: item.id,
label: `${item.StartDate} ${item.FlightNo}(${item.FromAirport}${item.FlightStart}-${item.ToAirport}${item.FlightEnd})(${item.FlightCabin})`,
extra: (
<Popconfirm
title="请确认要删除车票记录"
description=""
onConfirm={() => {
delete_flight_info(item.CLF_SN); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" size="small" disabled={item.Flightcost_AsJOSN.length == 0 ? false : true}>
删除
</Button>
</Popconfirm>
),
children: <Trainticket_form airInfo={item} />,
};
})
: [];
};
// begin
const [isModalOpen, setIsModalOpen] = useState(false);
const [isModalOpen_confirmInfo, setisModalOpen_confirmInfo] = useState(false);
const [isTicketType, setisTicketType] = useState(true);
const [isAddNew, setisAddNew] = useState(true); //
const [ticket_form] = Form.useForm();
const [confirmInfo_form] = Form.useForm();
const showModal = ticket => {
setIsModalOpen(true);
ticket_form.resetFields();
if (isEmpty(ticket.CostType)) ticket.CostType = "出票";
ticket.CostType == "出票" ? setisTicketType(true) : setisTicketType(false); //
const isNew = isEmpty(ticket.CLC_SN); //
setisAddNew(isNew);
if (isNew) {
ticket.ServiceFee = "60"; // // 60
}
ticket_form.setFieldsValue(ticket);
if (isEmpty(ticket.Memo)) ticket_form.setFieldsValue({ Memo: "" });
};
const handleOk = (close_modal = true) => {
ticket_form
.validateFields()
.then(values => {
//
postFlightCost(values)
.then(() => {
notification.success({
message: `成功`,
description: "保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
getPlanDetail(travelAgencyId, gri_sn);
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
if (close_modal) setIsModalOpen(false);
})
.catch(info => {
console.log("Validate Failed:", info);
});
};
const handleCancel = () => {
ticket_form.resetFields();
setIsModalOpen(false);
};
const handleDelete = CLC_SN => {
deleteFlightCost(CLC_SN)
.then(() => {
notification.success({
message: `成功`,
description: "删除成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
getPlanDetail(travelAgencyId, gri_sn);
})
.catch(() => {
notification.error({
message: `错误`,
description: "删除失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
};
const onChangeType = value => {
if (value == "出票") {
setisTicketType(true);
} else {
setisTicketType(false);
}
};
//
const showModal_confirmInfo = ConfirmInfo => {
setisModalOpen_confirmInfo(true);
confirmInfo_form.setFieldsValue({ ConfirmInfo: ConfirmInfo });
};
const handleCancel_confirmInfo = () => {
setisModalOpen_confirmInfo(false);
confirmInfo_form.resetFields();
};
const handleOk_confirmInfo = () => {
confirmInfo_form
.validateFields()
.then(values => {
console.log("Received values of form: ", values.ConfirmInfo);
postVeiFlightPlanConfirm(travelAgencyId, gri_sn, userId, values.ConfirmInfo)
.then(() => {
notification.success({
message: `成功`,
description: "保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
getVeiPlanChange(travelAgencyId, gri_sn);
})
.catch(() => {
notification.error({
message: `错误`,
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
confirmInfo_form.resetFields();
setisModalOpen_confirmInfo(false);
})
.catch(info => {
console.log("Validate Failed:", info);
});
};
// end
useEffect(() => {
getPlanDetail(travelAgencyId, gri_sn); //
getGuestList(coli_sn); //
getVeiPlanChange(travelAgencyId, gri_sn); //
}, []);
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row>
<Col md={20} lg={20} xxl={20}></Col>
<Col md={4} lg={4} xxl={4}>
<BackBtn to={"/trainticket"} />
</Col>
</Row>
<Row>
<Col md={24} lg={24} xxl={24} style={{ height: "100%" }}>
<iframe id="msdoc-iframe-reservation" title="msdoc-iframe-reservation" src={reservationPreviewUrl + "&v=" + Math.random()} style={{ width: "100%", height: "600px" }}></iframe>
<Button type="link" target="_blank" href={reservationUrl}>
下载
</Button>
</Col>
</Row>
<Row>
<Divider orientation="center">{planDetail ? `${planDetail[0].GRI_No} - ${planDetail[0].WL}` : ""}</Divider>
<Col md={24} lg={24} xxl={24}>
<Collapse items={detail_items()} />
</Col>
<Col md={24} lg={24} xxl={24}>
<br />
<p style={{ textAlign: "right" }}>
<Popconfirm
title="请确认要增加车票记录"
description=""
onConfirm={() => {
postFlightDetail("", gri_sn, travelAgencyId, { FlightNo: "新的记录", FlightStatus: 1, ServiceType: 2 }, []); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText="是"
cancelText="否">
<Button type="dashed" icon={<PlusOutlined />}>
新增车票记录
</Button>
</Popconfirm>
</p>
</Col>
</Row>
<Row>
<Divider orientation="left">计划变更</Divider>
<Col md={24} lg={12} xxl={12}>
<Space direction="vertical" style={{ width: "90%" }}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ChangeText} />
<Button
type="primary"
onClick={() => {
showModal_confirmInfo(veiPlanChangeTxt && veiPlanChangeTxt.ChangeText);
}}
disabled={isEmpty(veiPlanChangeTxt) || isEmpty(veiPlanChangeTxt.ChangeText)}>
确认变更
</Button>
</Space>
</Col>
<Col md={24} lg={12} xxl={12}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ConfirmInfo} />
</Col>
</Row>
<Modal title="变更" open={isModalOpen_confirmInfo} onOk={handleOk_confirmInfo} onCancel={handleCancel_confirmInfo}>
<Form
form={confirmInfo_form}
labelCol={{
span: 5,
}}>
<Form.Item label="确认信息" name="ConfirmInfo" rules={[{ required: true }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title="费用信息"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
okText="保存"
cancelText="关闭"
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
{isAddNew ? (
<>
<Button type="primary" onClick={() => handleOk(false)}>
添加并继续新增
</Button>{" "}
<Button type="primary" onClick={() => handleOk(true)}>
添加并关闭
</Button>
</>
) : (
<OkBtn />
)}
</>
)}>
<Form
form={ticket_form}
labelCol={{
span: 5,
}}>
<Form.Item label="费用类型" name="CostType">
<Select
style={{
width: 160,
}}
onChange={onChangeType}
options={[
{
value: "出票",
label: "出票",
},
{
value: "改签",
label: "改签",
},
{
value: "取消",
label: "取消",
},
]}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="车厢" name="Cabin" rules={[{ required: true }]}>
<Select
style={{
width: 160,
}}
options={Array.from({ length: 16 }, (_, index) => ({
value: `${String(index + 1).padStart(2, "0")}`,
label: `${String(index + 1).padStart(2, "0")}`,
}))}
/>
</Form.Item>
<Form.Item label="座位号" name="SeatNo" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label="座位类型" name="SeatClass" rules={[{ required: true }]}>
<Select
style={{
width: 160,
}}
options={seatTableList_select()}
/>
</Form.Item>
<Form.Item label="车票价格" name="Cost" rules={[{ required: true }]}>
<Input
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
</>
)}
<Form.Item label="服务费" name="ServiceFee" rules={[{ required: true }]}>
<Input
prefix="¥"
style={{
width: 160,
}}
/>
</Form.Item>
{isTicketType && (
<>
<Form.Item label="选择客人" name="MEI_Name66">
<Radio.Group
onChange={e => guestList_OnChange(e)}
style={{
minWidth: 320,
}}>
<List
bordered
dataSource={guestList_select()}
renderItem={item => (
<List.Item>
<Radio value={item.value}>{item.label}</Radio>
</List.Item>
)}></List>
</Radio.Group>
{/* <Select onChange={value => guestList_OnChange(value)} options={guestList_select()} placeholder="如果列表里面没有客人信息,请手动录到备注里" /> */}
</Form.Item>
</>
)}
<Form.Item label="客人信息/备注" name="Memo">
<Input.TextArea rows={4} disabled={isTicketType} />
</Form.Item>
<Form.Item name="CLF_SN" hidden>
<input />
</Form.Item>
<Form.Item name="GRI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="VEI_SN" hidden>
<input />
</Form.Item>
<Form.Item name="CLC_SN" hidden>
<input />
</Form.Item>
</Form>
</Modal>
</Space>
);
};
export default TrainticketPlan;

File diff suppressed because one or more lines are too long
Loading…
Cancel
Save