Compare commits

...

585 Commits

Author SHA1 Message Date
Lei OT cf47dd44a8 Merge remote-tracking branch 'origin/main' 4 days ago
Lei OT 40ca0bdf8d perf: 滚动条 4 days ago
LiaoYijun 87d1e0d3ae fix: 解决预览 Doc 重复刷新 4 days ago
Lei OT 5694081269 perf: 产品管理: 编辑: 允许不选语种提交 4 days ago
LiaoYijun 2b01fc01ea 2.0.20 6 days ago
LiaoYijun 1e3e9b9942 Merge remote-tracking branch 'origin/火车出票模块' 6 days ago
Ycc 534dd60140 站点更新说明 6 days ago
Ycc 0a6a630441 添加站点英文名 6 days 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: 产品管理: 产品列表树: 名称显示 9 months ago
Ycc 171a5f94c4 Merge branch 'main' of github.com:hainatravel/GHHub 9 months ago
Ycc 59427a1a3a 添加导出功能,重复添加费用信息 9 months ago
Lei OT ef8046d1ae 2.0.1 9 months ago
Lei OT 024f066075 fix: 产品管理: 编辑页面: 无管理权限的头部参数 9 months ago
Lei OT f37e5302a1 Merge remote-tracking branch 'origin/main' 9 months ago
Lei OT c28d68960e fix: 产品管理: 国内供应商导航 9 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
Lei OT 577b033005 2.0.0-rc.1 10 months ago
Lei OT c4d0ac4e4e Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT f0f7dc3939 . 10 months ago
YCC 3839e2a212 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
YCC fd86659634 显示账单状态 10 months ago
Lei OT c4b7fd4dde 2.0.0-rc.0 10 months ago
Lei OT cb0f519adf feat: 导出产品合同 .docx 10 months ago
Lei OT 6ecb8908f9 test: 删除测试的ID列 10 months ago
Jimmy Liow 4e5f880b4c 2.0.0-beta.6 10 months ago
Jimmy Liow 3e5528eeb6 报价类型增加提示文字 10 months ago
YCC 418d16c138 账单提交和变更显示和确认 10 months ago
Lei OT 8fff3a2885 Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT 9a387067f9 test: 导出word 10 months ago
Jimmy Liow 59c0f5df4f 删除表单调试代码 10 months ago
Lei OT 2c674d5a03 Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT dcc20afd03 test: 导出word 10 months ago
Lei OT 07b9a7ddf5 conf: +导出Word的库 10 months ago
Jimmy Liow 0f518d04cf perf: 批量设置价格可以有多个有效期 10 months ago
Lei OT 7719b8df3b fix(产品管理): 绑定: 搜索所有产品: +年份, 必须 10 months ago
Lei OT d26a0a94bd Merge remote-tracking branch 'origin/main' 10 months ago
Lei OT 2762b5237d perf: 新增的产品, 也要红色提示 10 months ago
Lei OT 04287383dc perf: 修改提交审核的文字提示 10 months ago
YCC f34e2bc632 Merge branch 'main' of github.com:hainatravel/GHHub 10 months ago
YCC ecdba1ee09 账单页面,搜索功能 10 months ago
Jimmy Liow 663dbfcc50 2.0.0-beta.5 10 months ago
Jimmy Liow b285526dcb 增加内部测试144地址 10 months ago
Lei OT 03173157c7 style: 11 months ago
Lei OT 40053efbe8 fix: 修改价格, 重置状态 11 months ago
Jimmy Liow 5aec058060 2.0.0-beta.4 11 months ago
Lei OT e66431806b Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT d5777eaff1 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT be31765e44 fix: 产品管理: 头部, 审核发布/拒绝 11 months ago
Jimmy Liow bd5269f3f7 feat: 编辑账号搜索供应商增加防抖处理 11 months ago
YCC 31720a2930 Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
YCC 0c49bdcf6c 出票信息修改功能 11 months ago
Jimmy Liow 2f7184d837 fix: 取消测试删除价格失败 11 months ago
Jimmy Liow 910c6c02af fix: 删除价格导致产品只剩一项 11 months ago
Jimmy Liow 20bd547fd6 2.0.0-beta.3 11 months ago
Lei OT 6e9faa39f1 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT d65c12cb2c fix: 新增产品: 名称填充 11 months ago
Jimmy Liow f33dfa2415 fix: 新增价格取消默认周末设置 11 months ago
Jimmy Liow 6ecdb54938 解决新增价格后端删除错误 11 months ago
Lei OT b861cc66ae fix(产品管理): 编辑: 保存再次提交多语种重复 11 months ago
Lei OT eb4592acd0 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 04552f3f00 perf(产品管理): 编辑: 产品树+【城市】 11 months ago
Jimmy Liow 7b25c9962f feat: 重置密码使用大小写英文加数字,才能这国内供应商平台登录 11 months ago
Lei OT c101d6660c fix(产品管理): 编辑: 初始化空数据后新增 11 months ago
Jimmy Liow 722851aef6 feat: 增加账号、角色验证提示信息 11 months ago
Jimmy Liow 688c9caaee 2.0.0-beta.2 11 months ago
Lei OT a78f128c26 perf(产品管理): 整理多语种 11 months ago
Lei OT c8299b747b Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/views/products/Detail/ProductInfoQuotation.jsx
11 months ago
Lei OT 19fc024967 产品管理: 增加刷新 11 months ago
Lei OT be392588db perf(产品管理): form tooltip 11 months ago
Lei OT 96ae28e947 产品管理: 调整提交保存的字段 11 months ago
Jimmy Liow 058e8ef33a perf: 调整供应商名字宽度 11 months ago
Jimmy Liow d1977c180e perf: 删除不需要的多语言 11 months ago
Lei OT ea3c468feb Merge remote-tracking branch 'origin/main' 11 months ago
Jimmy Liow 12ab80403f perf: 增加价格校验文字 11 months ago
Lei OT 3fb6872538 test: 11 months ago
Jimmy Liow f315b22b90 perf:删除无效组件 11 months ago
Jimmy Liow fa9feea260 fix: 解决价格组件无效判断 11 months ago
Jimmy Liow 97013bf61f Merge branch 'main' of github.com:hainatravel/GHHub
# Conflicts:
#	src/views/products/Detail/PriceCompactInput.jsx
11 months ago
Lei OT acaf5a3de7 # 11 months ago
Lei OT 4829b0c12c 产品管理: 新增按钮 11 months ago
Lei OT 7267eb145f Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 31abdfefea fix: 语种参数初始化 11 months ago
Lei OT d83f39efe6 产品管理: 编辑权限 11 months ago
Jimmy Liow 301031884b feat: 价格币种增加:RMB,USD,THB,JPY 11 months ago
Lei OT 53407fa57a perf: 产品管理: form : 必填 11 months ago
Lei OT ef8cda5002 perf: 产品管理: form tooltip: new Product 11 months ago
Jimmy Liow 2aa7769709 feat: 分离表单价格录入组件 11 months ago
Lei OT d8cf4da101 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT fdddfe8db6 perf: 产品管理: 树组件操作 11 months ago
Jimmy Liow 307b23021d fix: 修复获取默认语言不严谨 11 months ago
Lei OT bdb3caf735 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 628a9ab14f perf: 调整报价表格位置 11 months ago
Lei OT 6b470fedb8 style: 11 months ago
Jimmy Liow c475d7c766 fix: 解决默认语言未加添到 request 参数 11 months ago
Jimmy Liow 247caad4f5 Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
Jimmy Liow 872f302ed4 feat: 登录时要选择中英文,方便业务数据查询。 11 months ago
Lei OT cb0c2a9e8e 产品管理: 提交报价数据; 产品名称 11 months ago
Jimmy Liow 4688c78dbd fix: 删除无效引用 11 months ago
Jimmy Liow eba209ea15 feat: 增加删除价格出错提示;给每个 Store 命名,方便调试 11 months ago
Jimmy Liow fe562c260f feat: 增加为测试环境构建的命令及配置 11 months ago
Jimmy Liow 0932742837 2.0.0-beta.1 11 months ago
Lei OT 0f6cf8e1bc Merge remote-tracking branch 'origin/main' 11 months ago
Jimmy Liow 8dbedbcf38 fix: 删除价格后,同时更新 State 数据 11 months ago
Lei OT 4bc7e74825 perf: commons 11 months ago
Lei OT 8f43258d99 style: 11 months ago
Jimmy Liow b1ddfc9bda fix: CNY -> RMB; 迁移批量设置价格代码 11 months ago
Jimmy Liow 41b32aaff4 fix: 解决 confirm 静态方法警告 11 months ago
Jimmy Liow 8970be0c10 fix: 解决表格没有 Key 的警告 11 months ago
Jimmy Liow 7fb85e308d fix: 删除表单调试 JSON;修复周末数据格式 11 months ago
Jimmy Liow 0f65540733 feat: 增加价格变化对象存储 11 months ago
Jimmy Liow 60be7b7457 feat: 完编辑价格保存 11 months ago
Jimmy Liow 9b0427bc00 Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
Jimmy Liow 65e534bf3e feat: 增加删除产品价格 11 months ago
Lei OT 7c85e61f78 todo: form tooltip 11 months ago
Lei OT 0601729c92 产品管理: 编辑: 报价: CNY-> RMB 11 months ago
Lei OT 63561e3804 产品管理: 复制: 文字说明 11 months ago
Jimmy Liow 2254a09c5f Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
Jimmy Liow a186142cc0 feat: 价格使用 Form 管理 11 months ago
Lei OT ceb3690530 产品管理: 编辑: 报价数组 11 months ago
Lei OT f308b994e3 perf: 产品管理: catch 11 months ago
Lei OT 605eae0db5 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 09cacf5f85 产品管理: 控制头部操作 11 months ago
Jimmy Liow b3faf7453e Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
Jimmy Liow e6af8c1e1e feat: 对比价格是否有修改 11 months ago
Lei OT 98329e9876 产品管理: header 11 months ago
Lei OT c03a8fd1fe Revert "产品管理: 编辑: 仅信息, 多语种"
This reverts commit aae952f5e3.
11 months ago
Lei OT aae952f5e3 产品管理: 编辑: 仅信息, 多语种 11 months ago
Lei OT 86d0d175d3 产品管理: 管理: 编辑title 11 months ago
Lei OT 3040ca6220 style: 审核: 变更: 红色 11 months ago
Lei OT 88b91387e4 # 附加项目 11 months ago
Lei OT 8e6db3bf85 复制产品后, 跳转 11 months ago
Lei OT 5b363c409f 开放时间 11 months ago
Lei OT 6dce885450 Merge remote-tracking branch 'origin/main' 11 months ago
Jimmy Liow 3ec112e88b feat:增加价格对比;增加是否可编辑价格判断 11 months ago
Lei OT 1cb54dbb75 编辑页面的各操作权限: 有 `htid` 则不允许修改信息; 提交审核方法 11 months ago
Lei OT 8dfb787db5 style: 多语种信息编辑 11 months ago
Lei OT f6b0dd8d04 多语种信息编辑 11 months ago
Lei OT b2dcec8c37 Merge remote-tracking branch 'origin/main' 11 months ago
Jimmy Liow 4ccb3cbdfd 2.0.0-beta.0 11 months ago
Jimmy Liow 59aa37cd99 feat: 准备客服测试系统 11 months ago
Lei OT 5e0013f0b4 产品类型: 控制编辑字段 11 months ago
Lei OT 2004aafa6d 多语种信息编辑 11 months ago
Jimmy Liow 16a40e6280 Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
Jimmy Liow 19dfc575b4 fix:批量设置人等取值不对
del:不用的组件
11 months ago
Lei OT e7a83df8a2 跳转链接控制: 权限, 页面 11 months ago
Lei OT c2e4b027a1 保存, 价格 11 months ago
Lei OT 4fbcc93134 可编辑状态 11 months ago
Lei OT 2692579437 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 0647d845ba 产品管理: 审核页面调整编辑, 定位; 11 months ago
Jimmy Liow b8b679a0ec 批量设置价格后马上预览 11 months ago
Lei OT 98937960e5 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 80a54193a5 产品管理: 审核页面调整编辑, 定位 11 months ago
Lei OT b9a2b0e6ac 产品编辑页面: 保存后更新树 11 months ago
Jimmy Liow 7f9025db5c 开发报价新增表单 11 months ago
Lei OT 2caddfd092 供应商权限: 产品管理导航 11 months ago
Lei OT 09d9cf5ec6 产品编辑页面: 保存 11 months ago
Jimmy Liow e0784eb593 合并价格表单 11 months ago
Jimmy Liow 5f4f525f45 Merge branch 'main' of github.com:hainatravel/GHHub
# Conflicts:
#	src/views/products/Detail/ProductInfoForm.jsx
11 months ago
Jimmy Liow b6e57cd4ba 开发报价新增表单 11 months ago
Lei OT 0d9a80cefd 产品编辑页面: 组件 新增 11 months ago
Lei OT ab527e36c2 产品编辑页面: 组件 editable 11 months ago
Lei OT a63887a7b6 产品编辑页面: 组件 info, form, lgc 11 months ago
Lei OT 2a193f9955 todo: 产品编辑页面: 组件 11 months ago
Lei OT 26d354e0ea Merge remote-tracking branch 'origin/main' 11 months ago
Jimmy Liow 2e68c7621d 价格设置、人等增加默认值,使用颜色表示必填 11 months ago
Lei OT 6f761a233e Merge remote-tracking branch 'origin/main'
# Conflicts:
#	src/views/products/Detail.jsx
11 months ago
Lei OT 1dfc2d23d7 todo: 产品编辑页面: 组件 11 months ago
Jimmy Liow 2b9a365c45 完成批量价格生成和预览 11 months ago
Jimmy Liow cae1a8774a 完成批量设置价格界面 11 months ago
Jimmy Liow f68370fa72 批量添加价格可以同时设置人等 11 months ago
YCC 606c54a4f9 Merge branch 'main' of github.com:hainatravel/GHHub 11 months ago
YCC 655114ab44 机票信息编辑 11 months ago
Jimmy Liow 7b0a48360b 2.0.0-alpha.3 11 months ago
Lei OT ab50a891f2 style: 头部账户信息 11 months ago
黄文强@HWQ-PC 01e4dbbf4f 增加Header,把提交审核按钮和增加产品按钮移到Header 11 months ago
Lei OT af71ebf18e Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 71147a728d 产品管理: 审核页面: 调整Header组件 11 months ago
Jimmy Liow e6bc9b2dc2 fix: ESLint warning 11 months ago
黄文强@HWQ-PC 58fc202f69 1.增加年份选择功能
2.增加地接需要审核结果页面,查看哪些产品和价格是否通过功能
3.地接可以在审核界面跳过去修改产品价格功能
11 months ago
Lei OT 3a592a640d 审核页: 数据更新 11 months ago
Lei OT 09091eaff5 Merge remote-tracking branch 'origin/main' 11 months ago
Lei OT 698a8b256c docs: 11 months ago
Jimmy Liow 3544419d11 feat: SearchForm 增加时间 onMounted,解决 View 获取初始值加载数据 11 months ago
Lei OT d9c45f9962 feat: 产品管理: 审核: 切换状态 12 months ago
黄文强@HWQ-PC 83275f2d25 Merge branch 'main' of github.com:hainatravel/GHHub 12 months ago
黄文强@HWQ-PC 9cb6d187c0 1.增加代理供应商录入产品描述、价格功能
2.增加供应商提交产品审核功能
12 months ago
Lei OT 05bbfaf49f Merge remote-tracking branch 'origin/main' 12 months ago
Jimmy Liow f1defd23e6 doc: 注释 dev 模式热更新出错原因 12 months ago
Lei OT fe868543ff 产品管理: 客服首页不限制审核跳转 12 months ago
Jimmy Liow 517d54614c 2.0.0-alpha.2 12 months ago
Jimmy Liow 8a37c6f3f2 更新版本预发布例子 12 months ago
Jimmy Liow ab01680747 更新价格状态迁移流程 12 months ago
黄文强@HWQ-PC 944c607c77 Merge branch 'main' of github.com:hainatravel/GHHub 12 months ago
黄文强@HWQ-PC 0db9a5429a 1.增加供应商新增产品
2.供应商保存产品
12 months ago
Lei OT 2671015b43 feat: 产品管理: 复制产品: 增加是否复制价格; 所有项目类型 12 months ago
Lei OT 6cf4f81cdf feat: 产品管理: 审核: 切换年份; 切换供应商; 12 months ago
Lei OT bc6b5cbed1 feat: 供应商选择; perf: 产品管理二级页面头部 12 months ago
Lei OT 0a391acdd0 Merge remote-tracking branch 'origin/main' 12 months ago
Lei OT 31d5db8387 产品管理: 客服首页, 审核状态 12 months ago
Jimmy Liow 96c0e9f516 2.0.0-alpha.1 12 months ago
Jimmy Liow 2e49c01dac fix: 账号、角色保存出错不关闭表单 12 months ago
YCC d97638b843 Merge branch 'main' of github.com:hainatravel/GHHub 12 months ago
Lei OT 7a411ebfa9 test: 绑定产品 12 months ago
Lei OT 5b9f8eeb7f test: 绑定产品 12 months ago
Lei OT dc87c7bdb6 绑定产品: 完毕之后更新 12 months ago
Lei OT 19ba6f3f2c Merge remote-tracking branch 'origin/main' 12 months ago
Lei OT 93572e6594 feat: 城市选择器 12 months ago
Jimmy Liow 2d9e577859 feat:账号管理界面增加中英文 12 months ago
Jimmy Liow d7118b0791 fix: 删除 Mobx 代码 12 months ago
Jimmy Liow cce861bd52 feat: 调整导航宽度,适应小屏幕 12 months ago
黄文强@HWQ-PC b22e9d2edd 修改报价报价的编辑界面 12 months ago
Lei OT b5953c5c12 style: 顶部: 账户名, 太长导致换行, 样式撑开了 12 months ago
Lei OT 9491ada91a 审核价格: 单行审核: 取消每次从后端更新数据, 仅前端响应 12 months ago
Lei OT c6739e5279 perf: 公共函数: 深拷贝. 解决内存泄漏 12 months ago
Lei OT e1ec006772 审核状态名称和后端统一 12 months ago
Lei OT 6ae1984b91 feat: 复制产品价格 12 months ago
黄文强@HWQ-PC 5d78c9ee72 Merge branch 'main' of github.com:hainatravel/GHHub 12 months ago
黄文强@HWQ-PC d1ca39eccc 修改报价报价的编辑界面 12 months ago
Jimmy Liow aac7effc0c feat:账号管理界面增加中英文 12 months ago
Jimmy Liow c6a868503c fix: 删除 Mobx 代码 12 months ago
Jimmy Liow e31c4903c4 feat: 调整导航宽度,适应小屏幕 12 months ago
Lei OT 472273396c 表单的返回值处理 12 months ago
Lei OT 2f2c86026a feat: 绑定项目: 搜索已发布的产品 12 months ago
Lei OT ce921312af 表单的返回值处理 12 months ago
Lei OT cbfb2749d0 Merge remote-tracking branch 'origin/main' 12 months ago
Jimmy Liow db2ee69edf Merge branch 'main' of github.com:hainatravel/GHHub 12 months ago
Jimmy Liow a19d787fab feat: 账号删除 realname 搜索 12 months ago
黄文强@HWQ-PC a0f81d26aa Merge branch 'main' of github.com:hainatravel/GHHub 12 months ago
黄文强@HWQ-PC 852c784f9a 1.增加新增产品功能
2.修改设置全年日期功能
12 months ago
Jimmy Liow 395e2e44db feat: 团计划增加【未确认】搜索 12 months ago
Jimmy Liow 2a20ed0f14 feat: 导航增加产品管理权限 12 months ago
Jimmy Liow b7d7c86319 增加根据供应商搜索账号 12 months ago
Lei OT 05a4106fd8 conf: 审核页面权限 12 months ago
Lei OT 5baeb5162f style: primary, danger, muted 12 months ago
Lei OT 67a21477c6 绑定项目只显示名称 12 months ago
Lei OT 4350f37a6a 超公里不需要公里数字段 12 months ago
Jimmy Liow 70f5332839 feat: 登陆界面增加版本号;调整界面布局 1 year ago
黄文强@HWQ-PC f77bcf4288 修改批量录入界面功能 1 year ago
黄文强@HWQ-PC f13241f262 修改批量录入界面功能 1 year ago
Lei OT 06db76ce86 合并 1 year ago
Lei OT 1a958639d5 当前编辑的产品 1 year ago
Lei OT efa3f052a6 当前编辑的产品; 价格表i18n; 1 year ago
Lei OT efc12c25a9 Merge remote-tracking branch 'origin/main' 1 year ago
Lei OT 4fb6273ed1 # 1 year ago
Jimmy Liow eca38ae611 2.0.0-alpha.0 1 year ago
Jimmy Liow f11edab9cb Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
Jimmy Liow 4bdf085a35 feat: 开发模式不加载 PageSpy 1 year ago
黄文强@HWQ-PC 8bfb9defdc Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
黄文强@HWQ-PC 71c24be596 1.怎加批量录入价格功能 1 year ago
Jimmy Liow 7948d16613 2.0.0-0 1 year ago
Jimmy Liow dfd7d2a749 feat: 更新版本号及设置命令 1 year ago
Jimmy Liow 9008ae8511 fix:登录超时后没有用户名 1 year ago
Jimmy Liow 3f4fcc3789 feat: 把 style 转为 Tailwind CSS 1 year ago
Jimmy Liow 81397e7a68 fix: 解决常量未导入 1 year ago
Jimmy Liow 4940a54e13 feat: 价格编辑增加路由限制 1 year ago
Jimmy Liow e3038e0a9d fix:解决 Forget to pass form prop 1 year ago
Jimmy Liow 7801719522 feat: 路由403增加账号和权限提示 1 year ago
Jimmy Liow 551b082168 fix: 解决产品管理路由错误 1 year ago
Jimmy Liow 3bbd694dc6 Merge remote-tracking branch 'remotes/origin/feature/price_manager'
# Conflicts:
#	src/assets/global.css
#	src/main.jsx
#	src/views/App.jsx
1 year ago
Jimmy Liow ae5548a4d0 fix: 修复方法名错误 1 year ago
Jimmy Liow c6e9857814 feat: 同步加载权限,避免刷新后路由没有权限数据 1 year ago
Jimmy Liow e68e0ddd7d feat: 团计划使用 Tailwind CSS 1 year ago
Jimmy Liow 52fcb20dbf feat: App Layout 统一使用 Tailwind CSS 1 year ago
Jimmy Liow 8b44f5ce5c feat: 迁移类样式到 Tailwind CSS 1 year ago
Jimmy Liow 1b1db0937d 增加标签和 PUSH 命令 1 year ago
黄文强@HWQ-PC 11b5a80986 增加批量添加报价界面 1 year ago
Jimmy Liow 3b25a9dc7b feat: 更新供应商默认首页 1 year ago
Jimmy Liow 653533e80c feat: 集成 PageSpy;统一用户信息读取 1 year ago
Jimmy Liow 0eef2cee3a feat: 增加打标签命令;删除不必要角色权限;优化 main asnyc 确保可以 build 1 year ago
黄文强@HWQ-PC e3727db78f Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager 1 year ago
黄文强@HWQ-PC ed651ff3d4 1.增加搜索框
2.动态显示产品项目信息
3.修改变量名
1 year ago
Lei OT 6caa17ea4c feat: 复制供应商产品: +小组, 选择类型 1 year ago
黄文强@HWQ-PC 8fd197a3e0 Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager 1 year ago
Lei OT 27aeb82fb5 Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager 1 year ago
Lei OT 42cb1d583f style: products Audit 1 year ago
Jimmy Liow e9026eb103 Merge tag 'feat-RBAC-0.9.1' into feature/price_manager 1 year ago
黄文强@HWQ-PC acea97d3e5 Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager 1 year ago
黄文强@HWQ-PC 90f84fa874 优化显示界面,多语种逻辑 1 year ago
Jimmy Liow 579689f3e0 feat: 删除 GA JS;菜单显示用户姓名;用户信息增加姓名和角色;Auth 增加初始指,统一登陆和超时接口 1 year ago
Lei OT d01684b1c2 Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager 1 year ago
Lei OT 80c1b98671 产品管理: 附加项目 添加/删除接口 1 year ago
Jimmy Liow 2adeb31e73 Merge tag 'feat-RBAC-0.9' into feature/price_manager
# Conflicts:
#	src/main.jsx
#	src/stores/Auth.js
1 year ago
Jimmy Liow 9304c4735d feat: 新增账号使用空对象;重命名 lifecycleware 1 year ago
YCC e5407d6fa0 fix 1 year ago
黄文强@HWQ-PC 40e8d97aa8 Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager 1 year ago
黄文强@HWQ-PC 64f690801e 1.优化界面显示
2.对接供应商编辑接口
1 year ago
YCC cbf99e2718 改为modal编辑表单 1 year ago
Jimmy Liow ce60237b3d feat: 统一登陆和刷新页面逻辑代码 1 year ago
Lei OT 07b196fc66 test: 调整antd间距 1 year ago
Lei OT 2ad323edbb 客服审核价格: 超公里+公里数列 1 year ago
Jimmy Liow e3a28ebf25 权限增加不同角色不同默认首页 1 year ago
Lei OT ff6071cd2f wu_id 改 lmi_sn 1 year ago
Lei OT cc2bf23bf5 feat: 产品管理: `餐` 类型使用`R` ; 优化审核页面价格表; 附加项目表显示类型; 超公里Ultra Service 1 year ago
Jimmy Liow 09726a51bf feat: 增加账号状态和启用和禁用功能 1 year ago
Lei OT f440b08fad Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager 1 year ago
Lei OT 1534ce6979 debug: 搜索可以绑定的附加项目; 价格value改为adult_cost; 1 year ago
Jimmy Liow 16a21aae43 Merge tag 'feat-RBAC-0.8.1' into feature/price_manager
# Conflicts:
#	src/main.jsx
1 year ago
Jimmy Liow 81f1c9cea9 fix: wu_id -> lmi_sn 1 year ago
Lei OT 4b77aa689e 产品缺少英文名称时, 使用语种表中文名称 1 year ago
Jimmy Liow d8b141cda8 feat: 使用通知作为默认首页;删除 GA 数据收集; 1 year ago
Jimmy Liow fcef4c486b feat: 账号角色管理增加中文;统一 userId; 1 year ago
Lei OT bebb8c0a09 feat: 从页面语言获取默认的HT语种编码 1 year ago
Lei OT 461263839b 产品管理: 客服首页: 核对搜索结果字段 1 year ago
Lei OT 545bee21cb Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager 1 year ago
Lei OT d0a4453ce6 产品信息属性的 i8n 1 year ago
Jimmy Liow 6f446d852a feat: 登陆超时刷新要重新登陆 1 year ago
Lei OT 1084db4b96 搜索表单自定义排序 1 year ago
Jimmy Liow a3d02f4caf 统一应用初始化 1 year ago
Jimmy Liow 51aa59abe1 Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager 1 year ago
Jimmy Liow a5b82e245b Merge tag 'feat-RBAC-0.8' into feature/price_manager
# Conflicts:
#	src/main.jsx
#	src/views/App.jsx
1 year ago
黄文强@HWQ-PC c843fab969 1.完善表格显示内容
2.修改供应商报价表格的对应字段
1 year ago
Jimmy Liow 37c3b5aa45 使用Lifecycle回调权限初始化 1 year ago
黄文强@HWQ-PC 51834fbc97 1.完善界面显示效果
2.修改表单对应的接口字段
1 year ago
Lei OT cbdb737163 feat: 客服首页: 搜索: 供应商多选 1 year ago
Lei OT 65064937f0 feat: 复制供应商产品 1 year ago
Lei OT e4cc07eefe feat: 附加项目: 搜索供应商的产品, 已审核发布的 1 year ago
Lei OT 056a006847 增加审核页面跳转`编辑`; style: 1 year ago
Lei OT 2bdf22c140 feat: 产品: 附加项目: 新增; 删除; 读取 1 year ago
Jimmy Liow f2177a9e8e fix: 修复供应商ID错误
feat: 底部增加公司名字和版本
1 year ago
Jimmy Liow 752bd729eb fix: 修复初始化错误 1 year ago
Jimmy Liow 702d7876c0 feat: 初始化账号权限、系统开始判断权限 1 year ago
Lei OT 1b97cb41dd 解决冲突和补充 1 year ago
Lei OT ad10ef9313 Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager
# Conflicts:
#	public/locales/en/products.json
#	src/main.jsx
1 year ago
Lei OT 3c533c96b4 style: 1 year ago
黄文强@HWQ-PC 040fdb9d3a Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager
# Conflicts:
#	public/locales/en/products.json
#	public/locales/zh/products.json
#	src/main.jsx
1 year ago
黄文强@HWQ-PC 261849fa5f 1.修改供应商报价页面字段
2.增加日期组件到components
3.国际化
1 year ago
Lei OT 7409ec1c16 获取指定产品的附加项目 1 year ago
Lei OT 079b74c3b4 产品管理权限 1 year ago
Lei OT b0d4943f09 有效期; 有效的周X; style: 1 year ago
Jimmy Liow efad9d8697 feat: 完成新增/编辑账号选择供应商 1 year ago
Jimmy Liow 4afb1d4371 增加权限类别名称,初始化数据 SQL 1 year ago
Lei OT 697fa00be3 产品管理的权限 1 year ago
Jimmy Liow e9289bbe80 导航和菜单增加权限判断 1 year ago
Jimmy Liow 3ec9a43833 完成权限新增、编辑、分配角色权限 1 year ago
Lei OT 86242addab 删除 Mobx 相关的引入 1 year ago
Lei OT 219372c06e 新增HT产品类型: J 车费, Q 导游 1 year ago
Lei OT 240697a42d Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager 1 year ago
Jimmy Liow 5c236defd1 Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager
# Conflicts:
#	src/views/App.jsx
1 year ago
Lei OT ce345c7347 HT语种; 当前语种 1 year ago
Jimmy Liow 4987b7bcd8 合并权限管理v0.6
# Conflicts:
#	src/components/SearchForm.jsx
#	src/main.jsx
#	src/views/App.jsx
1 year ago
Jimmy Liow c4145a13df feat: 更新价格编辑界面;完成编辑角色;新增角色;重置账号密码 1 year ago
Lei OT bf1d668301 Merge remote-tracking branch 'origin/feature/price_manager' into feature/price_manager
# Conflicts:
#	src/main.jsx
1 year ago
Lei OT cacc4832c5 feat: 产品管理: 搜索: 多选 1 year ago
黄文强@HWQ-PC 1194cc84a0 Merge branch 'feature/price_manager' of github.com:hainatravel/GHHub into feature/price_manager
# Conflicts:
#	src/main.jsx
1 year ago
Lei OT e9b7aec521 feat: 产品管理: 搜索+年份,状态; 产品类型排序; 审核页: 成人,儿童价; 语种列表 1 year ago
黄文强@HWQ-PC 7b2c836e91 增加供应商价格管理页面 1 year ago
Lei OT a61ef8882b 增加默认参数: wu_id 1 year ago
Lei OT a7c87170a0 feat: 产品管理: 客服首页, 审核产品: 核对接口字段 1 year ago
YCC 2a8dcabdec Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
YCC e3dc13bc6c 机票计划显示 1 year ago
Lei OT f8ffaf899d feat: 产品管理: 客服审核产品, 价格 1 year ago
Jimmy Liow 32b097c5b7 feat: 新增角色管理界面 1 year ago
Lei OT d24c34d4ac feat: 产品管理: 客服首页 1 year ago
Lei OT a579ee4ee5 默认参数: lgc 1 year ago
Lei OT 232a8f85c8 审核产品页面: 表格, 数据结构 1 year ago
Jimmy Liow 2cb059cc06 feat: postForm 增加业务错误校验,搜索帐号,新增、编辑账号 1 year ago
Lei OT f323f0b511 审核状态 1 year ago
Jimmy Liow f22c9eb497 bug: 修复 ThemeContext 导入错误 1 year ago
Jimmy Liow 74a68d705c feat: 增加 ThemeContext 1 year ago
Jimmy Liow c352581f98 feat: Mobx-Zustand迁移完成,删除依赖和相关代码 1 year ago
Lei OT fa0550dfdb 客服审核页面 1 year ago
Jimmy Liow 5d4b5cc8f1 Merge branch 'main' of github.com:hainatravel/GHHub
# Conflicts:
#	src/main.jsx
1 year ago
Jimmy Liow bfc994cd12 feat: 更新价格界面原型;所有海外供应商路由加上权证验证 1 year ago
YCC aa501c7f3a Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
YCC a7f3faac13 机票计划详细页 1 year ago
Lei OT 1142a1a893 供应商搜索产品页面 1 year ago
Jimmy Liow 849e6ceef0 feat: 增加权限控制组件方式 1 year ago
Jimmy Liow 043c02f8a8 Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
Jimmy Liow 749084f0aa feat: 增加权限路由组件,还有账号管理常量 1 year ago
Lei OT ca0edbd63a refactor: 从hooks/usingStorage 获取登录信息 1 year ago
Lei OT 30012ae37e feat: invoice 迁移zustand 1 year ago
YCC b62bba8587 Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
YCC e2766501ca 添加机票计划搜索和列表显示 1 year ago
Jimmy Liow bff5197765 feat:完成团计划全部迁移 zustand;
增加分页中英文;
1 year ago
Jimmy Liow 5913970154 feat:合并编辑产品界面原型
增加账号权限管理导航
1 year ago
Lei OT 61076ed908 Merge remote-tracking branch 'origin/main' 1 year ago
Lei OT 97b19cf951 docs: 编辑-产品项目 1 year ago
Jimmy Liow 680364eae3 feat:增加权限表设计,增加禁用、重置密码操作 1 year ago
Jimmy Liow 0c91e88674 feat: 更新权限选择样式;删除 Table Column Key 1 year ago
Jimmy Liow 1e439621a5 feat: 初步完成账号权限管理界面 1 year ago
Jimmy Liow 8e07c82614 fix: 修改检查 TOKEN 频率 1 year ago
Jimmy Liow 5412b872a0 Merge branch 'main' of github.com:hainatravel/GHHub 1 year ago
Jimmy Liow 3465c7f575 feat: Profile 更换 zustand 1 year ago
Lei OT af2ce43b2a Merge remote-tracking branch 'origin/main' 1 year ago
Jimmy Liow 8d3649da9d feat: 国际化修改密码界面 1 year ago
Jimmy Liow 63a32f5412 feat: AuthStore 完成所有方法转换成 zustand 1 year ago
Lei OT 9522fece90 fix: SearchForm style 1 year ago
Jimmy Liow 600ba44d8d feat:
增加 ErrorBoundary
useStorage->usingStorage
1 year ago
Jimmy Liow 322646a9dc feat:WebStorage 增加 JSON 格式 1 year ago
Jimmy Liow e60be41b98 feat:增加会话超时使用的 username 1 year ago
Jimmy Liow 428e8f4aa4 bug:代码提交错误 1 year ago
Jimmy Liow a1890df323 fix:解决刷新页面没有 Token 1 year ago
Lei OT 9fef49263b perf: 搜索组件: 修改属性名: fieldsConfig 1 year ago
Lei OT 28e2b9f6a6 conf: 调整eslint, 允许数据层使用hooks 1 year ago
Jimmy Liow e31a20e92d feat: 完成 useStorage 1 year ago
Jimmy Liow 3235e7e574 reservation.Detail 错误 1 year ago
Jimmy Liow 9071e87bc7 Merge branch 'main' of github.com:hainatravel/GHHub
# Conflicts:
#	src/views/reservation/Detail.jsx
1 year ago
Jimmy Liow 83d08dd099 完成 useStorage 1 year ago
Lei OT b682fcb9cb # 1 year ago
Lei OT 96c8017271 refactor: Component/BackBtn 1 year ago
Lei OT 733c1c0fec refactor: Invoice/index, Detail, 读取authstore; 使用搜索组件 1 year ago
Lei OT 2c8da99712 refactor: Invoice/Paid, PaidDetail, 读取authstore; 使用搜索组件 1 year ago
Lei OT 30a8301329 refactor: Invoice/index, Detail, 读取authstore; 使用搜索组件 1 year ago
Lei OT 6b2d3d2b04 i18n: + vendor.json 1 year ago
Lei OT 877a020dc0 perf: report loading 1 year ago
Lei OT 5ef8c9a72f 搜索: invoice status 1 year ago
Lei OT d8bc4d2ff4 perf(i18n): 检测使用设备语言 1 year ago
Lei OT b5c13f238d perf: 搜索框, 自定义配置 1 year ago
Lei OT bf8c45033b refactor: Notice 读取userId 1 year ago
Lei OT 5b30aec3e5 refactor: Report 1 year ago
Lei OT 6e66a31bb0 refactor: Feedback 状态管理 1 year ago
Lei OT 844ad99bbd refactor: Feedback Detail 1 year ago
Lei OT a608c457b8 Merge remote-tracking branch 'origin/main' 1 year ago
Lei OT 6ad342cb2e refactor: Feedback CustomerDetail 1 year ago
Jimmy Liow bc18a05254 feat:完成分页调用新接口 1 year ago
Lei OT c7f29f4999 Merge remote-tracking branch 'origin/main' 1 year ago
Lei OT 65eaf11d42 refactor: Feedback index 1 year ago
Jimmy Liow bf50e29ed7 使用 SearchForm 搜索计划 1 year ago
Lei OT 66c7ba9574 搜索框: 不删除空字段 1 year ago
Lei OT b568379f41 搜索框时间增加转换格式: YYYY-MM-DD 23:59 1 year ago
Lei OT 887b47a55a feat: 搜索框 1 year ago
Jimmy Liow d443f21805 完成团计划搜索使用 Zustand 1 year ago
Jimmy Liow 8d3a7354c7 初步完成使用 Zustand 管理登陆 1 year ago
Jimmy Liow 4fff1229fd Merge branch 'main' of github.com:hainatravel/GHHub
# Conflicts:
#	src/stores/Root.js
#	src/views/Login.jsx
1 year ago
Jimmy Liow abf527a53d 登陆验证使用 Zustand 1 year ago
Lei OT 3a6ce1d366 # 1 year ago
Lei OT ceb397a895 fix: antd 更新install之后 cssinjs 的hash值更新; 1 year ago
Lei OT 0be4bc9504 fix: antd 更新install之后 cssinjs 的hash值更新; 1 year ago
Lei OT ea0bec8579 refactor: Notice 的Mobx store 迁移到 zustand; Detail 1 year ago
Lei OT 045eec2566 request 处理errcode 1 year ago
Lei OT 41a9c2a590 Merge remote-tracking branch 'origin/main' 1 year ago
Lei OT c06df03754 refactor: Notice 的Mobx store 迁移到 zustand; 仅保留管理未读数量 1 year ago
Lei OT bff8a1ea78 request 处理errcode 1 year ago
Lei OT 49f8cafade Merge branch 'refactor/store-zustand' 1 year ago
Jimmy Liow 384be3171b 更新 React 版本,开发默认端口 1 year ago
Jimmy Liow 7f3ed0b42c 增加 zustand 框架,删除切换供应商相关页面 1 year ago
Jimmy Liow 696f2bb816 删除供应商切换功能,使用 PageSpy 替换 1 year ago
Lei OT 64f32d9093 zustand Notice 1 year ago
YCC 03ac61fad0 地接语种 1 year ago
YCC ae0324f8cf Merge branch 'feat-public_report' 1 year ago
Lei OT df915c6114 fix: fetchJSON 1 year ago
Lei OT 22aa244f9e docs: 1 year ago
Lei OT d3b53b766b fix: fetchJSON 去除最后一个结尾的& 1 year ago
Lei OT 2aa20577d5 datePicker 适应语言变化 1 year ago
Lei OT 75fa47cfe0 conf: vite 的环境变量 1 year ago
Lei OT 200dbaad9e chore: dev: + eslint 1 year ago
Lei OT fc81ca0363 perf: + PageSpy 1 year ago
Lei OT 0d87b5dcb4 perf: 合并 utils 文件 1 year ago
Lei OT 6ba07440a7 chore: +tailwindcss 1 year ago
Lei OT 77a03b89bc 多语言readme 1 year ago
Lei OT e3a1788b7a 多语言: 账号信息页 1 year ago
Lei OT 458986f1ce 整理多语言 namespace 1 year ago
Lei OT 74d3b79019 语言切换组件 1 year ago
Lei OT c616921548 build: vendor 1 year ago
Lei OT 461e37c720 style: invoice index search form 1 year ago
Lei OT 353e713827 语言配置文件改到public下 1 year ago
Lei OT 7e0773fcb2 feat: 多语言, 切换语言 1 year ago

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

1
.gitignore vendored

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

@ -7,6 +7,21 @@ Global Highlights Hub 海外供应商平台
2. 运行开发环境npm run dev 或者 start.bat
3. 打包代码npm run build 或者 build.bat
## 版本设置
npm version [<newversion> | major | minor | patch | premajor | preminor | prepatch | prerelease | from-git]
npm version premajor --no-git-tag-version
1.0.0 -> 2.0.0-0
--preid beta | alpha | rc
npm version prerelease --preid beta --no-git-tag-version
npm version prerelease
2.0.0-alpha-0 -> 2.0.0-alpha-1 -> 2.0.0-alpha-2 ..n -> 2.0.0-alpha-n
npm version patch --no-git-tag-version
2.0.0-n -> 2.0.0
"push:tag": "npm version patch && git.exe push --progress "origin" main:main"
"push:tag": "npm version patch && git push origin master"
## 相关文档
需求文档 https://www.kdocs.cn/l/csZrIZlpuF2i
@ -24,7 +39,47 @@ Bucket 名称global-highlights-hub
Endpointoss-cn-hongkong.aliyuncs.com
global-highlights-hub.oss-cn-hongkong.aliyuncs.com
反馈表测试链接
http://202.103.68.111:5173/feedback/330948
---
```mermaid
---
title: GHHub 开发管理 - 机票和价格管理
---
gitGraph TB:
commit
commit
branch release
branch feature/i18n
checkout main
commit id: "release" tag: "1.0.5.31"
branch bug/x1
commit id: "fix(...): ..."
checkout feature/i18n
commit
commit
checkout main
merge feature/i18n id: "机票开始"
branch feature/price_manager
checkout main
commit
commit
commit id: "账户体系"
checkout feature/price_manager
commit
commit
commit
checkout main
merge feature/price_manager id: "合并价格" type:HIGHLIGHT
commit
commit
commit tag: "2.0.0" id: "完成机票"
commit
commit
commit id: "pre-release" tag: "pre-release" type:HIGHLIGHT
commit
```

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

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

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

Binary file not shown.

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

@ -1,28 +1,49 @@
{
"name": "global.highlights.hub",
"name": "global-highlights-hub",
"private": true,
"version": "0.1.0",
"version": "2.0.20",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"4test": "vite build --mode test",
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
"preview": "vite preview"
},
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@react-pdf/renderer": "^3.4.0",
"antd": "^5.4.2",
"mobx": "^6.9.0",
"mobx-react": "^7.6.0",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"antd": "^5.17.2",
"dayjs": "^1.11.13",
"docx": "^8.5.0",
"file-saver": "^2.0.5",
"i18next": "^23.11.5",
"i18next-browser-languagedetector": "^8.0.0",
"i18next-http-backend": "^2.5.2",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^14.1.2",
"react-router-dom": "^6.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"
},
"devDependencies": {
"@types/react": "^18.0.28",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-legacy": "^4.0.2",
"@vitejs/plugin-react": "^3.1.0",
"vite": "^4.2.0"
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",
"eslint-config-react-app": "^7.0.1",
"eslint-plugin-react": "^7.32.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.3",
"postcss": "^8.4.38",
"tailwindcss": "^3.4.3",
"vite": "^4.2.0",
"vite-plugin-eslint": "^1.8.1",
"vite-plugin-windicss": "^1.9.3",
"windicss": "^3.5.6"
}
}

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

@ -0,0 +1,42 @@
{
"CurrentPassword": "Current password",
"NewPassword": "New password",
"ReenterPassword": "Reenter password",
"Validation": {
"Success": "Your password has been successfully updated.",
"Fail": "Failed to change password. Please try again.",
"CurrentPassword": "Please input your password.",
"NewPassword": "Please input your new password.",
"ReenterPassword": "Please reenter your password.",
"username": "请重复输入用户名。",
"realname": "请重复输入真实姓名。",
"email": "请重复输入邮箱。",
"travelAgency": "请重复输供应商。",
"role": "请重复输入角色。",
"roleName": "请重复输入角色名称。"
},
"createdOn": "Created on",
"action": "Action",
"action.edit": "Edit",
"action.enable": "Enable",
"action.disable": "Disable",
"action.enable.title": "Do you want to enable account?",
"action.disable.title": "Do you want to disable account?",
"action.resetPassword": "Reset Password",
"action.resetPassword.tile": "Do you want to reset password?",
"accountList": "Account List",
"newAccount": "New Account",
"detail": "Detail",
"username": "Username",
"realname": "Realname",
"travelAgency": "Travel Agency",
"travelAgencyName": "Travel Agency Name",
"email": "Email",
"lastLogin": "Last Login",
"roleList": "Role List",
"newRole": "New Role",
"roleName": "Role Name",
"permission": "Permission"
}

@ -0,0 +1,108 @@
{
"lang": {
"en": "English",
"zh": "中文"
},
"Search": "Search",
"Reset": "Reset",
"Cancel": "Cancel",
"Submit": "Submit",
"Confirm": "Confirm",
"Close": "Close",
"Save": "Save",
"New": "New",
"Edit": "Edit",
"Audit": "Audit",
"Delete": "Delete",
"Add": "Add",
"View": "View",
"Back": "Back",
"Download": "Download",
"Upload": "Upload",
"preview": "Preview",
"Total": "Total",
"Action": "Action",
"Import": "Import",
"Export": "Export",
"Copy": "Copy",
"sureCancel": "Are you sure to cancel?",
"sureDelete": "Are you sure to delete?",
"sureSubmit": "Are you sure to submit?",
"Yes": "Yes",
"No": "No",
"Success": "Success",
"Failed": "Failed",
"All": "All",
"Table": {
"Total": "Total {{total}} items"
},
"Login": "Login",
"Username": "Username",
"Realname": "Realname",
"Password": "Password",
"ChangePassword": "Change password",
"Profile": "Profile",
"Logout": "Logout",
"LoginTimeout": "Login timeout",
"LoginTimeoutTip": "Please input your password",
"userProfile": "User Profile",
"Telephone": "Telephone",
"Email": "Email address",
"Address": "Address",
"Company": "Company",
"Department": "Department",
"datetime": {
"thisWeek": "This Week",
"lastWeek": "Last Week",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"nextMonth": "Next Month",
"lastThreeMonth": "Last Three Month",
"nextThreeMonth": "Next Three Month",
"firstHalfYear": "First Half Year",
"latterHalfYear": "Latter Half Year",
"thisYear": "This Year"
},
"weekdays": {
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Sunday"
},
"weekdaysShort": {
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat",
"7": "Sun"
},
"menu": {
"Reservation": "Reservation",
"Invoice": "Invoice",
"Feedback": "Feedback",
"Notice": "Notice",
"Report": "Report",
"Airticket": "AirTicket",
"Trainticket": "TrainTicket",
"Products": "Products"
},
"Validation": {
"Title": "Notification",
"LoginFailed": "Incorrect password, Login failed.",
"UsernameIsEmpty": "Please input your username",
"PasswordIsEmpty": "Please input your password"
},
"invoiceStatus": {
"Status": "Status",
"Not_submitted": "Not submitted",
"Submitted": "Submitted",
"Travel_advisor_approved": "Travel advisor approved",
"Finance_Dept_arrproved": "Finance Dept arrproved",
"Paid": "Paid"
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,40 +1,47 @@
import React from "react";
import dayjs from "dayjs";
export const PROJECT_NAME = "GHHub";
// mode: test内部测试使用
export const HT_HOST = import.meta.env.MODE === 'test' ? 'http://120.79.9.217:10024' : import.meta.env.PROD ? 'https://p9axztuwd7x8a7.mycht.cn' : 'http://202.103.68.144:890'
export const OVERSEA_HOST = 'https://ht20-p9axztuwd7x8a7.mycht.cn'
export const HT_HOST = process.env.NODE_ENV == "production" ? "https://p9axztuwd7x8a7.mycht.cn" :"http://202.103.68.144:890"; //"http://202.103.68.231:889";
export const DATE_FORMAT = "YYYY-MM-DD";
export const DATE_FORMAT_MONTH = "YYYY-MM";
export const DATE_PRESETS = [
{
label: "This Week",
value: [dayjs().startOf("w"), dayjs().endOf("w")],
},
{
label: "Last Week",
value: [dayjs().startOf("w").subtract(7, "days"), dayjs().endOf("w").subtract(7, "days")],
},
{
label: "This Month",
value: [dayjs().startOf("M"), dayjs().endOf("M")],
},
{
label: "Last Month",
value: [dayjs().subtract(1, "M").startOf("M"), dayjs().subtract(1, "M").endOf("M")],
},
{
label: "Last Three Month",
value: [dayjs().subtract(2, "M").startOf("M"), dayjs().endOf("M")],
},
{
label: "This Year",
value: [dayjs().startOf("y"), dayjs().endOf("y")],
},
// 本月: [dayjs().startOf("month"), moment().endOf("month")],
// 上个月: [dayjs().subtract(1, "months").startOf("month"), moment(new Date()).subtract(1, "months").endOf("month")],
// 近7天: [dayjs().add(-7, "days"), dayjs()],
// 近30天: [dayjs().subtract(30, "days"), moment()],
// 近三个月: [dayjs().subtract(2, "month").startOf("month"), moment().endOf("month")],
// 今年: [dayjs().startOf("year"), moment().endOf("year")],
// 去年: [dayjs().subtract(1, "year").startOf("year"), moment().subtract(1, "year").endOf("year")],
];
export const arrManager = ["testzp","testzac","testycc","testlyj","testqqs","testjjh","testhn"];//特定账号加修改所属供应商的菜单 zp
export const SMALL_DATETIME_FORMAT = "YYYY-MM-DD 23:59";
export const OFFICEWEBVIEWERURL = "https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src=";
const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '')
export const BUILD_VERSION = import.meta.env.PROD ? __BUILD_VERSION__ : import.meta.env.MODE;
// 权限常量定义
// 账号、权限管理
// category: system
export const PERM_ACCOUNT_MANAGEMENT = '/account/management'
export const PERM_ACCOUNT_NEW = '/account/new'
export const PERM_ACCOUNT_DISABLE = '/account/disable'
export const PERM_ACCOUNT_RESET_PASSWORD = '/account/reset-password'
export const PERM_ROLE_NEW = '/account/role-new'
// 海外供应商
// category: oversea
export const PERM_OVERSEA = '/oversea/all'
// 国内供应商
// category: domestic
export const PERM_DOMESTIC = '/domestic/all'
// 机票供应商
// category: air-ticket
export const PERM_AIR_TICKET = '/air-ticket/all'
// 火车票供应商
// category: train-ticket
export const PERM_TRAIN_TICKET = '/train-ticket/all'
// 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_NEW = '/products/new'; // 新增产品
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核
export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入
export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核
export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入

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

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

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

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

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

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

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

@ -1,85 +1,132 @@
import React from "react";
import { configure } from "mobx";
import ReactDOM from "react-dom/client";
import { createRoot } from 'react-dom/client'
import {
createBrowserRouter,
RouterProvider,
} from "react-router-dom";
import RootStore from "@/stores/Root";
import { StoreContext } from '@/stores/StoreContext';
import "@/assets/global.css";
import App from "@/views/App";
import Standlone from "@/views/Standlone";
import Login from "@/views/Login";
import Index from "@/views/index";
import ErrorPage from "@/views/error-page";
import ReservationNewest from "@/views/reservation/Newest";
import ReservationDetail from "@/views/reservation/Detail";
import ChangePassword from "@/views/account/ChangePassword";
import AccountProfile from "@/views/account/Profile";
import FeedbackIndex from "@/views/feedback/Index";
import FeedbackDetail from "@/views/feedback/Detail";
import FeedbackCustomerDetail from "@/views/feedback/CustomerDetail";
import ReportIndex from "@/views/report/Index";
import NoticeIndex from "@/views/notice/Index";
import NoticeDetail from "@/views/notice/Detail";
import InvoiceIndex from "@/views/invoice/Index";
import InvoiceDetail from "@/views/invoice/Detail";
import InvoicePaid from "@/views/invoice/Paid";
import InvoicePaidDetail from "@/views/invoice/PaidDetail";
import ChangeVendor from "@/views/account/ChangeVendor";
configure({
useProxies: "ifavailable",
enforceActions: "observed",
computedRequiresReaction: true,
observableRequiresReaction: false,
reactionRequiresObservable: true,
disableErrorBoundaries: process.env.NODE_ENV == "production"
});
const router = createBrowserRouter([
} from 'react-router-dom'
import '@/assets/global.css'
import App from '@/views/App'
import Standlone from '@/views/Standlone'
import Login from '@/views/Login'
import Logout from '@/views/Logout'
import ErrorPage from '@/components/ErrorPage'
import RequireAuth from '@/components/RequireAuth'
import ReservationNewest from '@/views/reservation/Newest'
import ReservationDetail from '@/views/reservation/Detail'
import ChangePassword from '@/views/account/ChangePassword'
import AccountProfile from '@/views/account/Profile'
import AccountManagement from '@/views/account/Management'
import RoleList from '@/views/account/RoleList'
import FeedbackIndex from '@/views/feedback/Index'
import FeedbackDetail from '@/views/feedback/Detail'
import FeedbackCustomerDetail from '@/views/feedback/CustomerDetail'
import ReportIndex from '@/views/report/Index'
import NoticeIndex from '@/views/notice/Index'
import NoticeDetail from '@/views/notice/Detail'
import InvoiceIndex from '@/views/invoice/Index'
import InvoiceDetail from '@/views/invoice/Detail'
import InvoiceHistory from '@/views/invoice/History'
import InvoicePaid from '@/views/invoice/Paid'
import InvoicePaidDetail from '@/views/invoice/PaidDetail'
import Airticket from '@/views/airticket/Index'
import AirticketPlan from '@/views/airticket/Plan'
import AirticketInvoice from '@/views/airticket/Invoice'
import AirticketInvoicePaid from '@/views/airticket/InvoicePaid'
import Trainticket from '@/views/trainticket/index'
import TrainticketPlan from '@/views/trainticket/plan'
import TrainticketInvoice from '@/views/trainticket/invoice'
import TrainticketInvoicePaid from '@/views/trainticket/invoicePaid'
import { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from './stores/Auth'
import { isNotEmpty } from '@/utils/commons'
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
import './i18n'
const initRouter = async () => {
return createBrowserRouter([
{
path: "/",
path: '/',
element: <App />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <Index /> },
{ path: "reservation/newest", element: <ReservationNewest />},
{ path: "reservation/:reservationId", element: <ReservationDetail />},
{ path: "account/change-password", element: <ChangePassword />},
{ path: "account/profile", element: <AccountProfile />},
{ path: "feedback", element: <FeedbackIndex />},
{ path: "feedback/:GRI_SN/:RefNo", element: <FeedbackDetail />},
{ path: "feedback/:GRI_SN/:CII_SN/:RefNo", element: <FeedbackCustomerDetail />},
{ path: "report", element: <ReportIndex />},
{ path: "notice", element: <NoticeIndex />},
{ path: "notice/:CCP_BLID", element: <NoticeDetail />},
{ path: "invoice",element:<InvoiceIndex />},
{ path: "invoice/detail/:GMDSN/:GSN",element:<InvoiceDetail />},
{ path: "invoice/paid",element:<InvoicePaid />},
{ path: "invoice/paid/detail/:flid",element:<InvoicePaidDetail />},
{ path: "account/change-vendor",element:<ChangeVendor />},
{ index: true, element: <NoticeIndex /> },
{ path: 'account/change-password', element: <ChangePassword />},
{ path: 'account/profile', element: <AccountProfile />},
{ path: 'account/management', element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
{ path: 'account/role-list', element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
{ path: 'reservation/newest', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
{ path: 'reservation/:reservationId', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
{ path: 'feedback', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
{ path: 'report', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
{ path: 'notice', element: <NoticeIndex />},
{ path: 'notice/:CCP_BLID', element: <NoticeDetail />},
{ path: 'invoice',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
{ path: 'invoice/detail/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
{ path: 'invoice/history/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceHistory /></RequireAuth>},
{ path: 'invoice/paid',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaid /></RequireAuth>},
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_OVERSEA} result={true}><InvoicePaidDetail /></RequireAuth>},
{ path: 'airticket',element: <RequireAuth subject={PERM_AIR_TICKET} result={true}><Airticket /></RequireAuth>},
{ path: 'airticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
{ path: 'airticket/invoice',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoice /></RequireAuth>},
{ path: 'airticket/invoicepaid',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoicePaid /></RequireAuth>},
{ path: 'trainticket',element: <RequireAuth subject={PERM_TRAIN_TICKET} result={true}><Trainticket /></RequireAuth>},
{ path: 'trainticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketPlan /></RequireAuth>},
{ path: 'trainticket/invoice',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoice /></RequireAuth>},
{ path: 'trainticket/invoicepaid',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoicePaid /></RequireAuth>},
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/audit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
]
},
{
element: <Standlone />,
children: [
{ path: "/login", element: <Login /> },
{ path: '/login', element: <Login /> },
{ path: '/logout', element: <Logout /> },
]
}
]);
])
}
const initAppliction = async () => {
const rootStore = new RootStore();
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
await useAuthStore.getState().initAuth()
}
ReactDOM.createRoot(document.getElementById("root")).render(
const router = await initRouter()
const root = document.getElementById('root')
if (!root) throw new Error('No root element found')
createRoot(root).render(
//<React.StrictMode>
<StoreContext.Provider value={rootStore}>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider
router={router}
fallbackElement={() => <div>Loading...</div>}
/>
</StoreContext.Provider>
</ThemeContext.Provider>
//</React.StrictMode>
);
)
}
initAppliction()

@ -0,0 +1,51 @@
import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } from '@/config';
export const loadPageSpy = (title) => {
if (import.meta.env.DEV || window.$pageSpy) return
const PageSpyConfig = { api: 'page-spy.mycht.cn', project: PROJECT_NAME, title: title, autoRender: false };
const PageSpySrc = [
'https://page-spy.mycht.cn/page-spy/index.min.js'+`?${BUILD_VERSION}`,
'https://page-spy.mycht.cn/plugin/data-harbor/index.min.js'+`?${BUILD_VERSION}`,
'https://page-spy.mycht.cn/plugin/rrweb/index.min.js'+`?${BUILD_VERSION}`,
];
Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
//
window.$harbor = new DataHarborPlugin();
window.$rrweb = new RRWebPlugin();
[window.$harbor, window.$rrweb].forEach(p => {
PageSpy.registerPlugin(p)
})
window.$pageSpy = new PageSpy(PageSpyConfig);
});
};
export const uploadPageSpyLog = async () => {
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
await window.$harbor.upload() // { clearCache: true, remark: '' }
alert('Success')
} else {
alert('Failure')
}
}
export const PageSpyLog = () => {
return (
<>
{window.$pageSpy && (
<a
className='text-primary'
onClick={() => {
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
}}>
上传Debug日志 ({window.$pageSpy.address.substring(0, 4)})
</a>
)}
</>
);
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,57 +1,37 @@
import { Outlet, Link, useHref, useLocation } from "react-router-dom";
import { useEffect } from "react";
import { observer } from "mobx-react";
import { Layout, Menu, ConfigProvider, theme, Typography, Space, Row, Col, Alert, App as AntApp } from "antd";
import { DownOutlined } from "@ant-design/icons";
import { Outlet } from "react-router-dom";
import { Layout, ConfigProvider, theme, Row, Col, App as AntApp } from "antd";
import "antd/dist/reset.css";
import AppLogo from "@/assets/logo-gh.png";
import { useStore } from "@/stores/StoreContext.js";
import { useThemeContext } from "@/stores/ThemeContext";
import { BUILD_VERSION } from "@/config";
const { Title } = Typography;
const { Header, Content, Footer } = Layout;
function Standlone() {
const { authStore } = useStore();
const {
token: { colorBgContainer },
} = theme.useToken();
const { colorPrimary } = useThemeContext();
return (
<ConfigProvider
theme={{
token: {
colorPrimary: "#00b96b",
colorPrimary: colorPrimary,
},
algorithm: theme.defaultAlgorithm,
}}>
<AntApp>
<Layout
style={{
minHeight: "100vh",
}}>
<Header className="header" style={{ position: "sticky", top: 0, zIndex: 1, width: "100%" }}>
<Row gutter={{ md: 24 }} justify="center">
<Col span={4}>
<img src={AppLogo} className="logo" alt="App logo" />
</Col>
<Col span={20}><Title style={{ color: "white", marginTop: "3.5px" }}>Global Highlights Hub</Title></Col>
</Row>
<Layout className="min-h-screen">
<Header className="sticky top-0 z-10 w-full">
<img src={AppLogo} className="float-left h-9 my-4 mr-6 ml-0 bg-white/30" alt="App logo" />
<p className="text-white text-center">Global Highlights Hub</p>
</Header>
<Content
style={{
padding: 24,
margin: 0,
minHeight: 280,
background: colorBgContainer,
}}>
<Content className="p-6 m-0 min-h-72 bg-white">
<Outlet />
</Content>
<Footer></Footer>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
</Layout>
</AntApp>
</ConfigProvider>
);
}
export default observer(Standlone);
export default Standlone;

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

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

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

@ -1,23 +1,24 @@
import { Descriptions, Col, Row } from 'antd';
import { useStore } from '@/stores/StoreContext.js';
import { Descriptions, Col, Row } from 'antd'
import { useTranslation } from 'react-i18next'
import useAuthStore from '@/stores/Auth'
function Profile() {
const { authStore } = useStore();
const { login } = authStore;
const { t } = useTranslation()
const currentUser = useAuthStore(state => state.currentUser)
return (
<Row>
<Col span={12} offset={6}>
<Descriptions title="User Profile" layout="vertical" column={2}>
<Descriptions.Item label="Username">{login.username}</Descriptions.Item>
<Descriptions.Item label="Telephone">{login.telephone}</Descriptions.Item>
<Descriptions.Item label="Email address">{login.emailAddress}</Descriptions.Item>
<Descriptions.Item label="Company">{login.travelAgencyName}</Descriptions.Item>
<Descriptions title={t('userProfile')} layout="vertical" column={2}>
<Descriptions.Item label={t("Username")}>{currentUser?.username}</Descriptions.Item>
<Descriptions.Item label={t("Realname")}>{currentUser?.realname}</Descriptions.Item>
<Descriptions.Item label={t("Email")}>{currentUser?.emailAddress}</Descriptions.Item>
<Descriptions.Item label={t("Company")}>{currentUser?.travelAgencyName}</Descriptions.Item>
</Descriptions>
</Col>
</Row>
);
}
export default Profile;
export default Profile

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

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

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

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

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

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

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

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

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

@ -1,22 +1,21 @@
import { useParams, useNavigate, NavLink } from "react-router-dom";
import { useEffect, useState, useRef } from "react";
import { observer } from "mobx-react";
import { toJS, runInAction } from "mobx";
import { Row, Col, Space, Button, Typography, Card, Form, Upload, Input, Divider, DatePicker, Select, App, Descriptions, Image, Steps } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import { PlusOutlined, AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons";
import { useParams, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Button, Typography, Card, Form, Upload, Input, Divider, DatePicker, Select, App, Descriptions, Image } from "antd";
import { PlusOutlined,AuditOutlined } from "@ant-design/icons";
import { isNotEmpty } from "@/utils/commons";
import * as config from "@/config";
import dayjs from "dayjs";
import { fetchInvoiceDetail, postEditInvoiceDetail, postAddInvoice } from "@/stores/Invoice";
import { removeFeedbackImages } from "@/stores/Feedback";
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
const { Title, Text } = Typography;
const { TextArea } = Input;
function Detail() {
const navigate = useNavigate();
const { GMDSN, GSN } = useParams();
const { invoiceStore, authStore } = useStore();
const { invoicekImages, invoiceGroupInfo, invoiceProductList, invoiceCurrencyList, invoiceZDDetail } = invoiceStore;
const { userId, travelAgencyId, loginToken } = usingStorage();
const [form] = Form.useForm();
const [dataLoading, setDataLoading] = useState(false);
const [edited, setEdited] = useState(true); //
@ -25,6 +24,14 @@ function Detail() {
const [invoicePicList, setInvoicePicList] = useState([]);
const [invoiceZDDetail, setInvoiceZDDetail] = useState([]);
const [invoiceCurrencyList, setInvoiceCurrencyList] = useState([]);
const [invoiceProductList, setInvoiceProductList] = useState([]);
const [invoiceGroupInfo, setInvoiceGroupInfo] = useState({});
const [invoiceFormData, setInvoiceFormData] = useState({});
const [invoicekImages, setInvoicekImages] = useState([]);
useEffect(() => {
console.info("Detail.useEffect: " + GMDSN + "/" + GSN);
defaultShow();
@ -32,19 +39,23 @@ function Detail() {
function defaultShow() {
setDataLoading(true);
invoiceStore
.fetchInvoiceDetail(GMDSN, GSN)
fetchInvoiceDetail(travelAgencyId, GMDSN, GSN)
.then(json => {
let ZDDetail = json.ZDDetail;
setInvoiceZDDetail(json.invoiceZDDetail);
setInvoiceGroupInfo(json.invoiceGroupInfo);
setInvoiceProductList(json.invoiceProductList);
setInvoiceCurrencyList(json.invoiceCurrencyList);
let ZDDetail = json.invoiceZDDetail;
if (isNotEmpty(ZDDetail)) {
let arrLen = ZDDetail.length;
const formData = ZDDetail.map((data, index) => {
if (data.GMD_Dealed == false && arrLen == index + 1) {
//
runInAction(() => {
invoiceStore.invoiceFormData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "", info_gmdsn: data.GMD_SN };
});
return { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "", info_gmdsn: data.GMD_SN };
const _formData = { info_money: data.GMD_Cost, info_Currency: data.GMD_Currency, info_date: isNotEmpty(data.GMD_PayDate) ? dayjs(data.GMD_PayDate) : "", info_gmdsn: data.GMD_SN };
setInvoiceFormData(_formData);
return _formData;
}
});
@ -68,9 +79,7 @@ function Detail() {
});
}
if (data.GMD_Dealed == false && arrLen == index + 1) {
runInAction(() => {
invoiceStore.invoicekImages = picList;
});
setInvoicekImages(picList);
}
return picList;
});
@ -90,7 +99,7 @@ function Detail() {
});
}
const fileList = toJS(invoicekImages);
const fileList = invoicekImages;
//
let arrimg = [];
if (isNotEmpty(fileList)) {
@ -106,15 +115,13 @@ function Detail() {
info_date: isNotEmpty(values["info_date"]) ? values["info_date"].format("YYYY-MM") : null,
info_images: JSON.stringify(arrimg),
};
console.log("Success:", fieldVaule);
// console.log("Success:", fieldVaule);
//
if (fieldVaule) {
invoiceStore.postEditInvoiceDetail(fieldVaule.info_gmdsn, fieldVaule.info_Currency, fieldVaule.info_money, fieldVaule.info_date, fieldVaule.info_images, "").then(data => {
console.log(data);
runInAction(() => {
postEditInvoiceDetail(userId, fieldVaule.info_gmdsn, fieldVaule.info_Currency, fieldVaule.info_money, fieldVaule.info_date, fieldVaule.info_images, "").then(data => {
// console.log(data);
let param = { info_money: fieldVaule.info_money, info_Currency: fieldVaule.info_Currency, info_date: fieldVaule.info_date };
invoiceStore.invoiceFormData = param;
});
setInvoiceFormData(param);
if (data.errcode == 0) {
setEdited(true);
notification.success({
@ -137,14 +144,12 @@ function Detail() {
}
return file;
});
runInAction(() => {
invoiceStore.invoicekImages = newFileList;
});
setInvoicekImages(newFileList);
};
const handRemove = info => {
console.log(info);
invoiceStore.removeFeedbackImages(info.url);
removeFeedbackImages(info.url);
return true;
};
@ -162,8 +167,7 @@ function Detail() {
}
function addInvoice() {
invoiceStore
.postAddInvoice(GSN, "", 0, "", "[]", "")
postAddInvoice(userId, travelAgencyId, GSN, "", 0, "", "[]", "")
.then(data => {})
.finally(() => {
defaultShow();
@ -189,6 +193,21 @@ function Detail() {
}
}
const invoiceStatus = FKState => {
switch (FKState - 1) {
case 1:
return "Submitted";
case 2:
return "Travel Advisor";
case 3:
return "Finance Dept";
case 4:
return "Paid";
default:
return "";
}
};
//
function bindSubmitForm() {
if (isNotEmpty(invoiceZDDetail)) {
@ -199,27 +218,29 @@ function Detail() {
<Row key={data.GMD_SN} gutter={16}>
<Col span={4}></Col>
<Col span={16}>
<Card type="inner" title={"Invoice " + ++index}>
<Card type="inner" title={"Invoice " + (index + 1)}>
<Row gutter={16}>
<Col span={12}>
<Descriptions column={1}>
<Descriptions.Item label="Amount">{data.GMD_Cost}</Descriptions.Item>
<Descriptions.Item label="Currency">{data.GMD_Currency}</Descriptions.Item>
<Descriptions.Item label="Due Dat">{data.GMD_PayDate}</Descriptions.Item>
<Descriptions.Item label="Status">{invoiceStore.invoiceStatus(data.FKState)}</Descriptions.Item>
<Descriptions.Item label="Due Date">{data.GMD_PayDate}</Descriptions.Item>
<Descriptions.Item label="Status">{invoiceStatus(data.FKState)}</Descriptions.Item>
</Descriptions>
</Col>
<Col span={12}>
<Image.PreviewGroup>
{invoicePicList[index] &&
invoicePicList[index].map(item => {
if (item.url) {
return <Image key={item.uid} width={90} src={item.url} />;
}
})}
</Image.PreviewGroup>
</Col>
</Row>
</Card>
{addButton(index++ == invoiceZDDetail.length)}
{addButton(index + 1 == invoiceZDDetail.length)}
</Col>
<Col span={4}></Col>
</Row>
@ -232,7 +253,7 @@ function Detail() {
<Col span={16}>
<Card
type="inner"
title={"Invoice " + ++index}
title={"Invoice " + (index + 1)}
extra={
<Button type="link" onClick={() => setEdited(false)}>
Edit
@ -264,17 +285,21 @@ function Detail() {
]}>
<Select placeholder="Select Currency type" onChange={onCurrencyChange} options={bindCurrency()}></Select>
</Form.Item>
<Form.Item name="info_date" label="Due Month"
<Form.Item
name="info_date"
label="Due Month"
rules={[
{
required: true,
message: "please select Due Month!",
},
]}
>
]}>
<DatePicker picker="month" />
</Form.Item>
<Text type="secondary">Payment is arranged during the last week of each month. If the invoice is issued after the 20th, please select the following month for payment. For urgent payments, please contact the travel advisor. </Text>
<Text type="secondary">
Payment is arranged during the last week of each month. If the invoice is issued after the 20th, please select the following month for payment. For urgent payments, please contact the
travel advisor.{" "}
</Text>
<Form.Item name="info_gmdsn" hidden={true}>
<input />
</Form.Item>
@ -290,13 +315,12 @@ function Detail() {
<Upload
name="ghhfile"
multiple={true}
action={config.HT_HOST + `/service-fileServer/FileUpload?GRI_SN=${GSN}&VEI_SN=${authStore.login.travelAgencyId}&FilePathName=invoice&token=${authStore.login.token}`}
action={config.OVERSEA_HOST + `/service-fileServer/FileUpload?GRI_SN=${GSN}&VEI_SN=${travelAgencyId}&FilePathName=invoice&token=${loginToken}`}
fileList={fileList}
listType="picture-card"
onChange={handleChange}
onRemove={handRemove}
accept=".jpg,.png,.peg,.bmp"
>
accept=".jpg,.png,.peg,.bmp">
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload Invoice</div>
@ -333,8 +357,9 @@ function Detail() {
<Title level={4}>Reference Number: {invoiceGroupInfo.VGroupInfo}</Title>
</Col>
<Col span={4}>
<Button type="link" onClick={() => navigate("/invoice")}>
Back
<BackBtn />
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/history/0/338787`)}>
Billing Records
</Button>
</Col>
</Row>
@ -345,4 +370,4 @@ function Detail() {
);
}
export default observer(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;

@ -1,23 +1,19 @@
import { NavLink, useNavigate } from "react-router-dom";
import { useEffect, useState } from "react";
import { observer } from "mobx-react";
import { toJS } from "mobx";
import { Row, Col, Space, Button, Table, Input, DatePicker, Typography, App, Steps ,Select } from "antd";
import { useStore } from "@/stores/StoreContext.js";
import * as config from "@/config";
import { Row, Col, Space, Button, Table, App, Steps } from "antd";
import { formatDate, isNotEmpty } from "@/utils/commons";
import { AuditOutlined, SmileOutlined, SolutionOutlined, EditOutlined } from "@ant-design/icons";
import SearchForm from '@/components/SearchForm';
import dayjs from 'dayjs';
import useInvoiceStore from '@/stores/Invoice';
import { usingStorage } from "@/hooks/usingStorage";
const { Title } = Typography;
function Index() {
const { authStore, invoiceStore } = useStore();
const { invoiceList, search_date_start, search_date_end } = invoiceStore;
const [groupNo, onGroupNoChange] = useState("");
const [OrderType, onOrderTypeChange] = useState(0); //
const {travelAgencyId, } = usingStorage();
const [invoiceList, fetchInvoiceList] = useInvoiceStore((state) => [state.invoiceList, state.fetchInvoiceList]);
const navigate = useNavigate();
const { notification } = App.useApp();
const showTotal = total => `Total ${invoiceList.length} items`;
const showTotal = total => `Total ${total} items`;
const invoiceListColumns = [
{
@ -75,84 +71,44 @@ function Index() {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Title level={3}></Title>
<Row gutter={16}>
<Col md={24} lg={4} xxl={4}>
<Input
placeholder="Reference Number"
onChange={e => {
onGroupNoChange(e.target.value);
<Col md={16} sm={16} xs={24} >
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
}}
/>
</Col>
<Col md={24} lg={2} xxl={3}>
<Select
defaultValue="Status"
style={{
width: 220,
}}
onChange={ value => {onOrderTypeChange(value);}}
options={[
{
value: '0',
label: 'Status',
},
{
value: '1',
label: 'Not submitted',
},
{
value: '2',
label: 'Submitted',
fieldsConfig={{
shows: ['referenceNo', 'invoiceStatus', 'dates'],
fieldProps: {
referenceNo: { col: 7 },
invoiceStatus: { col: 4},
dates: { col: 10 },
},
{
value: '3',
label: 'Travel advisor approved',
},
{
value: '4',
label: 'Finance Dept arrproved',
},
{
value: '5',
label: 'Paid',
},
]}
}}
onSubmit={(err, formVal, filedsVal) => {
fetchInvoiceList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.enddate, formVal.invoiceStatus);
}}
/>
</Col>
<Col md={24} lg={8} xxl={6}>
<Space direction="horizontal">
Arrival Date
<DatePicker.RangePicker format={config.DATE_FORMAT} allowClear={true} style={{ width: "100%" }} defaultValue={[search_date_start, search_date_end]} presets={config.DATE_PRESETS} onChange={invoiceStore.onDateRangeChange} allowEmpty={true} />
</Space>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button
type="primary"
loading={invoiceStore.loading}
onClick={() => invoiceStore.fetchInvoiceList(authStore.login.travelAgencyId, groupNo, search_date_start==null?null:search_date_start.format(config.DATE_FORMAT), search_date_end==null?null:search_date_end.format(config.DATE_FORMAT),OrderType)}>
Search
</Button>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/338787`)}>
<Col md={8} sm={8} xs={24} >
<Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/395074`)}>
Misc. Invoice
</Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/paid`)}>
Bank statement
</Button>
</Space>
</Col>
</Row>
<Title level={3}></Title>
<Row>
<Col md={24} lg={24} xxl={24}>
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={toJS(invoiceList)} />
<Table bordered pagination={{ defaultPageSize: 20, showTotal: showTotal }} columns={invoiceListColumns} dataSource={(invoiceList)} />
</Col>
</Row>
</Space>
);
}
export default observer(Index);
export default (Index);
//value={[search_date_start, search_date_end]} 20230803 zp

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

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

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

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

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

@ -0,0 +1,58 @@
import { useState } from 'react';
import { App, Divider, Empty, Flex } from 'antd';
import { isEmpty } from '@/utils/commons';
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
import Header from './Detail/Header';
import { useParams } from 'react-router-dom';
import useProductsStore from '@/stores/Products/Index';
import dayjs from 'dayjs';
import { usingStorage } from '@/hooks/usingStorage';
import ProductsTree from './Detail/ProductsTree';
import ProductInfo from './Detail/ProductInfo';
import NewProductModal from './Detail/NewProductModal';
function Detail() {
const { notification, modal } = App.useApp();
const { travel_agency_id, audit_state, use_year } = useParams();
const [addProductVisible, setAddProductVisible] = useState(false);
const [agencyProducts, switchParams] = useProductsStore((state) => [state.agencyProducts, state.switchParams]);
const [getAgencyProducts, activeAgency] = useProductsStore((state) => [state.getAgencyProducts, state.activeAgency]);
const [loading, setLoading] = useProductsStore((state) => [state.loading, state.setLoading]);
const { travelAgencyId } = usingStorage();
const handleGetAgencyProducts = async ({ pick_year, pick_agency, pick_state } = {}) => {
const year = pick_year || use_year || switchParams.use_year || dayjs().year();
const agency = pick_agency || travel_agency_id || travelAgencyId;
const state = pick_state ?? audit_state;
const param = { travel_agency_id: agency, use_year: year, audit_state: state };
// setEditingProduct({});
getAgencyProducts(param).catch((ex) => {
setLoading(false);
notification.error({
message: 'Notification',
description: ex.message,
placement: 'top',
duration: 4,
});
});
};
return (
<SecondHeaderWrapper
loading={loading}
backTo={false}
header={<Header title={activeAgency.travel_agency_name} refresh={handleGetAgencyProducts} handleNewProduct={() => setAddProductVisible(true)} />}>
<>
<Flex gap={10} className='h-full'>
{/* onNodeSelect={handleNodeSelect} */}
<ProductsTree className='basis-80 sticky top-0 overflow-y-auto shrink-0' style1={{ height: 'calc(100vh - 150px)' }} />
<Divider type={'vertical'} className='mx-1 h-auto' />
<div className=' flex-auto overflow-auto '>
<ProductInfo />
</div>
</Flex>
</>
</SecondHeaderWrapper>
);
}
export default Detail;

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save