Compare commits

..

1 Commits

Author SHA1 Message Date
Lei OT fb3b2d7bae perf: 地接首页先选年份再展示 10 months ago

@ -1,57 +0,0 @@
# Build & deploy
name: 🚀 Deploy GHH
on:
push:
tags:
- 'v*'
jobs:
build:
name: 🎉 Deploy
runs-on: ubuntu-latest
steps:
- name: 🚚 Get latest code
uses: actions/checkout@v4
- name: Use Node.js 20
uses: actions/setup-node@v4
with:
node-version: '20'
cache: "npm"
- name: 🔨 Build Project
run: |
npm install
npm run build --if-present
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: fe-build
path: |
dist
build
out
if-no-files-found: ignore
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
with:
name: fe-build
path: ./build-output
- name: 📂 Sync files
uses: SamKirkland/FTP-Deploy-Action@v4.3.4
with:
server: ${{ secrets.GHH_SERVER }}
username: ${{ secrets.GHH_USER }}
password: ${{ secrets.GHH_PASSWORD }}
port: ${{ secrets.GHH_PORT }}
local-dir: ./build-output
server-dir: ${{ secrets.GHH_PATH }}
# dangerous-clean-slate: true # deletes remote files before upload

@ -33,7 +33,6 @@ antd https://ant-design.antgroup.com/components/upload-cn#uploadfile
wps的文档预览 https://wwo.wps.cn/docs/front-end/introduction/quick-start
pdf生成 https://github.com/ivmarcos/react-to-pdf
react-pdf https://react-pdf.org
生成Docx文档 https://docx.js.org/#/?id=welcome
## 阿里云OSS
Bucket 名称global-highlights-hub

@ -1,5 +1,3 @@
use Tourmanager
CREATE TABLE auth_role
(
[role_id] [int] IDENTITY(1,1) NOT NULL,
@ -57,23 +55,8 @@ 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 ('搜索所有地接', '/agency/all', '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 ('团预订', '/reservation/all', 'oversea')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('账单', '/invoice/all', 'oversea')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('反馈表', '/feedback/all', 'oversea')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('质量评分', '/report/all', 'oversea')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('站外好评', '/review/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])
@ -83,11 +66,11 @@ VALUES ('所有火车票功能', '/train-ticket/all', 'train-ticket')
-- 价格管理
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('管理产品(供应商列表)', '/products/*', 'products')
VALUES ('管理产品', '/products/*', 'products')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
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/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])
@ -104,14 +87,6 @@ INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(客服)', 'route=/products', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('产品管理(供应商)', 'route=/products/edit', 'page')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('采购年份', 'route=/products/pick-year', 'page')
-- 公告管理
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('公告管理', '/notifs/management', 'system')
INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
VALUES ('删除公告', '/notifs/danger/delete', 'system')
INSERT INTO [dbo].[auth_permission] ([role_id] ,[res_id])
VALUES (1, 1)

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Highlights Hub</title>
<title>Global Highlights Hub</title>
<style>
.loading{width:150px;height:8px;border-radius:4px;margin:0 auto;margin-top:200px;position:relative;background:#777;overflow:hidden}
.loading span{display:block;width:100%;height:100%;border-radius:3px;background:#00b96b;animation:changePosition 4s linear infinite}

@ -1,7 +1,7 @@
{
"name": "global-highlights-hub",
"private": true,
"version": "2.3.10",
"version": "2.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -12,29 +12,26 @@
},
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@haina/utils-commons": "https://research.hainatravel.com/npm/utils-commons-0.1.0.tgz",
"@haina/utils-pagespy": "https://research.hainatravel.com/npm/utils-pagespy-0.1.1.tgz",
"@haina/utils-request": "https://research.hainatravel.com/npm/utils-request-0.1.0.tgz",
"@react-pdf/renderer": "^3.4.0",
"antd": "^5.28.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",
"quill": "^2.0.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-i18next": "^14.1.2",
"react-router-dom": "^6.30.1",
"react-router-dom": "^6.10.0",
"react-to-pdf": "^1.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.18.11/xlsx-0.18.11.tgz",
"zustand": "^4.5.7"
"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",
"autoprefixer": "^10.4.19",
"eslint": "^8.57.0",

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.5 KiB

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -1,130 +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",
"Clear": "Clear",
"Publish": "Publish",
"Read": "Read",
"UnRead": "UnRead",
"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",
"Select": "Select",
"Table": {
"Total": "Total {{total}} items"
},
"operator": "Operator",
"time": "Time",
"Login": "Login",
"Username": "Username",
"Realname": "Realname",
"Password": "Password",
"ChangePassword": "Change password",
"Profile": "Profile",
"Logout": "Logout",
"LoginTimeout": "Login timeout",
"LoginTimeoutTip": "Please input your password",
"userProfile": "User Profile",
"Telephone": "Telephone",
"Email": "Email address",
"Address": "Address",
"Company": "Company",
"Department": "Department",
"datetime": {
"thisWeek": "This Week",
"lastWeek": "Last Week",
"thisMonth": "This Month",
"lastMonth": "Last Month",
"nextMonth": "Next Month",
"lastThreeMonth": "Last Three Month",
"nextThreeMonth": "Next Three Month",
"firstHalfYear": "First Half Year",
"latterHalfYear": "Latter Half Year",
"thisYear": "This Year"
},
"weekdays": {
"1": "Monday",
"2": "Tuesday",
"3": "Wednesday",
"4": "Thursday",
"5": "Friday",
"6": "Saturday",
"7": "Sunday"
},
"weekdaysShort": {
"1": "Mon",
"2": "Tue",
"3": "Wed",
"4": "Thu",
"5": "Fri",
"6": "Sat",
"7": "Sun"
},
"menu": {
"Reservation": "Reservation",
"Invoice": "Invoice",
"Feedback": "Feedback",
"Notice": "Notice",
"Report": "Report",
"external-reviews": "External Reviews",
"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"
},
"Compare": "Compare",
"SystemUpdate": "System Update",
"review": {
"ReviewLink": "Review Link",
"DatePosted": "Date Posted",
"ReferenceNumber": "Reference Number",
"AdminNotes": "Admin Notes",
"CustomerID": "Customer ID",
"Guide": "Guide",
"Bonus": "Bonus",
"ApprovalStatus": "Approval Status",
"ApprovalDate": "Approval Date",
"Action": "Action"
}
}
{
"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"
}
}

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

@ -1,10 +1,5 @@
{
"ProductType": "Product Type",
"ProductName": "Product Name",
"ContractRemarks": "合同备注",
"versionHistory": "Version History",
"versionPublished": "Published",
"versionSnapshot": "Snapshot",
"type": {
"Experience": "Experience",
"Car": "Transport Services",
@ -54,8 +49,6 @@
"RecommendsRate": "Recommends Rate",
"OpenWeekdays": "Open Weekdays",
"DisplayToC": "Display To C",
"SortOrder": "Sort order",
"subTypeD": "Package Type",
"Dept": "Dept",
"Code": "Code",
"City": "City",
@ -86,8 +79,7 @@
"withQuote": "Whether to copy the quotation",
"requiredVendor": "Please pick a target vendor",
"requiredTypes": "Please select product types",
"requiredDept": "Please pick a owner department",
"copyTo": "Copy to"
"requiredDept": "Please pick a owner department"
},
"Validation": {
"adultPrice": "请输入成人价",

@ -1,117 +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": "复制",
"Clear": "清空",
"Publish": "发布",
"Read": "已读",
"UnRead": "未读",
"sureCancel": "确定取消?",
"sureDelete": "确定删除?",
"sureSubmit": "确定提交?",
"Yes": "是",
"No": "否",
"Success": "成功",
"Failed": "失败",
"All": "所有",
"Select": "选择",
"Table": {
"Total": "共 {{total}} 条"
},
"operator": "操作",
"time": "时间",
"Login": "登录",
"Username": "账号",
"Realname": "姓名",
"Password": "密码",
"ChangePassword": "修改密码",
"Profile": "账户中心",
"Logout": "退出",
"LoginTimeout": "登录超时",
"LoginTimeoutTip": "请输入密码",
"userProfile": "账号信息",
"Telephone": "联系电话",
"Email": "电子邮箱",
"Address": "公司地址",
"Company": "公司名称",
"Department": "部门",
"datetime": {
"thisWeek": "本周",
"lastWeek": "上周",
"thisMonth": "本月",
"lastMonth": "上月",
"nextMonth": "下月",
"lastThreeMonth": "前三个月",
"nextThreeMonth": "后三个月",
"firstHalfYear": "上半年",
"latterHalfYear": "下半年",
"thisYear": "今年"
},
"weekdays": {
"1": "周一",
"2": "周二",
"3": "周三",
"4": "周四",
"5": "周五",
"6": "周六",
"7": "周日"
},
"weekdaysShort": {
"1": "一",
"2": "二",
"3": "三",
"4": "四",
"5": "五",
"6": "六",
"7": "日"
},
"menu": {
"Reservation": "团预订",
"Invoice": "账单",
"Feedback": "反馈表",
"Notice": "通知",
"Report": "质量评分",
"Airticket": "机票订票",
"Trainticket": "火车订票",
"Products": "产品管理"
},
"Validation": {
"Title": "温馨提示",
"LoginFailed": "密码错误,登陆失败。",
"UsernameIsEmpty": "请输入账号",
"PasswordIsEmpty": "请输入密码"
},
"invoiceStatus": {
"Status": "审核状态",
"Not_submitted": "待提交",
"Submitted": "待审核",
"Travel_advisor_approved": "顾问已审核",
"Finance_Dept_arrproved": "财务已审核",
"Paid": "已打款"
},
"Compare": "对比",
"SystemUpdate": "系统更新"
}
{
"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": "已打款"
}
}

@ -1,10 +1,6 @@
{
"ProductType": "项目类型",
"ProductName": "产品名称",
"ContractRemarks": "合同备注",
"versionHistory": "查看历史",
"versionPublished": "已发布的",
"versionSnapshot": "快照",
"type": {
"Experience": "综费",
"Car": "车费",
@ -54,8 +50,6 @@
"RecommendsRate": "推荐指数",
"OpenWeekdays": "开放时间",
"DisplayToC": "报价信显示",
"SortOrder": "排序",
"subTypeD": "包价类型",
"Dept": "小组",
"Code": "简码",
"City": "城市",
@ -86,8 +80,7 @@
"withQuote": "是否复制报价",
"requiredVendor": "请选择目标供应商",
"requiredTypes": "请选择产品类型",
"requiredDept": "请选择所属小组",
"copyTo": "复制到"
"requiredDept": "请选择所属小组"
},
"Validation": {
"adultPrice": "请输入成人价",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

@ -2,7 +2,7 @@ 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 '@haina/utils-commons';
import { isNotEmpty } from '@/utils/commons';
const BackBtn = ({to, ...props}) => {
const { t } = useTranslation();

@ -1,7 +1,7 @@
import { createContext, useEffect, useState } from 'react';
import {} from 'antd';
import SearchInput from './SearchInput';
import { fetchJSON } from '@haina/utils-request';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { useTranslation } from 'react-i18next';
@ -15,7 +15,7 @@ const CitySelector = ({ ...props }) => {
const { t } = useTranslation();
return (
<>
<SearchInput initLoad={false}
<SearchInput
placeholder={t('products:City')}
mode={null}
maxTagCount={0}

@ -2,7 +2,7 @@ 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 "@haina/utils-commons";
import { isEmpty, getNestedValue } from "../utils/commons";
/**
* @property diffPercent
@ -10,15 +10,14 @@ import { isEmpty, getNestedValue } from "@haina/utils-commons";
* @property data1
* @property data2
*/
export const VSTag = ({ diffPercent, diffData, ...props }) => {
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 diffData===undefined ? null : parseInt(diffPercent) === 0 ? (
<span style={{marginLeft: '0.5rem'}}>
<Tag color={tagColor}>-%{' '}<span>{diffData}</span></Tag>
</span>
return parseInt(diffPercent) === 0 ? (
'-'
) : (
<span style={{marginLeft: '0.5rem'}}>
<span>
{/* <div>
{data1} vs {data2}
</div> */}
@ -49,7 +48,7 @@ export const TableExportBtn = (props) => {
);
// .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);

@ -1,14 +0,0 @@
.quill-editor .ql-container{
font-size: 14px;
}
.quill-editor .ql-container.ql-snow {
min-height: 12rem;
border-color: #d9d9d9;
}
.quill-editor .ql-toolbar.ql-snow {
border-radius: 6px 6px 0 0;
border-color: #d9d9d9;
}
.quill-editor .ql-editor{
min-height: 12rem;
}

@ -1,144 +0,0 @@
import React, { forwardRef, useEffect, useLayoutEffect, useRef } from 'react';
import Quill from 'quill';
import 'quill/dist/quill.snow.css';
// import 'quill/dist/quill.bubble.css';
import './custom.snow.css';
function decodeEntities(str) {
const entities = {
'&amp;': '&',
'&lt;': '<',
'&gt;': '>',
'&quot;': '"',
'&#39;': "'",
'&nbsp;': ' ',
};
return str.replace(/&amp;|&lt;|&gt;|&quot;|&#39;|&nbsp;/g, (match) => entities[match]);
}
const toolbarOptions = [
[{ 'header': [1, 2, 3, 4, 5, 6, false] }],
// [{ 'font': [] }],
['bold', 'italic', 'underline', 'strike'], // toggled buttons
// ['blockquote'], // 'code-block'
['link', ], // 'image' 'video', 'formula'
[{ 'list': 'ordered' }, { 'list': 'bullet' }], // { 'list': 'check' }
[{ 'color': [] }, { 'background': [] }], // dropdown with defaults from theme
// [{ 'size': ['small', false, 'large', 'huge'] }], // custom dropdown
// [{ 'indent': '-1' }, { 'indent': '+1' }], // outdent/indent
// [{ 'align': [] }],
['clean'], // remove formatting button
];
// Editor is an uncontrolled React component
const Editor = forwardRef(({ readOnly, defaultValue, value, onChange, onTextChange, onSelectionChange }, ref) => {
const containerRef = useRef(null);
const cntContainerRef = useRef(null);
const defaultValueRef = useRef(defaultValue);
const valueRef = useRef(value);
const onTextChangeRef = useRef(onTextChange);
const onChangeRef = useRef(onChange);
const onSelectionChangeRef = useRef(onSelectionChange);
useLayoutEffect(() => {
onTextChangeRef.current = onTextChange;
onChangeRef.current = onChange;
onSelectionChangeRef.current = onSelectionChange;
});
useEffect(() => {
const editor = ref.current;
if (!editor) return;
valueRef.current = value;
const current = editor.root.innerHTML;
const incoming = value || defaultValue || '';
// treat quill empty markup as empty string
const currentNormalized = current === '<p><br></p>' ? '' : current;
// if (incoming !== currentNormalized) {
if (!currentNormalized) { // ! value
const value = valueRef.current;
const delta = editor.clipboard.convert({ html: value });
editor.setContents(delta, 'silent');
const sel = editor.getSelection && editor.getSelection();
// restore selection if possible
if (sel && sel.index != null) {
try { editor.setSelection(Math.min(sel.index, editor.getLength() - 1), sel.length || 0); } catch {}
}
}
}, [ref, defaultValue, value]);
useEffect(() => {
ref.current?.enable(!readOnly);
}, [ref, readOnly]);
useEffect(() => {
const container = containerRef.current;
const cntContainer = cntContainerRef.current;
const editorContainer = container.appendChild(container.ownerDocument.createElement('div'));
// Add fonts to whitelist
// const Font = Quill.import('formats/font');
// Font.whitelist = ['mirza', 'roboto'];
// Quill.register(Font, true);
Quill.register('modules/counter', function (quill, options) {
// quill.on('text-change', function() {
quill.on(Quill.events.TEXT_CHANGE, () => {
// const text = quill.getText();
const length = quill.getLength();
cntContainer.innerText = length; // text.length; // text.split(/\s+/).length;
});
});
const quill = new Quill(editorContainer, {
theme: 'snow',
modules: {
toolbar: toolbarOptions,
counter: true,
},
});
ref.current = quill;
// if (defaultValueRef.current) {
// quill.setContents(defaultValueRef.current);
// }
quill.on(Quill.events.TEXT_CHANGE, (...args) => {
// const text = quill.getText();
onTextChangeRef.current?.(...args);
const html = quill.getSemanticHTML();
const htmlText = decodeEntities(html);
onChangeRef.current?.(htmlText);
});
quill.on(Quill.events.SELECTION_CHANGE, (...args) => {
onSelectionChangeRef.current?.(...args);
});
return () => {
ref.current = null;
container.innerHTML = '';
};
}, [ref]);
return (
<>
<div ref={containerRef} style={{ minHeight: '12rem' }} className='quill-editor'></div>
<div style={{ border: '1px solid #ccc', borderWidth: '0px 1px 1px 1px', color: '#aaa', padding: '0 3px', textAlign: 'right', borderRadius: '0 0 6px 6px' }}>
<span ref={cntContainerRef}>0</span>&nbsp;/&nbsp;4000
</div>
</>
);
});
Editor.displayName = 'Editor';
export default Editor;

@ -1,12 +1,12 @@
import { useEffect, useState } from "react";
import { Upload, List, Button, Tooltip, Popconfirm, Col, Row } from "antd";
import { UploadOutlined, FileTextOutlined, DeleteOutlined, StopOutlined } from "@ant-design/icons";
import { Upload, List } from "antd";
import { UploadOutlined } from "@ant-design/icons";
import { Image } from "antd";
import { fetchJSON } from "@haina/utils-request";
import { fetchJSON } from "@/utils/request";
import { HT3_HOST } from "@/config";
//
export const simple_encrypt = text => {
const simple_encrypt = text => {
const key = "TPDa*UU8h5%!zS";
let encrypted = [];
let keyIndex = 0;
@ -21,15 +21,9 @@ export const simple_encrypt = text => {
};
//
const getImageList = async (key, overlist = false, ignore_case = true) => {
const getImageList = async key => {
try {
let url;
if (overlist) {
url = `${HT3_HOST}/oss/list_over_unique_key?key=${key}&ignore_case=${ignore_case}`;
} else {
url = `${HT3_HOST}/oss/list_unique_key?key=${key}&ignore_case=${ignore_case}`;
}
const { errcode, result } = await fetchJSON(url);
const { errcode, result } = await fetchJSON(`${HT3_HOST}/oss/list_unique_key?key=${key}`);
if (errcode === 0) {
return result
.map(file => ({
@ -53,9 +47,9 @@ const getImageList = async (key, overlist = false, ignore_case = true) => {
};
//
const deleteImage = async (key, ignore_case =true) => {
const deleteImage = async key => {
try {
const { errcode } = await fetchJSON(`${HT3_HOST}/oss/delete_unique_key?key=${key}&ignore_case=${ignore_case}`, {
const { errcode } = await fetchJSON(`${HT3_HOST}/oss/delete_unique_key?key=${key}`, {
method: "GET",
});
return errcode === 0;
@ -66,9 +60,9 @@ const deleteImage = async (key, ignore_case =true) => {
};
//
const getSignature = async (file, key, onSuccess, onError, ignore_case = true) => {
const getSignature = async (file, key, onSuccess, onError) => {
try {
const { errcode, result } = await fetchJSON(`${HT3_HOST}/oss/signature_unique_key?key=${key}&filename=${file.name}&ignore_case=${ignore_case}`);
const { errcode, result } = await fetchJSON(`${HT3_HOST}/oss/signature_unique_key?key=${key}&filename=${file.name}`);
if (errcode === 0) {
const { method, host, signed_headers } = result;
const response = await fetch(host, {
@ -88,19 +82,18 @@ const getSignature = async (file, key, onSuccess, onError, ignore_case = true) =
}
};
export const ImageUploader = props => {
const ImageUploader = props => {
const [fileList, setFileList] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState("");
const key = simple_encrypt(props.osskey);
const ignore_case = props.ignore_case;
//
useEffect(() => {
const loadImages = async () => {
setIsLoading(true);
const images = await getImageList(key, false, ignore_case);
const images = await getImageList(key);
setFileList(images);
if (props.onChange) {
//
@ -116,17 +109,16 @@ export const ImageUploader = props => {
//
const handleDelete = async file => {
const success = await deleteImage(file.encrypt_key, ignore_case);
const success = await deleteImage(file.encrypt_key);
if (success) {
const newImages = fileList.filter(item => item.encrypt_key !== file.encrypt_key);
if (props.onChange) {
props.onChange(newImages);
}
props.handleSaveDatabase(newImages);
setFileList(newImages);
//console.log("");
console.log("删除成功");
} else {
//console.error("");
console.error("删除失败");
}
};
@ -140,11 +132,10 @@ export const ImageUploader = props => {
file,
key,
(response, file) => {
getImageList(key, false, ignore_case).then(newImages => {
getImageList(key).then(newImages => {
if (props.onChange) {
props.onChange(newImages);
}
props.handleSaveDatabase(newImages);
setFileList(prevList => {
//
const index = prevList.findIndex(item => item.status === "uploading");
@ -161,8 +152,7 @@ export const ImageUploader = props => {
});
});
},
onError,
ignore_case
onError
);
};
@ -174,41 +164,12 @@ export const ImageUploader = props => {
setPreviewOpen(true);
};
const handleRemove = () => {
return false;
};
return (
<>
<Upload
customRequest={handleUploadFile}
multiple={true}
onRemove={handleRemove}
listType="picture-card"
fileList={fileList}
onPreview={handlePreview}
onChange={handleChange}
showUploadList={{
showDownloadIcon: true,
showRemoveIcon: props.deletable,
removeIcon: file => {
return (
<Popconfirm
title="Delete"
description="Are you sure you want to delete the file?"
onConfirm={() => {
handleDelete(file);
}}
onCancel={() => setFileList([...fileList])}
okText="Yes"
cancelText="No">
<DeleteOutlined />
</Popconfirm>
)
},
}}>
<Upload customRequest={handleUploadFile} multiple={true} onRemove={handleDelete} listType="picture-card" fileList={fileList} onPreview={handlePreview} onChange={handleChange}>
<div>
<UploadOutlined />
<div style={{ marginTop: 8 }}>Select File</div>
<div style={{ marginTop: 8 }}>上传图片</div>
</div>
</Upload>
<List loading={isLoading} dataSource={fileList} />
@ -227,53 +188,4 @@ export const ImageUploader = props => {
);
};
export const ImageViewer = props => {
const [fileList, setFileList] = useState([]);
const key = props.osskey;
const overlist = props.overlist;
const ignore_case = props.ignore_case;
//
useEffect(() => {
const loadImages = async () => {
const images = await getImageList(key, overlist,ignore_case);
setFileList(images);
if (props.onChange) {
//
props.onChange(images);
}
};
if (key) {
loadImages();
}
}, [key]);
return (
<>
<Image.PreviewGroup>
<Row gutter={[20, 20]}>
{fileList &&
fileList.map(item => {
return (
<Col key={item.encrypt_key}>
{item.key.match(/\.(jpg|jpeg|png|gif|bmp|webp)$/i) ? (
<Image width={200} src={item.url} />
) : (
<a href={item.url} download>
<Button type="primary" icon={<FileTextOutlined />} size="large" title={item.key.replace(/^.*[\\\/]/, "")}>
...{item.key.slice(-10)}
</Button>
</a>
)}
</Col>
);
})}
</Row>
</Image.PreviewGroup>
</>
);
};
export default ImageUploader;

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

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

@ -3,7 +3,7 @@ import { useProductsTypes } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
const ProductsTypesSelector = ({...props}) => {
const productsTypes = useProductsTypes(true);
const productsTypes = useProductsTypes();
const { t } = useTranslation();
return (
<>

@ -1,18 +1,17 @@
import { useEffect } from "react";
import { Form, Input, Row, Col, Select, DatePicker, Space, Button, Checkbox, Tag } from "antd";
import { objectMapper, at, isEmpty } from "@haina/utils-commons";
import { DATE_FORMAT, SMALL_DATETIME_FORMAT, PERM_AGENCY_ALL } from "@/config";
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 useAuthStore from '@/stores/Auth'
import { SearchOutlined } from '@ant-design/icons';
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";
import { InfoCircleOutlined } from "@ant-design/icons";
const { RangePicker } = DatePicker;
@ -21,28 +20,17 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
const presets = useDatePresets();
const [formValues, setFormValues] = useFormStore(state => [state.formValues, state.setFormValues]);
const [formValuesToSub, setFormValuesToSub] = useFormStore(state => [state.formValuesToSub, state.setFormValuesToSub]);
const [isPermitted, {travelAgencyName: myAgencyName, travelAgencyId: myAgencyId}] = useAuthStore((state) => [state.isPermitted, state.currentUser])
const [form] = Form.useForm();
// , ;
const showVendorSwitcher = isPermitted(PERM_AGENCY_ALL);
const hasAgencyField = (props.fieldsConfig?.shows || []).includes('agency');
const defaultShowFields = ['agency'];
const agencyFieldsProps = hasAgencyField ? {} : { agency: { col: 24, hidden: !showVendorSwitcher }, };
const agencyFieldsComProps = hasAgencyField ? {} : { agency: { mode: null, allowClear: false }, };
const agencyInitialValue = hasAgencyField ? {} : { agency: { key: myAgencyId, value: myAgencyId, label: myAgencyName }};
const keepSelectedAgency = isEmpty(formValues?.agency) ? agencyInitialValue : { agency: formValues.agency } ;
const { sort, hides, shows, fieldProps, fieldComProps } = {
sort: "",
// initialValue: '',
fieldProps: "",
fieldComProps: "",
hides: [],
shows: [],
...props.fieldsConfig,
shows: [...defaultShowFields, ...(props.fieldsConfig?.shows || [])],
fieldProps: { ...agencyFieldsProps, ...(props.fieldsConfig?.fieldProps || {}) },
fieldComProps: { ...agencyFieldsComProps, ...(props.fieldsConfig?.fieldComProps || {}) },
};
const readValues = hasAgencyField ? { ...agencyInitialValue, ...initialValue, ...formValues } : { ...initialValue, ...formValues, ...keepSelectedAgency };
const readValues = { ...initialValue, ...formValues };
const formValuesMapper = values => {
const destinationObject = {
@ -55,20 +43,15 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
{ key: "starttime", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "endtime", transform: arrVal => (arrVal ? arrVal[1].format(SMALL_DATETIME_FORMAT) : "") },
],
datesDiff: [
{ key: "datesDiff1", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "datesDiff2", transform: arrVal => (arrVal ? arrVal[1].endOf('month').format(SMALL_DATETIME_FORMAT) : "") },
{ key: "timeDiff1", transform: arrVal => (arrVal ? arrVal[0].format(DATE_FORMAT) : "") },
{ key: "timeDiff2", 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 : ""; }, },
{ key: "veisn", transform: value => { return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : ""; }, },
{ key: "VEI_SN", transform: value => { return Array.isArray(value) ? value.map(ele => ele.key).join(",") : value ? value.value : ""; }, },
],
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",
@ -92,10 +75,9 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
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) },
approvalStatus: { key: "approvalStatus", transform: value => value },
};
let dest = {};
const { dates, datesDiff, ...omittedValue } = values;
const { dates, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
@ -120,9 +102,9 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
}, []);
const onFinish = values => {
console.log("Received values of form, origin form value: \n", values);
//console.log("Received values of form, origin form value: \n", values);
const dest = formValuesMapper(values);
console.log("form value send to onSubmit:\n", dest);
//console.log("form value send to onSubmit:\n", dest);
const str = new URLSearchParams(dest).toString();
setFormValues(values);
setFormValuesToSub(dest);
@ -159,7 +141,7 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
{/* 'textAlign': 'right' */}
<Col flex="1 0 90px" className="flex justify-normal items-start">
<Space align="center">
<Button size={"middle"} type="primary" htmlType="submit" icon={<SearchOutlined />} loading={loading}>
<Button size={"middle"} type="primary" htmlType="submit" loading={loading}>
{confirmText || t("Search")}
</Button>
{/* <Button size="small" onClick={onReset}>
@ -175,7 +157,7 @@ const SearchForm = ({ initialValue, onSubmit, onReset, onMounted, confirmText, f
};
function getFields(props) {
const { fieldProps, fieldComProps, form, presets, t, shows } = props;
const { fieldProps, fieldComProps, form, presets, t } = props;
const bigCol = 4 * 2;
const midCol = 6;
const layoutProps = {
@ -260,19 +242,10 @@ function getFields(props) {
item(
"dates", //
99,
<Form.Item label={t("group:ArrivalDate")} {...fieldProps.dates} >
<>
<Form.Item noStyle name={"dates"} label={t("group:ArrivalDate")} {...fieldProps.dates} initialValue={at(props, "initialValue.dates")[0]}>
<RangePicker key='date1' allowClear={true} inputReadOnly={true} presets={presets} placeholder={["From", "Thru"]} {...fieldComProps.dates} />
</Form.Item>
{ fieldProps?.dates?.hide_vs === false &&
<><Form.Item noStyle name={'datesDiff'} label={t("common:Compare")} title={t("common:Compare")} >
<RangePicker key='date2' allowClear={true} inputReadOnly={true} presets={presets} placeholder={["Compare", ""]} variant={'underlined'} {...fieldComProps.dates} title={t("common:Compare")} />
</Form.Item>
<div><InfoCircleOutlined /> Comparison data is shown as: <Tag color={'lime'} >{'Delta% {Data}'}</Tag></div>
</>}
</>
</Form.Item>,
<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(
@ -281,7 +254,7 @@ function getFields(props) {
<Form.Item name="username" label={t("account:username")} {...fieldProps.username}>
<Input placeholder={t("account:username")} allowClear />
</Form.Item>,
fieldProps?.username?.col || 6
fieldProps?.username?.col || 4
),
/**
*
@ -296,9 +269,9 @@ function getFields(props) {
),
item(
"agency", //
98, //
99,
<Form.Item name="agency" label={t("products:Vendor")} {...fieldProps.agency} initialValue={at(props, "initialValue.agency")[0]}>
<VendorSelector maxTagCount={1} {...fieldComProps.agency} />
<VendorSelector {...fieldComProps.agency} />
</Form.Item>,
fieldProps?.agency?.col || 6
),
@ -375,21 +348,6 @@ function getFields(props) {
</Form.Item>,
fieldProps?.unconfirmed?.col || 2
),
item(
"approvalStatus", //
99,
<Form.Item name="approvalStatus" label="Approval Status" initialValue={at(props, "initialValue.approvalStatus")[0] || ' '}>
<Select
options={[
{ value: ' ', label: "All" },
{ value: 136001, label: "Pending Review" },
{ value: 136002, label: "Approved" },
{ value: 136003, label: "Rejected" },
]}
/>
</Form.Item>,
fieldProps?.approvalStatus?.col || 6
),
];
baseChildren = baseChildren
.map(x => {

@ -1,49 +1,29 @@
import React, { useMemo, useRef, useState, useEffect } from 'react';
import React, { useMemo, useRef, useState } from 'react';
import { Select, Spin } from 'antd';
import { debounce, objectMapper } from '@haina/utils-commons';
import { debounce, objectMapper } from '@/utils/commons';
function DebounceSelect({ fetchOptions, debounceTimeout = 500, initLoad = false, defaultOptions=[], onFetch=null, ...props }) {
function DebounceSelect({ fetchOptions, debounceTimeout = 800, ...props }) {
const [fetching, setFetching] = useState(false);
const [options, setOptions] = useState(defaultOptions);
const [options, setOptions] = useState([]);
const fetchRef = useRef(0);
//
useEffect(() => {
if (defaultOptions.length!==0) setOptions(defaultOptions);
if (initLoad && defaultOptions.length===0) {
loadOptions(' ');
}
}, [initLoad, defaultOptions]);
const loadOptions = (value) => {
fetchRef.current += 1;
const fetchId = fetchRef.current;
const currentFilter = options.filter(option => (option?.label ?? '').toLowerCase().includes(value.toLowerCase()))
if (currentFilter.length!==0) { setOptions(currentFilter); return; }
else if (value) setOptions([]);
// if (value) 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);
onFetch && onFetch(mapperOptions);
});
};
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]);
const onChange = (v) => {
props.onChange && props.onChange(v);
setOptions(defaultOptions);
}
return (
<Select
labelInValue
@ -51,16 +31,18 @@ function DebounceSelect({ fetchOptions, debounceTimeout = 500, initLoad = false,
showSearch
allowClear
maxTagCount={1}
loading={fetching}
// dropdownStyle={{width: '20rem'}}
styles={{ root: { width: 'min(20rem, 100%)' }, popup: { root: { width: '20rem' } } }}
dropdownStyle={{width: '20rem'}}
{...props}
onChange={onChange}
onSearch={debounceFetcher}
notFoundContent={fetching ? <Spin size='small' /> : null}
optionFilterProp='label'
options={options}
/>
>
{options.map((d) => (
<Select.Option key={d.value} title={d.label}>
{d.label}
</Select.Option>
))}
</Select>
);
}

@ -1,52 +1,27 @@
import { createContext, useEffect, useState } from 'react';
import {} from 'antd';
import SearchInput from './SearchInput';
import { fetchJSON } from '@haina/utils-request';
import { fetchJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { useTranslation } from 'react-i18next';
import useFormStore from "@/stores/Form";
import { usingStorage } from "@/hooks/usingStorage";
import useAuthStore from '@/stores/Auth'
import { isEmpty, objectMapper } from '@haina/utils-commons';
const mapper = { travel_agency_name: 'label', travel_agency_id: 'value' };
/**
* 供应商列表
*/
const fetchVendorList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q, useweb: -1 });
//
export const fetchVendorList = async (q) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q });
return errcode !== 0 ? [] : result;
};
const fetchVendorListDefault = async (q = '') => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/VendorList`, { q, useweb: 1 });
return errcode !== 0 ? [] : result.map(ele => objectMapper(ele, mapper));
};
const VendorSelector = ({ ...props }) => {
const { t } = useTranslation();
const [{ vendorList }, setCache] = useFormStore(state => [state.cache, state.setCache]);
const [vendorList0, setVendorList0] = useFormStore(state => [state.vendorList0, state.setVendorList0]);
useEffect(() => {
if (isEmpty(vendorList0)) {
fetchVendorListDefault().then(res => setVendorList0(res));
}
}, []);
return (
<>
<SearchInput initLoad={false}
<SearchInput
placeholder={t('products:Vendor')}
labelInValue
mode={'multiple'}
maxTagCount={0}
{...props}
fetchOptions={fetchVendorList}
map={mapper}
onFetch={(v) => setCache({ vendorList: v })}
defaultOptions={vendorList0}
map={{ travel_agency_name: 'label', travel_agency_id: 'value' }}
/>
</>
);

@ -3,7 +3,7 @@ export const PROJECT_NAME = "GHHub";
// mode: test内部测试使用
export const HT_HOST = import.meta.env.MODE === 'test' ? 'http://120.79.9.217:10024' : import.meta.env.PROD ? 'https://p9axztuwd7x8a7.mycht.cn' : 'http://202.103.68.144:890'
export const HT3_HOST = 'https://hub.globalhighlights.com/ht3.0'
export const TGA_HOST = import.meta.env.PROD ? 'https://discovery.mycht.cn' : 'http://discovery.mycht.local'
export const OVERSEA_HOST = 'https://ht20-p9axztuwd7x8a7.mycht.cn'
export const DATE_FORMAT = "YYYY-MM-DD";
@ -12,31 +12,20 @@ export const SMALL_DATETIME_FORMAT = "YYYY-MM-DD 23:59";
export const OFFICEWEBVIEWERURL = "https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src=";
const __BUILD_VERSION__ = `__BUILD_VERSION__`.replace(/"/g, '')
const __BUILD_DATE__ = `__BUILD_DATE__`;
const __GIT_HEAD__ = `__GIT_HEAD__`
export const BUILD_VERSION = import.meta.env.PROD ? __BUILD_VERSION__ : import.meta.env.MODE;
export const BUILD_DATE = import.meta.env.PROD ? __BUILD_DATE__ : new Date().toLocaleString();
export const GIT_HEAD = import.meta.env.PROD ? __GIT_HEAD__ : 'current';
// 权限常量定义
// 账号、权限管理
// category: system
export const PERM_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'
export const PERM_AGENCY_ALL = '/agency/all' // 可以搜索所有供应商
// 海外供应商
// category: oversea
export const PERM_OVERSEA = '/oversea/all' // @Deprecated 准备作废...
export const PERM_RESERVATION_ALL = '/reservation/all'
export const PERM_INVOICE_ALL = '/invoice/all'
export const PERM_FEEDBACK_ALL = '/feedback/all'
export const PERM_REPORT_ALL = '/report/all'
export const PERM_REVIEW_ALL = '/review/all'
export const PERM_OVERSEA = '/oversea/all'
// 国内供应商
// category: domestic
@ -53,11 +42,7 @@ export const PERM_TRAIN_TICKET = '/train-ticket/all'
// 价格管理
export const PERM_PRODUCTS_MANAGEMENT = '/products/*'; // 管理
export const PERM_PRODUCTS_NEW = '/products/new'; // 新增产品
// export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核 @deprecated
export const PERM_PRODUCTS_INFO_AUDIT = '/products/info/audit'; // 信息.审核
export const PERM_PRODUCTS_INFO_PUT = '/products/info/put'; // 信息.录入
export const PERM_PRODUCTS_OFFER_AUDIT = '/products/offer/audit'; // 价格.审核
export const PERM_PRODUCTS_OFFER_PUT = '/products/offer/put'; // 价格.录入
// 公告管理
export const PERM_NOTIFS_MANAGEMENT = '/notifs/management'
export const PERM_NOTIFS_DELETE_DANGER = '/notifs/danger/delete'

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

@ -2,7 +2,7 @@ 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 '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
/**
* 产品管理 相关的预设数据
@ -70,9 +70,6 @@ export const useProductsTypesMapVal = (value) => {
return stateMapVal;
};
/**
* 价格的审核状态
*/
export const useProductsAuditStates = () => {
const [types, setTypes] = useState([]);
const { t, i18n } = useTranslation();
@ -92,29 +89,27 @@ export const useProductsAuditStates = () => {
return types;
};
export const useProductsAuditStatesMapVal = () => {
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', 'city_list'], ['title']];
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 subTypeD = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sub_type_D'] : [];
const sortOrder = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? ['sort_order'] : [];
const infoTypesMap = {
'6': [[...infoDisplay], []],
'B': [['km', ...infoDisplay], []],
'J': [[...infoRecDisplay, 'duration', ...infoDisplay, ...subTypeD, ...sortOrder], ['description']],
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay, ...subTypeD, ...sortOrder], ['description']],
'D': [[...infoRecDisplay, 'duration', ...infoDisplay, ...subTypeD, ...sortOrder], ['description']],
'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], []],
@ -156,11 +151,7 @@ export const useNewProductRecord = () => {
'lastedit_changed': '',
'create_date': '',
'created_by': '',
'edit_status': 2, // 信息的审核状态 1已发布2未发布 ~~已审核~~
'sort_order': '',
'sub_type_D': '', // 包价类型, 值保存在`item_type`字段中
'item_type': '', // 产品子类型的值
'city_list': [],
'edit_status': 2,
},
lgc_details: [
{
@ -191,33 +182,3 @@ export const useNewProductRecord = () => {
],
};
};
export const PackageTypes = [
{ key: '35001', value: '35001', label: '飞机接送' },
{ key: '35002', value: '35002', label: '车站接送' },
{ key: '35003', value: '35003', label: '码头接送' },
{ key: '35004', value: '35004', label: '一天游' },
{ key: '35005', value: '35005', label: '半天游' },
{ key: '35006', value: '35006', label: '夜间活动' },
{ key: '35007', value: '35007', label: '大车游' },
{ key: '35008', value: '35008', label: '单车单导' },
{ key: '35009', value: '35009', label: '单租车' },
{ key: '35010', value: '35010', label: '单导游' },
{ key: '35011', value: '35011', label: '火车站接送' },
{ key: '35012', value: '35012', label: '门票预定' },
{ key: '35013', value: '35013', label: '车导费' },
{ key: '35014', value: '35014', label: '其它(餐补等)' },
];
export const formatGroupSize = (min, max, suffix = false) => {
const minSize = min === 0 ? 1 : min;
// const rangeStr = max === min ? `${minSize}` : `${minSize}-${max}`;
const rangeStr = `${minSize}-${max}`;
return max === 1000 ? minSize <= 1 ? '不分人等' : `${minSize}人以上` : (`${rangeStr}`+(suffix ? '人' : ''));
};
export const formatGroupSizeRender = (min, max, suffix = false) => {
const minSize = min === 0 ? 1 : min;
const rangeStr = max === min ? `${minSize}` : `${minSize}-${max}`;
return max === 1000 ? minSize <= 1 ? '不分人等' : `${minSize}人以上` : (`${rangeStr}`+(suffix ? '人' : ''));
};

@ -17,17 +17,6 @@ export function usingStorage() {
}
}
const getValue = (key) => {
if (window.localStorage) {
return window.localStorage.getItem(key)
} else if (window.sessionStorage) {
return window.sessionStorage.getItem(key)
} else {
console.error('browser not support localStorage and sessionStorage.')
return ''
}
}
const setProperty = (key, value) => {
const webStorage = getStorage()
const typeAndKey = key.split(':')
@ -86,7 +75,6 @@ export function usingStorage() {
return {
...persistObject,
getValue,
setStorage: (key, value) => {
setProperty(key, value)
},

@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { Dropdown } from 'antd';
import { useTranslation } from 'react-i18next';
import { appendRequestParams } from '@haina/utils-request';
import { appendRequestParams } from '@/utils/request';
const i18n_to_htcode = {
'zh': 2,

@ -20,18 +20,13 @@ 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 NotifsIndex from '@/views/notifs/Index'
import NotifsEdit from '@/views/notifs/Edit'
import NotifsDetail from '@/views/notifs/Detail'
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 ReviewList from '@/views/external-reviews/review-list'
import Airticket from '@/views/airticket/Index'
import AirticketPlan from '@/views/airticket/Plan'
import AirticketInvoice from '@/views/airticket/Invoice'
@ -45,21 +40,12 @@ import TrainticketInvoicePaid from '@/views/trainticket/invoicePaid'
import { ThemeContext } from '@/stores/ThemeContext'
import { usingStorage } from '@/hooks/usingStorage'
import useAuthStore from './stores/Auth'
import { isNotEmpty } from '@haina/utils-commons'
import { isNotEmpty } from '@/utils/commons'
import ProductsManage from '@/views/products/Manage';
import ProductsDetail from '@/views/products/Detail';
import ProductsAudit from '@/views/products/Audit';
import ImageViewer from '@/views/ImageViewer';
import CustomerImageViewer from '@/views/CustomerImageViewer';
import AkamaiLog from '@/views/AkamaiLog';
import PickYear from './views/products/PickYear'
import { BUILD_VERSION, PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW,
PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT,
PERM_RESERVATION_ALL, PERM_FEEDBACK_ALL, PERM_INVOICE_ALL, PERM_REPORT_ALL, PERM_REVIEW_ALL, PERM_PRODUCTS_INFO_PUT
} from '@/config'
import { appendRequestHeader } from '@haina/utils-request';
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'
@ -70,50 +56,42 @@ const initRouter = async () => {
element: <App />,
errorElement: <ErrorPage />,
children: [
{ index: true, element: <NotifsIndex /> },
{ 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_RESERVATION_ALL} result={true}><ReservationNewest /></RequireAuth>},
{ path: 'reservation/:reservationId', element: <RequireAuth subject={PERM_RESERVATION_ALL} result={true}><ReservationDetail /></RequireAuth>},
//
{ path: 'feedback', element: <RequireAuth subject={PERM_FEEDBACK_ALL} result={true}><FeedbackIndex /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: <RequireAuth subject={PERM_FEEDBACK_ALL} result={true}><FeedbackCustomerDetail /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:RefNo', element: <RequireAuth subject={PERM_FEEDBACK_ALL} result={true}><FeedbackDetail /></RequireAuth>},
//
{ path: 'report', element: <RequireAuth subject={PERM_REPORT_ALL} result={true}><ReportIndex /></RequireAuth>},
//
{ path: 'notice', element: <NotifsIndex />},
{ path: 'notice/edit/:CCP_BLID', element: <NotifsEdit />},
{ path: 'notice/:CCP_BLID', element: <NotifsDetail />},
//
{ path: 'invoice',element:<RequireAuth subject={PERM_INVOICE_ALL} result={true}><InvoiceIndex /></RequireAuth>},
{ path: 'invoice/detail/:GMDSN/:GSN/:VEI',element:<RequireAuth subject={PERM_INVOICE_ALL} result={true}><InvoiceDetail /></RequireAuth>},
{ path: 'invoice/history/:GMDSN/:GSN/:VEI',element:<RequireAuth subject={PERM_INVOICE_ALL} result={true}><InvoiceHistory /></RequireAuth>},
{ path: 'invoice/paid',element:<RequireAuth subject={PERM_INVOICE_ALL} result={true}><InvoicePaid /></RequireAuth>},
{ path: 'invoice/paid/detail/:flid', element: <RequireAuth subject={PERM_INVOICE_ALL} result={true}><InvoicePaidDetail /></RequireAuth>},
// External Reviews
{ path: 'external-reviews', element: <RequireAuth subject={PERM_REVIEW_ALL} result={true}><ReviewList /></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_INFO_PUT} result={true}><ProductsDetail /></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_INFO_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/pick-year",element: <RequireAuth subject={PERM_PRODUCTS_INFO_PUT} result={true}><PickYear /></RequireAuth>},
//
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
]
},
{
@ -121,9 +99,6 @@ const initRouter = async () => {
children: [
{ path: '/login', element: <Login /> },
{ path: '/logout', element: <Logout /> },
{ path: '/image-viewer/:GRI_SN/:GRI_No', element: <ImageViewer /> },
{ path: '/customer-image/:key', element: <CustomerImageViewer /> },
{ path: '/akamai-log', element: <AkamaiLog /> },
]
}
])
@ -131,8 +106,6 @@ const initRouter = async () => {
const initAppliction = async () => {
appendRequestHeader('X-Web-Version', BUILD_VERSION);
const { loginToken, userId } = usingStorage()
if (isNotEmpty(userId) && isNotEmpty(loginToken)) {
@ -148,9 +121,6 @@ const initAppliction = async () => {
//<React.StrictMode>
<ThemeContext.Provider value={{ colorPrimary: '#00b96b', borderRadius: 4 }}>
<RouterProvider
future={{
v7_startTransition: true,
}}
router={router}
fallbackElement={() => <div>Loading...</div>}
/>

@ -1,23 +1,51 @@
import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } from '@/config';
import { fetchJSON } from '@haina/utils-request'
import { usingStorage } from "@/hooks/usingStorage";
export const sendNotify = async (message) => {
export const loadPageSpy = (title) => {
const { userId, travelAgencyId } = usingStorage();
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup';
if (import.meta.env.DEV || window.$pageSpy) return
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: `${message}\\n\\nID: ${userId}, ${travelAgencyId} | ${PROJECT_NAME} (${BUILD_VERSION})`,
};
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}`,
];
return fetchJSON(notifyUrl, params).then((json) => {
if (json.errcode === 0) {
console.info('发送通知成功');
} else {
throw new Error(json?.errmsg + ': ' + json.errcode);
}
Promise.all(PageSpySrc.map((src) => loadScript(src))).then(() => {
//
window.$harbor = new DataHarborPlugin();
window.$rrweb = new RRWebPlugin();
[window.$harbor, window.$rrweb].forEach(p => {
PageSpy.registerPlugin(p)
})
window.$pageSpy = new PageSpy(PageSpyConfig);
});
};
export const uploadPageSpyLog = async () => {
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
await window.$harbor.upload() // { clearCache: true, remark: '' }
alert('Success')
} else {
alert('Failure')
}
}
export const PageSpyLog = () => {
return (
<>
{window.$pageSpy && (
<a
className='text-primary'
onClick={() => {
window.$pageSpy.triggerPlugins('onOfflineLog', 'download');
window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
}}>
上传Debug日志 ({window.$pageSpy.address.substring(0, 4)})
</a>
)}
</>
);
};

@ -1,7 +1,7 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request'
import { isEmpty, isNotEmpty } from '@haina/utils-commons'
import { fetchJSON, postForm } from '@/utils/request'
import { isEmpty, isNotEmpty } from '@/utils/commons'
import { HT_HOST } from "@/config"
import { usingStorage } from '@/hooks/usingStorage'
@ -188,4 +188,4 @@ const useAccountStore = create(devtools((set) => ({
},
}), { name: 'accountStore' }))
export default useAccountStore
export default useAccountStore

@ -1,6 +1,6 @@
import { create } from "zustand";
import { fetchJSON, postForm } from "@haina/utils-request";
import { prepareUrl, isNotEmpty } from "@haina/utils-commons";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST, DATE_FORMAT } from "@/config";
import dayjs from "dayjs";

@ -1,15 +1,13 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { appendRequestParams, fetchJSON, postForm } from '@haina/utils-request'
import { HT_HOST, PROJECT_NAME } from "@/config"
import { loadPageSpy } from '@haina/utils-pagespy'
import { appendRequestParams, fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config"
import { loadPageSpy } from '@/pageSpy'
import { usingStorage } from '@/hooks/usingStorage'
import { isEmpty } from "@haina/utils-commons";
const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN'
const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID'
const KEY_USER_ID = 'G-INT:USER_ID'
const KEY_I18N = 'i18nextLng'
const WILDCARD_TOKEN = '*'
@ -39,7 +37,14 @@ export const fetchPermissionListByUserId = async (userId) => {
return errcode !== 0 ? {} : result
}
// 取消令牌时间过期检测,待删除
async function fetchLastRequet() {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/GetLastReqDate`)
return errcode !== 0 ? {} : result
}
const initialState = {
tokenInterval: null,
loginStatus: 0,
defaltRoute: '',
currentUser: {
@ -48,7 +53,6 @@ const initialState = {
rolesName: '',
emailAddress: '',
travelAgencyName: '',
travelAgencyId: '',
},
permissionList: []
}
@ -59,9 +63,7 @@ const useAuthStore = create(devtools((set, get) => ({
initAuth: async () => {
const { loadUserPermission } = get()
const { setStorage, getValue, loginToken } = usingStorage()
const language = getValue(KEY_I18N)
appendRequestParams("lgc", language === "zh" ? 2 : 1)
const { setStorage, loginToken } = usingStorage()
// Dev 模式使用 localStorage会有 token 失效情况,需要手动删除
// Prod 环境没有该问题
@ -78,15 +80,14 @@ const useAuthStore = create(devtools((set, get) => ({
set(() => ({
currentUser: {
username: userJson.LoginName,
realname: isEmpty(userJson.real_name) ? userJson.LoginName : userJson.real_name,
realname: userJson.real_name,
rolesName: userJson.roles_name,
emailAddress: userJson.LMI_listmail,
travelAgencyName: isEmpty(userJson.VName) ? userJson.LMI_VEI_SN : userJson.VName,
travelAgencyId: userJson.LMI_VEI_SN
travelAgencyName: userJson.VName,
}
}))
loadPageSpy(`${userJson.LoginName}-${userJson.VName}`, PROJECT_NAME, false)
loadPageSpy(`${userJson.real_name}-${userJson.VName}`)
},
authenticate: async (usr, pwd) => {
@ -124,9 +125,10 @@ const useAuthStore = create(devtools((set, get) => ({
},
logout: () => {
const { currentUser } = get()
const { tokenInterval, currentUser } = get()
const { clearStorage } = usingStorage()
clearStorage()
clearInterval(tokenInterval)
set(() => ({
...initialState,
currentUser: {
@ -173,16 +175,6 @@ const useAuthStore = create(devtools((set, get) => ({
})
},
// 根据某项数据来判断是否有权限
//
// INSERT INTO [dbo].[auth_resource] ([res_name] ,[res_pattern], [res_category])
// VALUES ('审核CH直销产品', '[125, 375]', 'data')
//
// const PERM_PRODUCTS_AUDIT_CH = '[125, 375]'
isAllowed: (perm, data) => {
return true
},
}), { name: 'authStore' }))
export default useAuthStore

@ -1,82 +0,0 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request'
import { DATE_FORMAT, TGA_HOST } from "@/config"
import dayjs from "dayjs";
export const fetchAllItinerary = async (reservationId) => {
const { errcode, result } = await fetchJSON(
`${TGA_HOST}/api/index.php/oversea/all_itinerary/`,
{ group_id: reservationId })
return errcode !== 0 ? {} : result
}
export const fetchTransport = async (reservationId) => {
const { errcode, result } = await fetchJSON(
`${TGA_HOST}/api/index.php/oversea/transport/`,
{ group_id: reservationId })
return errcode !== 0 ? {} : result
}
export const fetchRecentGroup = async (travelAgencyId) => {
const { errcode, result } = await fetchJSON(
`${TGA_HOST}/api/index.php/plan/get_recent_group/`,
{ travel_agency_id: travelAgencyId })
return errcode !== 0 ? {} : result
}
export const initialState = {
review: {
"reviewId": undefined,
"reviewLink": '',
"datePosted": undefined,
"customerId": '',
"referenceId": null,
"guideId": null,
"cityId": null
}
};
const useExternalReviewStore = create(devtools((set, get) => ({
reviewList: [],
fetchReviewList: async (formValues, travelAgencyId) => {
const { errcode, errmsg, result } = await fetchJSON(`${TGA_HOST}/api/index.php/customer_review/search/`,
{ agent_id: travelAgencyId, group_number: formValues?.referenceNo??'',
from_date: formValues?.startdate??'', thru_date: formValues?.enddate??'',
approval_status: formValues?.approvalStatus??''
})
if (errcode !== 0) {
return Promise.reject(new Error(errmsg + ': ' + errcode))
} else {
set(() => ({
reviewList: result
}))
return Promise.resolve(result)
}
},
postReview: (formValues, travelAgencyId) => {
const formData = new FormData()
formData.append('agent_id', travelAgencyId)
formData.append('review_id', formValues.reviewId??'')
formData.append('review_link', formValues.reviewLink)
formData.append('date_posted', dayjs(formValues.datePosted).format(DATE_FORMAT))
formData.append('customer_id', formValues.customerId)
formData.append('reference_id', formValues.referenceId)
formData.append('guide_id', formValues.guideId)
formData.append('city_id', formValues.cityId)
return postForm(`${TGA_HOST}/api/index.php/customer_review/agent_submit/`, formData)
},
}), { name: 'externalReviewStore' }))
export default useExternalReviewStore

@ -1,5 +1,5 @@
import { fetchJSON, postForm , postJSON} from '@haina/utils-request';
import { groupBy } from '@haina/utils-commons';
import { fetchJSON, postForm } from '@/utils/request';
import { groupBy } from '@/utils/commons';
import * as config from '@/config';
import dayjs from 'dayjs';
@ -8,21 +8,6 @@ import { devtools } from 'zustand/middleware';
const { HT_HOST } = config;
/*
GRI_SN 团SN
travelAgencyId 供应商id
*/
export const save_database = async (GRI_SN, travelAgencyId, file) => {
try {
await postJSON(`/ht3-multimodal/multimodal/overseas_image_records`, {
VPI_GRI_SN: parseInt(GRI_SN),
VPI_VEI_SN: parseInt(travelAgencyId),
VPI_PicNum: file.length
});
} catch (error) {
console.error("保存数据库失败:", error);
}
}
/*
GRI_SN 团SN
VEI_SN 供应商SN
@ -125,7 +110,7 @@ const useFeedbackStore = create(
const allGroup = groupBy(_result, 'EOI_GRI_SN');
const filterV = Object.keys(allGroup).reduce((r, gsn) => {
const v2 = allGroup[gsn].filter((v) => v.EOI_CII_SN);
const withAllGuide = allGroup[gsn].map((row) => ({ ...row, CityGuide: row.GriName.map((rg) => `${rg.GuideCity}: ${rg.GuideName}`).join(' ; ') }));
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);

@ -1,20 +1,12 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { usingStorage } from '@/hooks/usingStorage'
export const useFormStore = create(
devtools((set, get) => ({
formValues: {},
setFormValues: (values) => set((state) => ({ formValues: { ...state.formValues, ...values } })),
formValuesToSub: { agency: usingStorage().travelAgencyId },
formValuesToSub: {},
setFormValuesToSub: (values) => set((state) => ({ formValuesToSub: { ...state.formValuesToSub, ...values } })),
vendorList0: [],
setVendorList0: (values) => set((state) => ({ vendorList0: values })),
cache: { },
setCache: (values) => set((state) => ({ cache: { ...state.cache, ...values } })),
}), { name: 'formStore' })
);
export default useFormStore;

@ -1,4 +1,4 @@
import { fetchJSON, postForm } from "@haina/utils-request";
import { fetchJSON, postForm } from "@/utils/request";
import { HT_HOST } from "@/config";
import { create } from 'zustand';

@ -1,10 +1,9 @@
import dayjs from 'dayjs';
import * as config from "@/config";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import { fetchJSON, postJSON } from '@haina/utils-request';
import { fetchJSON, } from '@/utils/request';
const { HT_HOST } = config;
/**
@ -26,56 +25,6 @@ export const fetchNoticeDetail = async (LMI_SN, CCP_BLID) => {
return errcode !== 0 ? {} : Result;
}
/**
* Notification 相关的请求 `notifs`
* -- 客服端
*/
export const fetchNotifsList = async (params={}) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/notifications_list`, params);
return errcode !== 0 ? [] : result.map(ele => ({...ele, notificationsReadState: 1, date: dayjs(ele.LastEditTime || ele.CreateDate).format('YYYY-MM-DD')}));
};
export const fetchNotifsDetail = async (notificationId) => {
const result = await fetchNotifsList({ notificationId });
return result?.[0] || {};
};
export const fetchNotifsStatus = async (notificationId) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/notifications_status`, { notificationId });
return errcode !== 0 ? {} : (result?.[0] || {});
};
export const fetchNotifsStatusAgency = async (notificationId) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/notifications_status_agency`, { notificationId });
return errcode !== 0 ? [] : result;
};
export const postNotifsNewOrEdit = async (notificationId, data) => {
const { errcode, result } = await postJSON(`${HT_HOST}/service-CooperateSOA/notifications_addORedit`, { id: notificationId, ...data });
};
/**
* -- 供应商
*/
export const fetchMyNotifsList = async (params={}) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/me_notifications_list`, params);
return errcode !== 0 ? [] : result.map(ele => ({...ele, date: dayjs(ele.LastEditTime || ele.CreateDate).format('YYYY-MM-DD')}));
};
export const fetchMyNotifsDetail = async (notificationId) => {
const result = await fetchMyNotifsList({ notificationId });
return result?.[0] || {};
};
export const fetchMyNotifsUnReadCount = async () => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/me_notifications_unread_count`, { });
return errcode !== 0 ? 0 : (result?.unReadCount || 0);
}
export const updateNotifsConfirm = async (notificationId) => {
const queryStr = new URLSearchParams({notificationId}).toString();
const { errcode, result } = await postJSON(`${HT_HOST}/service-CooperateSOA/me_notifications_confirm?${queryStr}`);
return errcode !== 0 ? {} : result;
}
export const delNotifs = async (notificationId) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/service-CooperateSOA/notifications_delete`, { notificationId });
return errcode !== 0 ? {} : result;
}
/**
* Notice Store
*/
@ -98,11 +47,6 @@ export const useNoticeStore = create(
const noticeUnRead = await fetchBulletinUnReadCount(LMI_SN);
setNoticeUnRead(noticeUnRead);
},
getNoticeUnReadCount: async () => {
const { setNoticeUnRead } = get();
const noticeUnRead = await fetchMyNotifsUnReadCount();
setNoticeUnRead(noticeUnRead);
},
}), { name: 'noticeStore' })
);

@ -1,9 +1,9 @@
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
import dayjs from 'dayjs'
import { fetchJSON, postForm, postJSON } from '@haina/utils-request';
import { fetchJSON, postForm, postJSON } from '@/utils/request';
import { HT_HOST } from '@/config';
import { groupBy, generateId, isNotEmpty } from '@haina/utils-commons';
import { groupBy, generateId, isNotEmpty } from '@/utils/commons';
export const searchAgencyAction = async (param) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/products_search`, param);
@ -146,29 +146,13 @@ export const fetchRemarkList = async (params) => {
}
/**
* 保存合同备注
* 获取合同备注
*/
export const postRemarkList = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_memo_add`, params)
return { errcode, result, success: errcode === 0 }
}
/**
* 产品价格快照
*/
export const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
/**
* 修改产品的类型
*/
export const moveProductTypeAction = async (params) => {
const { errcode, result } = await postJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_move`, params)
return errcode !== 0 ? [] : result;
};
const defaultRemarkList = [
{id: 0, "product_type_id": "6","Memo": ""},
{id: 0, "product_type_id": "B","Memo": ""},
@ -275,7 +259,7 @@ export const useProductsStore = create(
}
},
newEmptyQuotation: (useDates) => ({
newEmptyQuotation: () => ({
id: null,
adult_cost: 0,
child_cost: 0,
@ -283,7 +267,10 @@ export const useProductsStore = create(
unit_id: '0',
group_size_min: 1,
group_size_max: 10,
use_dates: useDates,
use_dates: [
dayjs().startOf('M'),
dayjs().endOf('M')
],
weekdayList: [],
fresh: true // 标识是否是新记录,新记录才用添加列表
}),
@ -310,7 +297,7 @@ export const useProductsStore = create(
weekdays: definition.weekend.join(','),
WPI_SN: editingProduct.info.id,
WPP_VEI_SN: activeAgency.travel_agency_id,
lastedit_changed: {},
lastedit_changed: '',
audit_state_id: -1,
key: generateId(),
fresh: false
@ -341,23 +328,24 @@ export const useProductsStore = create(
if (formValues.fresh) {
formValues.key = generateId()
formValues.lastedit_changed = {}
formValues.lastedit_changed = ''
formValues.audit_state_id = -1 // 新增,
formValues.fresh = false // 添加到列表后就不是新纪录,保存要修改原来记录
mergedList = [...quotationList,...[formValues]]
} else {
mergedList = quotationList.map(prevQuotation => {
if (prevQuotation.key === formValues.key) {
const changedObject = {}
const changedList = []
for (const [key, value] of Object.entries(formValues)) {
if (key === 'use_dates' || key === 'id' || key === 'key' || key === 'weekdayList'
|| key === 'WPI_SN' || key === 'WPP_VEI_SN') continue
if (key === 'use_dates' || key === 'id' || key === 'key') continue
const preValue = prevQuotation[key]
const hasChanged = preValue !== value
if (hasChanged) {
changedObject[key] = preValue
changedList.push({
[key]: preValue,
})
}
}
@ -373,7 +361,7 @@ export const useProductsStore = create(
use_dates_start: formValues.use_dates_start,
use_dates_end: formValues.use_dates_end,
weekdays: formValues.weekdays,
lastedit_changed: changedObject
lastedit_changed: JSON.stringify(changedList, null, 2)
}
} else {
return prevQuotation
@ -415,14 +403,16 @@ export const useProductsStore = create(
quotationList: newQuotationList
})
let promiseDelete = Promise.resolve(newQuotationList)
if (isNotEmpty(quotationId)) {
const { success } = await deleteQuotationAction(quotationId)
if (success) {
return Promise.resolve(newQuotationList)
} else {
return Promise.reject(newQuotationList)
const { result, success } = await deleteQuotationAction(quotationId)
if (!success) {
promiseDelete = Promise.reject(result)
}
}
return promiseDelete
},
// side effects

@ -1,4 +1,4 @@
import { fetchJSON } from "@haina/utils-request";
import { fetchJSON } from "@/utils/request";
import { HT_HOST } from "@/config";
import { create } from 'zustand';
import { devtools } from 'zustand/middleware';
@ -8,10 +8,6 @@ const initialState = {
vendorScoresData: [], //地接统计数据集,合计数据,每月数据,地接考核分数
productScoresData: [], //产品体验分析 常用酒店分析, 导游接待情况
commendScoresData: [], //表扬情况, 投诉情况, 评建议
vendorScoresDataDiff: [],
productScoresDataDiff: [],
commendScoresDataDiff: [],
};
export const useReportStore = create(
devtools((set, get) => ({
@ -23,12 +19,8 @@ export const useReportStore = create(
setProductScoresData: (productScoresData) => set({ productScoresData }),
setCommendScoresData: (commendScoresData) => set({ commendScoresData }),
setVendorScoresDataDiff: (vendorScoresDataDiff) => set({ vendorScoresDataDiff }),
setProductScoresDataDiff: (productScoresDataDiff) => set({ productScoresDataDiff }),
setCommendScoresDataDiff: (commendScoresDataDiff) => set({ commendScoresDataDiff }),
async getHWVendorScores(VEI_SN, StartDate, EndDate, isCompare = false) {
const { setLoading, setVendorScoresData, setVendorScoresDataDiff } = get();
async getHWVendorScores(VEI_SN, StartDate, EndDate) {
const { setLoading, setVendorScoresData } = get();
setLoading(true);
const searchParams = {
VEI_SN,
@ -38,13 +30,12 @@ export const useReportStore = create(
OrderType: '-1',
GroupType: '-1',
};
const { errcode, errmsg, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWVendorScores`, searchParams);
if(isCompare===false) setVendorScoresData(errcode === 0 ? Result : {});
else setVendorScoresDataDiff(errcode === 0 ? Result : {});
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWVendorScores`, searchParams);
setVendorScoresData(errcode === 0 ? Result : {});
// setLoading(false);
},
async getHWProductScores(VEI_SN, StartDate, EndDate, isCompare = false) {
const { setLoading, setProductScoresData, setProductScoresDataDiff } = get();
async getHWProductScores(VEI_SN, StartDate, EndDate) {
const { setLoading, setProductScoresData } = get();
setLoading(true);
const searchParams = {
VEI_SN,
@ -54,13 +45,12 @@ export const useReportStore = create(
OrderType: '-1',
GroupType: '-1',
};
const { errcode, errmsg, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWProductScores`, searchParams);
if(isCompare===false) setProductScoresData(errcode === 0 ? Result : {});
else setProductScoresDataDiff(errcode === 0 ? Result : {});
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWProductScores`, searchParams);
setProductScoresData(errcode === 0 ? Result : {});
setLoading(false);
},
async getHWCommendScores(VEI_SN, StartDate, EndDate, isCompare = false) {
const { setLoading, setCommendScoresData, setCommendScoresDataDiff } = get();
async getHWCommendScores(VEI_SN, StartDate, EndDate) {
const { setLoading, setCommendScoresData } = get();
setLoading(true);
const searchParams = {
VEI_SN,
@ -70,9 +60,8 @@ export const useReportStore = create(
OrderType: '-1',
GroupType: '-1',
};
const { errcode, errmsg, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWCommendScores`, searchParams);
if(isCompare===false) setCommendScoresData(errcode === 0 ? Result : {});
else setCommendScoresDataDiff(errcode === 0 ? Result : {});
const { errcode, ...Result } = await fetchJSON(`${HT_HOST}/service-cusservice/PTGetHWCommendScores`, searchParams);
setCommendScoresData(errcode === 0 ? Result : {});
// setLoading(false);
},
}), { name: 'reportStore'})

@ -1,15 +1,15 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { fetchJSON, postForm } from '@haina/utils-request'
import { HT_HOST, TGA_HOST } from "@/config"
import { prepareUrl } from '@haina/utils-commons'
import { fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config"
import { prepareUrl } from '@/utils/commons'
import { usingStorage } from '@/hooks/usingStorage'
export const fetchCityList = async (travelAgencyId, reservationId) => {
const { errcode, Result } = await fetchJSON(
`${HT_HOST}/service-cusservice/PTGetCityGuide`,
{ VEI_SN: travelAgencyId, GRI_SN: reservationId, LGC: 1 })
return errcode !== 0 ? [] : Result??[]
return errcode !== 0 ? {} : Result
}
export const fetchPlanDetail = async (travelAgencyId, reservationId) => {
@ -27,21 +27,6 @@ export const fetchPlanDetail = async (travelAgencyId, reservationId) => {
}
}
export const fetchAllItinerary = async (reservationId) => {
const { errcode, result } = await fetchJSON(
`${TGA_HOST}/api/index.php/oversea/all_itinerary/`,
{ group_id: reservationId })
return errcode !== 0 ? {} : result
}
export const fetchTransport = async (reservationId) => {
const { errcode, result } = await fetchJSON(
`${TGA_HOST}/api/index.php/oversea/transport/`,
{ group_id: reservationId })
return errcode !== 0 ? {} : result
}
export const fetchAttachList = async (reservationId) => {
const { errcode, result } = await fetchJSON(
@ -53,7 +38,7 @@ export const fetchAttachList = async (reservationId) => {
const useReservationStore = create(devtools((set, get) => ({
cityList: [],
selectedAgencyId: -1,
selectedReservation: null,
selectedConfirmation: null,
arrivalDateRange: [],
@ -71,17 +56,16 @@ const useReservationStore = create(devtools((set, get) => ({
total: 0
},
confirmationList: [],
itineraryList: [],
flightList: [],
confirmationList: [
],
getCityListByReservationId: async (reservationId) => {
const { selectedAgencyId } = get()
const { travelAgencyId } = usingStorage()
set(() => ({
cityList: []
}))
const cityListJson = await fetchCityList(selectedAgencyId, reservationId)
const cityListJson = await fetchCityList(travelAgencyId, reservationId)
const mapCityList = cityListJson.map((data) => {
return {
key: data.CII_SN,
@ -109,14 +93,12 @@ const useReservationStore = create(devtools((set, get) => ({
},
fetchReservationList: (formValues, current=1) => {
set(() => ({
selectedAgencyId: formValues.agency
}))
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', formValues.agency)
.append('VEI_SN', travelAgencyId)
.append('GroupNo', formValues.referenceNo)
.append('DateStart', formValues.startdate)
.append('DateEnd', formValues.enddate)
@ -156,7 +138,8 @@ const useReservationStore = create(devtools((set, get) => ({
})
},
fetchAgencyGuideList: (travelAgencyId) => {
fetchAllGuideList: () => {
const { travelAgencyId } = usingStorage()
const fetchUrl = prepareUrl(HT_HOST + '/service-cusservice/PTGetGuideList')
.append('VEI_SN', travelAgencyId)
.build()
@ -168,8 +151,7 @@ const useReservationStore = create(devtools((set, get) => ({
return {
guideId: data.TGI_SN,
guideName: data.TGI2_Name,
mobileNo: data.TGI_Mobile,
cityName: data.city_name,
mobileNo: data.TGI_Mobile
}
})
return guideList
@ -179,12 +161,10 @@ const useReservationStore = create(devtools((set, get) => ({
})
},
getReservationDetail: async (travelAgencyId, reservationId) => {
getReservationDetail: async (reservationId) => {
const { travelAgencyId } = usingStorage()
const { planDetail, planChangeList } = await fetchPlanDetail(travelAgencyId, reservationId)
const attachListJson = await fetchAttachList(reservationId)
const itineraryListJson = await fetchAllItinerary(reservationId)
const transportDetailJson = await fetchTransport(reservationId)
const mapConfirmationList = planChangeList.map((data) => {
const filterAttchList = attachListJson.filter(attch => {
@ -209,9 +189,7 @@ const useReservationStore = create(devtools((set, get) => ({
arrivalDate: planDetail.eoi_getdate,
reservationId: reservationId
},
confirmationList: mapConfirmationList,
itineraryList: itineraryListJson,
flightList: transportDetailJson.flight
confirmationList: mapConfirmationList
}))
},

@ -1,6 +1,6 @@
import { create } from "zustand";
import { fetchJSON, postForm } from "@haina/utils-request";
import { prepareUrl, isNotEmpty } from "@haina/utils-commons";
import { fetchJSON, postForm } from "@/utils/request";
import { prepareUrl, isNotEmpty } from "@/utils/commons";
import { HT_HOST, DATE_FORMAT } from "@/config";
import dayjs from "dayjs";

@ -1,5 +1,3 @@
console.warn('Warning: `commons.js` is deprecated and will be removed in next version.');
export function copy(obj) {
return JSON.parse(JSON.stringify(obj));
}
@ -573,10 +571,6 @@ export const calcCacheSizes = async () => {
}
};
/**
* Clear Browser Cache: Service Worker Cache, HTTP Cache, IndexedDB
* * But keep cookies, local storage, and session storage.
*/
export const clearAllCaches = async cb => {
try {
// 1. Clear the service worker cache

@ -1,5 +1,3 @@
console.warn('Warning: `lifecycle.js` is deprecated and will be removed in next version.');
const initListener = []
const authListener = []

@ -1,8 +1,6 @@
import { BUILD_VERSION } from '@/config'
console.warn('Warning: `request.js` is deprecated and will be removed in next version.');
const customHeaders = []
// 添加 HTTP Reuqest 自定义头部

@ -1,141 +0,0 @@
import React, { useState, useEffect } from 'react';
import { Input, Button, Card, Typography, Space, Alert, Spin, Table, Tag } from 'antd';
import { useSearchParams } from 'react-router-dom';
import { postJSON } from "@haina/utils-request";
const { Title, Text } = Typography;
const AkamaiLog = () => {
const [searchParams] = useSearchParams();
const ip = searchParams.get('ip');
const [log, setLog] = useState([]);//
const [loading, setLoading] = useState(false);//
const [error, setError] = useState('');//
//
const STATIC_EXTENSIONS = [
'.jpg', '.jpeg', '.png', '.gif', '.bmp', '.webp', '.svg', '.ico',
'.css', '.scss', '.less',
'.js', '.jsx', '.ts', '.tsx', '.map',
'.woff', '.woff2', '.ttf', '.eot', '.otf',
'.mp4', '.mp3', '.avi', '.mov', '.flv', '.webm', '.m3u8',
'.pdf', '.doc', '.docx', '.xls', '.xlsx', '.ppt', '.pptx',
'.zip', '.tar', '.gz', '.rar', '.7z', '.xml', '.json', '.txt',
'.php'
];
// URL
const FILTER_URLS = [
'/api/', '/admin/', '/wp-admin/',
'robots.txt', 'sitemap.xml', '/404', '/.well-known/'
];
//
const filterLogData = (data) => {
if (!Array.isArray(data)) return [];
return data.filter(item => {
if (!item.request_path) return true;
const fullUrl = `${item.request_host || ''}${item.request_path}`;
const requestPath = item.request_path.toLowerCase();
const hasStaticExtension = STATIC_EXTENSIONS.some(ext =>
requestPath.endsWith(ext)
);
const hasFilteredUrl = FILTER_URLS.some(url =>
fullUrl.includes(url) || requestPath.includes(url)
);
return !hasStaticExtension && !hasFilteredUrl;
});
};
useEffect(() => {
const fetchLog = async () => {
if (!ip) {
setError('未获取到 IP 地址');
return;
}
setLoading(true);
setError('');
try {
const res = await postJSON(`http://202.103.69.110:8010/webhook/60601af9-dbd7-4b01-8718-a1c05018b4be`,
{ ip: ip }
);
// console.log(':', res);
const filteredData = filterLogData(res);
setLog(filteredData);
} catch (err) {
console.error('请求日志失败:', err);
setError('日志加载失败,请稍后重试');
} finally {
setLoading(false);
}
};
fetchLog();
}, [ip]);
//
const columns = [
{
title: '来源地址',
dataIndex: 'referer',
key: 'referer',
width: 250,
},
// {
// title: '(bytes)',
// dataIndex: 'bytes',
// key: 'bytes',
// width: 100,
// },
{
title: '请求路径',
key: 'request_path',
width: 300,
render: (_, record) => {
return record.request_host + '/' + record.request_path + (record.query_str !== '-' ? '?' + record.query_str : '');
},
},
{
title: '请求方法',
dataIndex: 'request_method',
key: 'request_method',
width: 80,
render: (method) => <Tag color="blue">{method}</Tag>,
},
{
title: '状态码',
dataIndex: 'status_code',
key: 'status_code',
width: 80,
},
{
title: '请求时间',
dataIndex: 'request_time',
key: 'request_time',
width: 180,
},
];
return (
<div style={{ padding: '20px' }}>
<Title level={2}>Akamai 日志查询</Title>
{error && <Alert message={error} type="error" showIcon style={{ marginBottom: 16 }} />}
<Card size="small" style={{ marginBottom: 16 }}>
<Text strong>当前查询 IP</Text>
<Text>{ip || '未获取到ip'}</Text>
</Card>
<Spin spinning={loading}>
<Table
rowKey="id"
columns={columns}
dataSource={log}
bordered
size="middle"
pagination={false}
/>
</Spin>
</div>
);
};
export default AkamaiLog;

@ -1,32 +1,26 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react'
import {
Popover, Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp,
Button, Form, Input,
Alert
} from 'antd'
import { Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp } from 'antd'
import { DownOutlined } from '@ant-design/icons'
import 'antd/dist/reset.css'
import AppLogo from '@/assets/highlights_travel_600_550.png'
import { isEmpty } from '@haina/utils-commons'
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, GIT_HEAD, PERM_PRODUCTS_INFO_PUT } from '@/config'
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 '@haina/utils-request'
import LogUploader from '@/components/LogUploader'
import { appendRequestParams } from '@/utils/request'
import { uploadPageSpyLog } from '@/pageSpy';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_REVIEW_ALL, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET,
PERM_RESERVATION_ALL, PERM_FEEDBACK_ALL, PERM_INVOICE_ALL, PERM_REPORT_ALL
} from '@/config'
import ReloadUpdate from './ReloadUpdate'
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
@ -41,6 +35,8 @@ function App() {
const { loginToken } = usingStorage()
const [messageApi, contextHolder] = message.useMessage()
const noticeUnRead = useNoticeStore((state) => state.noticeUnRead)
const href = useHref()
const navigate = useNavigate()
@ -67,15 +63,26 @@ function App() {
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/pick-year'
const productLink = isPermitted(PERM_PRODUCTS_MANAGEMENT) ? '/products' : '/products/edit'
return (
<ConfigProvider locale={antdLng}
theme={{
token: {
colorPrimary: colorPrimary,
// "sizeStep": 3,
// "sizeUnit": 3,
},
algorithm: theme.defaultAlgorithm,
}}>
@ -86,27 +93,27 @@ function App() {
insetInlineEnd: 94,
}}
>
<LogUploader />
<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-12 my-2 mr-6 ml-0' alt='App logo' />
<img src={AppLogo} className='float-left h-9 my-4 mr-6 ml-0 bg-white/30' alt='App logo' />
</NavLink>
<Menu
theme='dark'
mode='horizontal'
selectedKeys={[defaultPath]}
items={[
isPermitted(PERM_RESERVATION_ALL) ? { key: 'reservation', label: <Link to='/reservation/newest'>{t('menu.Reservation')}</Link> } : null,
isPermitted(PERM_INVOICE_ALL) ? { key: 'invoice', label: <Link to='/invoice'>{t('menu.Invoice')}</Link> } : null,
isPermitted(PERM_FEEDBACK_ALL) ? { key: 'feedback', label: <Link to='/feedback'>{t('menu.Feedback')}</Link> } : null,
isPermitted(PERM_REPORT_ALL) ? { key: 'report', label: <Link to='/report'>{t('menu.Report')}</Link> } : null,
isPermitted(PERM_REVIEW_ALL) ? { key: 'external-reviews', label: <Link to='/external-reviews'>{t('menu.external-reviews')}</Link> } : null,
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,
@ -136,8 +143,6 @@ function App() {
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: <ReloadUpdate type='link' className='px-0' />, key: '5' },
{ type: 'divider' },
{ label: <Link to='/logout'>{t('Logout')}</Link>, key: '99' },
]
],
@ -157,9 +162,7 @@ function App() {
<Content className='p-6 m-0 min-h-72 bg-white overflow-auto'>
{needToLogin ? <>login...</> : <Outlet />}
</Content>
<Footer>Highlights Travel Co., LTD, Version: {BUILD_VERSION}({GIT_HEAD})
<ReloadUpdate />
</Footer>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
</Layout>
</ErrorBoundary>
</AntApp>

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

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

@ -1,116 +1,109 @@
import { useNavigate } from "react-router-dom";
import { useEffect } from "react";
import { Button, Form, Input, Row, Radio, App, Typography } from "antd";
import { useTranslation } from "react-i18next";
import useAuthStore from "@/stores/Auth";
import { appendRequestParams } from "@haina/utils-request";
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 [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 { t, i18n } = useTranslation()
const { notification } = App.useApp()
const navigate = useNavigate()
const [form] = Form.useForm()
const handleLngChange = (lng) => {
appendRequestParams("lgc", lng === "zh" ? 2 : 1);
i18n.changeLanguage(lng);
};
appendRequestParams('lgc', lng === 'zh' ? 2 : 1)
i18n.changeLanguage(lng)
}
const defaultLng = i18n.language ?? "zh";
appendRequestParams("lgc", defaultLng === "zh" ? 2 : 1);
const defaultLng = i18n.language??'zh'
appendRequestParams('lgc', defaultLng === 'zh' ? 2 : 1)
useEffect(() => {
useEffect (() => {
if (loginStatus === 302) {
navigate(defaultRoute);
navigate(defaultRoute)
}
}, [loginStatus]);
}, [loginStatus])
const onFinish = (values) => {
authenticate(values.username, values.password).catch((ex) => {
console.error(ex);
notification.error({
message: t("Validation.Title"),
description: t("Validation.LoginFailed"),
placement: "top",
duration: 4,
});
});
};
authenticate(values.username, values.password)
.catch(ex => {
console.error(ex)
notification.error({
message: t('Validation.Title'),
description: t('Validation.LoginFailed'),
placement: 'top',
duration: 4,
})
})
}
const onFinishFailed = (errorInfo) => {
console.log("Failed:", errorInfo);
};
console.log('Failed:', errorInfo);
}
return (
<>
<Typography.Title className="text-center" level={3}>
Highlights Hub
</Typography.Title>
<Row justify="center" align="middle" className="min-h-96">
<Form
name="login"
layout="vertical"
form={form}
size="large"
labelCol={{
span: 8,
}}
wrapperCol={{
span: 24,
}}
className="max-w-xl"
initialValues={{
language: defaultLng,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
<Row justify='center' align='middle' className='min-h-96'>
<Form
name='login'
layout='vertical'
form={form}
size='large'
labelCol={{
span: 8,
}}
wrapperCol={{
span: 24,
}}
className='max-w-xl'
initialValues={{
language: defaultLng,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete='off'
>
<Form.Item
label={t('Username')}
name='username'
rules={[
{
required: true,
message: t('Validation.UsernameIsEmpty'),
},
]}
>
<Form.Item
label={t("Username")}
name="username"
rules={[
{
required: true,
message: t("Validation.UsernameIsEmpty"),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("Password")}
name="password"
rules={[
{
required: true,
message: t("Validation.PasswordIsEmpty"),
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item name="language">
<Radio.Group onChange={(e) => handleLngChange(e.target.value)}>
<Radio value="zh">中文</Radio>
<Radio value="en">English</Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit" className="w-full">
{t("Login")}
</Button>
</Form.Item>
</Form>
</Row>
</>
);
<Input />
</Form.Item>
<Form.Item
label={t('Password')}
name='password'
rules={[
{
required: true,
message: t('Validation.PasswordIsEmpty'),
},
]}
>
<Input.Password />
</Form.Item>
<Form.Item name='language'>
<Radio.Group onChange={e => handleLngChange(e.target.value)}>
<Radio value='zh'>中文</Radio>
<Radio value='en'>English</Radio>
</Radio.Group>
</Form.Item>
<Form.Item>
<Button type='primary' htmlType='submit' className='w-full'>
{t('Login')}
</Button>
</Form.Item>
</Form>
</Row>
)
}
export default Login;
export default Login

@ -1,18 +0,0 @@
import { Button } from 'antd';
import { clearAllCaches } from '@haina/utils-commons';
import { useTranslation } from 'react-i18next'
const ReloadUpdate = ({ ...props }) => {
const { t } = useTranslation()
const forceReload = async () => {
await clearAllCaches();
window.location.reload(true);
};
return (
<Button type={'link'} onClick={forceReload} className={`ms-2 `} {...props}>
{t('SystemUpdate')} 🚀
</Button>
);
};
export default ReloadUpdate;

@ -1,9 +1,9 @@
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/highlights_travel_600_550.png";
import AppLogo from "@/assets/logo-gh.png";
import { useThemeContext } from "@/stores/ThemeContext";
import { BUILD_VERSION, GIT_HEAD } from "@/config";
import { BUILD_VERSION } from "@/config";
const { Header, Content, Footer } = Layout;
@ -20,13 +20,14 @@ function Standlone() {
}}>
<AntApp>
<Layout className="min-h-screen">
<Header className="sticky top-0 z-10 w-full text-center">
<img src={AppLogo} className="h-12 my-2 mr-6 ml-0" alt="App logo" />
<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 className="p-6 m-0 min-h-72 bg-white">
<Outlet />
</Content>
<Footer>Highlights Travel Co., LTD, Version: {BUILD_VERSION}({GIT_HEAD})</Footer>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}</Footer>
</Layout>
</AntApp>
</ConfigProvider>

@ -9,10 +9,7 @@ function ChangePassword() {
const { t } = useTranslation()
const navigate = useNavigate()
const [changeUserPassword, defaultRoute] = useAuthStore((state) => [
state.changeUserPassword,
state.defaultRoute,
])
const changeUserPassword = useAuthStore((state) => state.changeUserPassword)
const { notification } = App.useApp()
const [form] = Form.useForm()
@ -97,7 +94,7 @@ function ChangePassword() {
<Button type="primary" htmlType="submit">
{t('Submit')}
</Button>
<Button onClick={() => navigate(defaultRoute)}>
<Button onClick={() => navigate('/reservation/newest')}>
{t('Cancel')}
</Button>
</Space>

@ -1,9 +1,9 @@
import SearchForm from '@/components/SearchForm'
import useAccountStore, { fetchRoleList, fetchTravelAgencyByName, genRandomPassword } from '@/stores/Account'
import useFormStore from '@/stores/Form'
import { isEmpty, debounce } from '@haina/utils-commons'
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, Spin } from 'antd'
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'
@ -60,7 +60,6 @@ function Management() {
showDisableConfirm(account, checked)
}} />
<Button type='link' key='resetPassword' onClick={() => showResetPasswordConfirm(account)}>{t('account:action.resetPassword')}</Button>
<Button type='link' key='editAccount' onClick={() => onAccountSeleted(account)}>{t('account:action.edit')}</Button>
</Space>
)
}
@ -232,7 +231,7 @@ function Management() {
}}
title={t('account:detail')}
open={isAccountModalOpen} onCancel={() => setAccountModalOpen(false)}
destroyOnHidden
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
@ -306,7 +305,7 @@ function Management() {
showSearch
filterOption={false}
onSearch={debounce(handleTravelAgencySearch, 800)}
notFoundContent={dataLoading ? <Spin size='small' /> : null}
notFoundContent={null}
>
</Select>
</Form.Item>

@ -1,9 +1,9 @@
import RequireAuth from '@/components/RequireAuth'
import { PERM_ROLE_NEW } from '@/config'
import useAccountStore, { fetchPermissionList, fetchPermissionListByRoleId, fetchRoleList } from '@/stores/Account'
import { isEmpty } from '@haina/utils-commons'
import { isEmpty } from '@/utils/commons'
import {
PushpinTwoTone,
SyncOutlined,
} from '@ant-design/icons'
import { App, Button, Col, Form, Input, Modal, Row, Space, Table, Tag, TreeSelect, Typography } from 'antd'
import dayjs from 'dayjs'
@ -34,7 +34,7 @@ function RoleList() {
function actionRender(_, role) {
if (role.role_id == 1) {
return (<Button type='text'><PushpinTwoTone twoToneColor="#c0192a" /></Button>)
return (<Tag icon={<SyncOutlined spin />} color='warning'>不能修改</Tag>)
} else {
return (
<Button type='link' key='edit' onClick={() => onRoleSeleted(role)}>{t('account:action.edit')}</Button>
@ -89,7 +89,7 @@ function RoleList() {
children: permissisonList.map(p => {
return {
disableCheckbox: p.res_id == 1,
title: p.res_pattern.indexOf('danger') === -1 ? p.res_name : (<span style={{color: 'firebrick'}}>{p.res_name}</span>),
title: p.res_name,
value: p.res_id,
key: p.res_id,
}
@ -164,7 +164,7 @@ function RoleList() {
}}
title={t('account:detail')}
open={isRoleModalOpen} onCancel={() => setRoleModalOpen(false)}
destroyOnHidden
destroyOnClose
forceRender
modalRender={(dom) => (
<Form

@ -2,14 +2,13 @@ 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 "@haina/utils-commons";
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";
import useFormStore from "@/stores/Form";
const planListColumns = [
{
@ -87,9 +86,7 @@ const planListColumns = [
const Airticket = props => {
const navigate = useNavigate();
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { travelAgencyId: myAgencyId } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const { travelAgencyId } = usingStorage();
const [getPlanList, planList, loading] = airTicketStore(state => [state.getPlanList, state.planList, state.loading]);
const showTotal = total => `合计 ${total} `;
@ -113,7 +110,7 @@ const Airticket = props => {
},
}}
onSubmit={(err, formVal, filedsVal) => {
getPlanList(formVal.agency || travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
}}
/>
</Col>

@ -2,7 +2,7 @@ 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 "@haina/utils-commons";
import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
import BackBtn from "@/components/BackBtn";
@ -11,13 +11,10 @@ import { TableExportBtn } from "@/components/Data";
import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage";
import useFormStore from "@/stores/Form";
const Invoice = props => {
const navigate = useNavigate();
const { notification } = App.useApp();
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { travelAgencyId: myAgencyId } = usingStorage();
const travelAgencyId = agency; //; // || myAgencyId;
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([]);
@ -220,7 +217,7 @@ const Invoice = props => {
},
}}
onSubmit={(err, formVal, filedsVal) => {
getVEIFlightBill(formVal.agency || travelAgencyId, formVal.referenceNo, formVal.invoiceCheckStatus, formVal.startdate, formVal.endtime);
getVEIFlightBill(travelAgencyId, formVal.referenceNo, formVal.invoiceCheckStatus, formVal.startdate, formVal.endtime);
}}
/>
</Col>

@ -2,7 +2,7 @@ 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 "@haina/utils-commons";
import { isEmpty, formatColonTime, formatDate, isNotEmpty } from "@/utils/commons";
import { DATE_FORMAT } from "@/config";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
@ -13,13 +13,11 @@ import { fetchInvoicePaidDetail } from "@/stores/Invoice";
import airTicketStore from "@/stores/Airticket";
import { usingStorage } from "@/hooks/usingStorage";
import useFormStore from "@/stores/Form";
const InvoicePaid = props => {
const navigate = useNavigate();
const navigate = useNavigate();
const { notification } = App.useApp();
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { travelAgencyId:myAgencyId } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const { travelAgencyId } = usingStorage();
const [invoicePaidDetail, setInvoicePaidDetail] = useState([]);
const [invoiceNO, setInvoiceNO] = useState([]); //
const [loading, invoicePaid, fetchInvoicePaid] = useInvoiceStore(state => [state.loading, state.invoicePaid, state.fetchInvoicePaid]);
@ -108,7 +106,7 @@ const InvoicePaid = props => {
},
}}
onSubmit={(err, formVal) => {
fetchInvoicePaid(formVal.agency || travelAgencyId, "", formVal.startdate, formVal.enddate);
fetchInvoicePaid(travelAgencyId, "", formVal.startdate, formVal.enddate);
setInvoicePaidDetail([]);
}}
/>

@ -1,19 +1,17 @@
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 '@haina/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';
import useFormStore from '@/stores/Form';
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 [{ agency, ...formValuesToSub }] = useFormStore(state => [state.formValuesToSub]);
const { coli_sn, gri_sn } = useParams();
const { travelAgencyId: myAgencyId, loginToken, userId } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const { travelAgencyId, loginToken, userId } = usingStorage();
const [
getPlanDetail,
planDetail,
@ -60,7 +58,7 @@ const AirticketPlan = props => {
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} age , ${item.MEI_Birthday}(YYYY-MM-DD)` };
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}` };
})
);
};
@ -81,62 +79,62 @@ const AirticketPlan = props => {
//
const costListColumns = [
{
title: '客人信息/备注',
key: 'Memo',
dataIndex: 'Memo',
title: "客人信息/备注",
key: "Memo",
dataIndex: "Memo",
},
{
title: '状态',
key: 'CostType',
dataIndex: 'CostType',
title: "状态",
key: "CostType",
dataIndex: "CostType",
},
{
title: '票号',
key: 'TicketNo',
dataIndex: 'TicketNo',
render: (text, record) => (record.CostType == '出票' ? text : '-'),
title: "票号",
key: "TicketNo",
dataIndex: "TicketNo",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: 'PNR',
key: 'PNR',
dataIndex: 'PNR',
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: "FlightType",
dataIndex: "FlightType",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: '机票价格',
key: 'Cost',
dataIndex: 'Cost',
render: (text, record) => (record.CostType == '出票' ? text : '-'),
title: "机票价格",
key: "Cost",
dataIndex: "Cost",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: '服务费',
key: 'ServiceFee',
dataIndex: 'ServiceFee',
title: "服务费",
key: "ServiceFee",
dataIndex: "ServiceFee",
},
{
title: '折扣',
key: 'Discount',
dataIndex: 'Discount',
render: (text, record) => (record.CostType == '出票' ? text : '-'),
title: "折扣",
key: "Discount",
dataIndex: "Discount",
render: (text, record) => (record.CostType == "出票" ? text : "-"),
},
{
title: '编辑',
key: 'CLC_SN',
dataIndex: 'CLC_SN',
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'>
<Popconfirm title="删除" description="请确认是否删除?" onConfirm={() => handleDelete(record.CLC_SN)} okText="是" cancelText="否">
<Button danger type="link">
删除
</Button>
</Popconfirm>
@ -154,7 +152,7 @@ const AirticketPlan = props => {
<>
<Form
// form={airinfo_form}
name={'ticket_form_' + airInfo.id}
name={"ticket_form_" + airInfo.id}
labelCol={{
span: 6,
}}
@ -167,8 +165,8 @@ const AirticketPlan = props => {
.then(() => {
notification.success({
message: `成功`,
description: '机票信息保存成功!',
placement: 'top',
description: "机票信息保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
@ -176,133 +174,104 @@ const AirticketPlan = props => {
.catch(() => {
notification.error({
message: `错误`,
description: '保存失败',
placement: 'top',
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
}}
autoComplete='off'>
<Divider orientation='left'>航班信息</Divider>
autoComplete="off">
<Divider orientation="left">航班信息</Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label='出发日期、航班、城市、时间' required>
<Form.Item label="出发日期、航班、城市、时间" required>
<Space>
<Form.Item name='StartDate' noStyle rules={[{ required: true, message: '请输入出发日期!' }]}>
<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 name="FlightNo" noStyle rules={[{ required: true, message: "请输入航班号!" }]}>
<Input placeholder="航班号" />
</Form.Item>
<Form.Item name='FromCity' noStyle rules={[{ required: true, message: '请输入出发城市!' }]}>
<Input placeholder='出发' />
<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 name="FlightStart" noStyle rules={[{ required: true, message: "请输入出发时间!" }]}>
<Input placeholder="出发时间" />
</Form.Item>
-
<Form.Item name='ToCity' noStyle rules={[{ required: true, message: '请输入抵达城市!' }]}>
<Input placeholder='抵达' />
<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 name="FlightEnd" noStyle rules={[{ required: true, message: "请输入抵达时间!" }]}>
<Input placeholder="抵达时间" />
</Form.Item>
</Space>
</Form.Item>
<Form.Item label='机场、航站楼、仓位、行李重量' required>
<Form.Item label="机场、航站楼、仓位、行李重量" required>
<Space>
<Form.Item name='FromAirport' noStyle rules={[{ required: true, message: '请输入出发机场!' }]}>
<Form.Item name="FromAirport" noStyle rules={[{ required: true, message: "请输入出发机场!" }]}>
<Select
showSearch
placeholder='出发机场'
placeholder="出发机场"
style={{
minWidth: 160,
}}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
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 name="FromTerminal" noStyle rules={[{ required: true, message: "请输入出发航站楼!" }]}>
<Input placeholder="航站楼" />
</Form.Item>
-
<Form.Item name='ToAirport' noStyle rules={[{ required: true, message: '请输入抵达机场!' }]}>
<Form.Item name="ToAirport" noStyle rules={[{ required: true, message: "请输入抵达机场!" }]}>
<Select
showSearch
placeholder='抵达机场'
placeholder="抵达机场"
style={{
minWidth: 160,
}}
filterOption={(input, option) => (option?.label ?? '').toLowerCase().includes(input.toLowerCase())}
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 name="ToTerminal" noStyle rules={[{ required: true, message: "请输入抵达航站楼!" }]}>
<Input placeholder="航站楼" />
</Form.Item>
<Form.Item name='FlightCabin' noStyle rules={[{ required: true, message: '请输入仓位!' }]}>
{/* <Input placeholder="仓位" /> */}
<Select
showSearch
placeholder='仓位'
style={{
minWidth: 160,
}}
options={[
{
value: '经济舱 Economy class',
label: '经济舱 Economy class',
},
{
value: '头等舱 first-class',
label: '头等舱 first-class',
},
{
value: '高端经济舱 PremiumEconomy Class',
label: '高端经济舱 PremiumEconomy Class',
},
{
value: '公务舱 Business class',
label: '公务舱 Business class',
},
{
value: '来回程 round trip',
label: '来回程 round trip',
},
]}
/>
<Form.Item name="FlightCabin" noStyle rules={[{ required: true, message: "请输入仓位!" }]}>
<Input placeholder="仓位" />
</Form.Item>
<Form.Item name='Baggage' noStyle>
<Input placeholder='行李说明 20KG' />
<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='未处理' />
<Space direction="vertical">
<Form.Item name="TicketIssued">
<Switch checkedChildren="已处理" unCheckedChildren="未处理" />
</Form.Item>
<Button type='primary' htmlType='submit'>
<Button type="primary" htmlType="submit">
1. 保存机票信息
</Button>
</Space>
</Col>
</Row>
<Divider orientation='left'>出票信息</Divider>
<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} />
<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)}>
<Space direction="vertical">
<Button type="primary" onClick={() => showModal(airInfo)}>
2. 添加出票信息
</Button>
</Space>
@ -323,8 +292,8 @@ const AirticketPlan = props => {
.then(() => {
notification.success({
message: `成功`,
description: '提醒信息已发出!',
placement: 'top',
description: "提醒信息已发出!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
@ -333,26 +302,26 @@ const AirticketPlan = props => {
.catch(() => {
notification.error({
message: `错误`,
description: '提醒失败',
placement: 'top',
description: "提醒失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
});
}}
autoComplete='off'>
<Divider orientation='left'></Divider>
autoComplete="off">
<Divider orientation="left"></Divider>
<Row gutter={16}>
<Col md={24} lg={20} xxl={20}>
<Form.Item label='提醒信息' name='FlightMemo_messages'>
<Input placeholder='没有提醒请留空,信息会抄送给上下站地接' />
<Form.Item label="提醒信息" name="FlightMemo_messages">
<Input placeholder="没有提醒请留空,信息会抄送给上下站地接" />
</Form.Item>
<Form.Item label='已发提醒' name='FlightMemo'>
<Form.Item label="已发提醒" name="FlightMemo">
<Input.TextArea rows={4} readOnly disabled />
</Form.Item>
</Col>
<Col md={24} lg={4} xxl={4}>
<Button type='primary' htmlType='submit'>
<Button type="primary" htmlType="submit">
3. 通知顾问
</Button>
</Col>
@ -370,15 +339,15 @@ const AirticketPlan = props => {
label: `${item.StartDate} ${item.FlightNo}(${item.FromAirport}${item.FlightStart}-${item.ToAirport}${item.FlightEnd})(${item.FlightCabin})`,
extra: (
<Popconfirm
title='请确认要删除航班记录'
description=''
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}>
okText="是"
cancelText="否">
<Button type="dashed" size="small" disabled={item.Flightcost_AsJOSN.length == 0 ? false : true}>
删除
</Button>
</Popconfirm>
@ -401,11 +370,11 @@ const AirticketPlan = props => {
const showModal = ticket => {
setIsModalOpen(true);
ticket_form.resetFields();
if (isEmpty(ticket.CostType)) ticket.CostType = '出票';
ticket.CostType == '出票' ? setisTicketType(true) : setisTicketType(false); //
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: '' });
if (isEmpty(ticket.Memo)) ticket_form.setFieldsValue({ Memo: "" });
};
const handleOk = (close_modal = true) => {
@ -413,13 +382,13 @@ const AirticketPlan = props => {
.validateFields()
.then(values => {
//
console.log('Received values of form: ', values);
console.log("Received values of form: ", values);
postFlightCost(values)
.then(() => {
notification.success({
message: `成功`,
description: '保存成功!',
placement: 'top',
description: "保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
@ -428,8 +397,8 @@ const AirticketPlan = props => {
.catch(() => {
notification.error({
message: `错误`,
description: '保存失败',
placement: 'top',
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
@ -437,7 +406,7 @@ const AirticketPlan = props => {
if (close_modal) setIsModalOpen(false);
})
.catch(info => {
console.log('Validate Failed:', info);
console.log("Validate Failed:", info);
});
};
@ -451,8 +420,8 @@ const AirticketPlan = props => {
.then(() => {
notification.success({
message: `成功`,
description: '删除成功!',
placement: 'top',
description: "删除成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
@ -461,8 +430,8 @@ const AirticketPlan = props => {
.catch(() => {
notification.error({
message: `错误`,
description: '删除失败',
placement: 'top',
description: "删除失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
@ -470,7 +439,7 @@ const AirticketPlan = props => {
};
const onChangeType = value => {
if (value == '出票') {
if (value == "出票") {
setisTicketType(true);
} else {
setisTicketType(false);
@ -491,13 +460,13 @@ const AirticketPlan = props => {
confirmInfo_form
.validateFields()
.then(values => {
console.log('Received values of form: ', values.ConfirmInfo);
console.log("Received values of form: ", values.ConfirmInfo);
postVeiFlightPlanConfirm(travelAgencyId, gri_sn, userId, values.ConfirmInfo)
.then(() => {
notification.success({
message: `成功`,
description: '保存成功!',
placement: 'top',
description: "保存成功!",
placement: "top",
duration: 4,
icon: <LikeTwoTone />,
});
@ -506,8 +475,8 @@ const AirticketPlan = props => {
.catch(() => {
notification.error({
message: `错误`,
description: '保存失败',
placement: 'top',
description: "保存失败",
placement: "top",
duration: 4,
icon: <FrownTwoTone />,
});
@ -516,7 +485,7 @@ const AirticketPlan = props => {
setisModalOpen_confirmInfo(false);
})
.catch(info => {
console.log('Validate Failed:', info);
console.log("Validate Failed:", info);
});
};
@ -530,42 +499,42 @@ const AirticketPlan = props => {
}, []);
return (
<Space direction='vertical' style={{ width: '100%' }}>
<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'} />
<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}>
<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>
<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' }}>
<p style={{ textAlign: "right" }}>
<Popconfirm
title='请确认要增加航班记录'
description=''
title="请确认要增加航班记录"
description=""
onConfirm={async () => {
await postFlightDetail('', gri_sn, travelAgencyId, { FlightNo: '新的记录', FlightStatus: 1 }, []); //
await postFlightDetail("", gri_sn, travelAgencyId, { FlightNo: "新的记录", FlightStatus: 1 }, []); //
getPlanDetail(travelAgencyId, gri_sn); //
}}
okText='是'
cancelText='否'>
<Button type='dashed' icon={<PlusOutlined />}>
okText="是"
cancelText="否">
<Button type="dashed" icon={<PlusOutlined />}>
新增航班记录
</Button>
</Popconfirm>
@ -573,12 +542,12 @@ const AirticketPlan = props => {
</Col>
</Row>
<Row>
<Divider orientation='left'>计划变更</Divider>
<Divider orientation="left">计划变更</Divider>
<Col md={24} lg={12} xxl={12}>
<Space direction='vertical' style={{ width: '90%' }}>
<Space direction="vertical" style={{ width: "90%" }}>
<Input.TextArea rows={16} readOnly value={veiPlanChangeTxt && veiPlanChangeTxt.ChangeText} />
<Button
type='primary'
type="primary"
onClick={() => {
showModal_confirmInfo(veiPlanChangeTxt && veiPlanChangeTxt.ChangeText);
}}
@ -592,34 +561,34 @@ const AirticketPlan = props => {
</Col>
</Row>
<Modal title='变更' open={isModalOpen_confirmInfo} onOk={handleOk_confirmInfo} onCancel={handleCancel_confirmInfo}>
<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 }]}>
<Form.Item label="确认信息" name="ConfirmInfo" rules={[{ required: true }]}>
<Input.TextArea rows={4} />
</Form.Item>
</Form>
</Modal>
<Modal
title='费用信息'
title="费用信息"
open={isModalOpen}
onOk={handleOk}
onCancel={handleCancel}
okText='保存'
cancelText='关闭'
okText="保存"
cancelText="关闭"
footer={(_, { OkBtn, CancelBtn }) => (
<>
<CancelBtn />
{isAddNew ? (
<>
<Button type='primary' onClick={() => handleOk(false)}>
<Button type="primary" onClick={() => handleOk(false)}>
添加并继续新增
</Button>{' '}
<Button type='primary' onClick={() => handleOk(true)}>
</Button>{" "}
<Button type="primary" onClick={() => handleOk(true)}>
添加并关闭
</Button>
</>
@ -633,7 +602,7 @@ const AirticketPlan = props => {
labelCol={{
span: 5,
}}>
<Form.Item label='状态' name='CostType'>
<Form.Item label="状态" name="CostType">
<Select
style={{
width: 160,
@ -641,16 +610,16 @@ const AirticketPlan = props => {
onChange={onChangeType}
options={[
{
value: '出票',
label: '出票',
value: "出票",
label: "出票",
},
{
value: '改签',
label: '改签',
value: "改签",
label: "改签",
},
{
value: '退票',
label: '退票',
value: "退票",
label: "退票",
},
]}
/>
@ -658,41 +627,41 @@ const AirticketPlan = props => {
{isTicketType && (
<>
<Form.Item label='PNR' name='PNR' rules={[{ required: true }]}>
<Form.Item label="PNR" name="PNR" rules={[{ required: true }]}>
<Input />
</Form.Item>
<Form.Item label='票号' name='TicketNo' rules={[{ required: true }]}>
<Form.Item label="票号" name="TicketNo" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
/>
</Form.Item>
<Form.Item label='机票类型' name='FlightType' rules={[{ required: true }]}>
<Form.Item label="机票类型" name="FlightType" rules={[{ required: true }]}>
<Select
style={{
width: 160,
}}
options={[
{
value: '成人',
label: '成人',
value: "成人",
label: "成人",
},
{
value: '儿童',
label: '儿童',
value: "儿童",
label: "儿童",
},
{
value: '婴儿',
label: '婴儿',
value: "婴儿",
label: "婴儿",
},
]}
/>
</Form.Item>
<Form.Item label='机票价格' name='Cost' rules={[{ required: true }]}>
<Form.Item label="机票价格" name="Cost" rules={[{ required: true }]}>
<Input
placeholder='含基建和税'
prefix='¥'
placeholder="含基建和税"
prefix="¥"
style={{
width: 160,
}}
@ -700,9 +669,9 @@ const AirticketPlan = props => {
</Form.Item>
</>
)}
<Form.Item label='服务费' name='ServiceFee' rules={[{ required: true }]}>
<Form.Item label="服务费" name="ServiceFee" rules={[{ required: true }]}>
<Input
prefix='¥'
prefix="¥"
style={{
width: 160,
}}
@ -710,15 +679,15 @@ const AirticketPlan = props => {
</Form.Item>
{isTicketType && (
<>
<Form.Item label='折扣' name='Discount' rules={[{ required: true }]}>
<Form.Item label="折扣" name="Discount" rules={[{ required: true }]}>
<Input
style={{
width: 160,
}}
placeholder='如 0.9'
placeholder="如 0.9"
/>
</Form.Item>
<Form.Item label='选择客人' name='MEI_Name66'>
<Form.Item label="选择客人" name="MEI_Name66">
<Radio.Group
onChange={e => guestList_OnChange(e)}
style={{
@ -738,20 +707,20 @@ const AirticketPlan = props => {
</>
)}
<Form.Item label='客人信息/备注' name='Memo'>
<Form.Item label="客人信息/备注" name="Memo">
<Input.TextArea rows={4} disabled={isTicketType} />
</Form.Item>
<Form.Item name='CLF_SN' hidden>
<Form.Item name="CLF_SN" hidden>
<input />
</Form.Item>
<Form.Item name='GRI_SN' hidden>
<Form.Item name="GRI_SN" hidden>
<input />
</Form.Item>
<Form.Item name='VEI_SN' hidden>
<Form.Item name="VEI_SN" hidden>
<input />
</Form.Item>
<Form.Item name='CLC_SN' hidden>
<Form.Item name="CLC_SN" hidden>
<input />
</Form.Item>
</Form>

@ -1,390 +0,0 @@
import { useState, useEffect } from "react";
import {
Row,
Col,
Space,
Button,
Form,
Table,
Typography,
Input,
DatePicker,
Drawer,
Select,
App,
} from "antd";
import dayjs from "dayjs";
import { FileAddOutlined, EditOutlined } from "@ant-design/icons";
import { groupBy, isNotEmpty, isEmpty } from "@haina/utils-commons";
import { useTranslation } from "react-i18next";
import useFormStore from "@/stores/Form";
import { usingStorage } from "@/hooks/usingStorage";
import useExternalReviewStore, {
fetchRecentGroup, initialState
} from "@/stores/ExternalReview";
import useReservationStore, { fetchCityList } from "@/stores/Reservation";
import SearchForm from "@/components/SearchForm";
function ReviewList() {
const { t } = useTranslation();
const reviewListColumns = [
{
title: t("review.ReviewLink"),
dataIndex: "reviewLink",
width: "300px",
ellipsis: true,
render: (_, record) => (
<Typography.Link href={record.reviewLink} target="_blank">
{record.reviewLink}
</Typography.Link>
),
},
{
title: (<><div>{t('review.DatePosted')}</div><div>YYYY-MM-DD</div></>),
dataIndex: "datePosted",
width: "120px",
ellipsis: true,
render: (text) => (isEmpty(text) ? "" : dayjs(text).format("YYYY-MM-DD")),
},
{
title: t("review.ReferenceNumber"),
dataIndex: "referenceNumber",
width: "300px",
ellipsis: true,
},
{
title: t("review.AdminNotes"),
dataIndex: "adminNotes",
},
{
title: t("review.CustomerID"),
dataIndex: "customerId",
},
{
title: t("review.Guide"),
dataIndex: "guide",
},
{
title: t("review.Bonus"),
dataIndex: "bonus",
width: "120px",
ellipsis: true,
align: "right",
},
{
title: t("review.ApprovalStatus"),
dataIndex: "approvalStatus",
width: "150px",
ellipsis: true,
align: "center",
render: (text) => {
if (text === 136002) return "Approved";
else if (text === 136003) return "Rejected";
else if (text === 136001) return "Pending Review";
else return "-";
},
},
{
title: (<><div>{t('review.ApprovalDate')}</div><div>YYYY-MM-DD</div></>),
dataIndex: "approvalDate",
width: "150px",
ellipsis: true,
render: (text) =>
isEmpty(text) ? "" : dayjs(text).format("YYYY-MM-DD HH:mm"),
},
{
title: t("review.Action"),
width: "120px",
ellipsis: true,
render: (_, review) => {
return (
<Button
type="link"
icon={<EditOutlined />}
onClick={() => openReviewDrawer(review)}
>
Edit
</Button>
);
},
},
];
const [formReview] = Form.useForm();
const [openReview, setOpenReview] = useState(false);
const [dataLoading, setDataLoading] = useState(false);
const [submitDisabled, setSubmitDisabled] = useState(true);
const [guideSelectOptions, setGuideSelectOptions] = useState([]);
const [groupListOptions, setGroupListOptions] = useState([]);
const [cityListOptions, setCityListOptions] = useState([]);
const formValuesToSub = useFormStore((s) => s.formValuesToSub);
const { travelAgencyId } = usingStorage();
const [fetchAgencyGuideList] = useReservationStore((s) => [
s.fetchAgencyGuideList,
]);
const [fetchReviewList, reviewList, postReview] = useExternalReviewStore(
(s) => [s.fetchReviewList, s.reviewList, s.postReview]
);
const { notification } = App.useApp();
useEffect(() => {
initAgentData(travelAgencyId);
}, [travelAgencyId]);
const initAgentData = async (travelAgencyId) => {
fetchAgencyGuideList(travelAgencyId).then((guideList) => {
const guideCity = groupBy(guideList, "cityName");
const guideOptions = Object.keys(guideCity).map((city) => ({
label: isEmpty(city) ? "-" : city,
options: guideCity[city].map((guide) => ({
value: guide.guideId,
label: guide.guideName + "(" + guide.mobileNo + ")",
})),
}));
setGuideSelectOptions(guideOptions);
});
const recentGroupList = await fetchRecentGroup(travelAgencyId);
setGroupListOptions(
recentGroupList.map((group) => ({ value: group.id, label: group.number }))
);
};
const searchReview = (submitValues) => {
setDataLoading(true);
fetchReviewList(submitValues, travelAgencyId)
.then((result) => {
//
const currentGroupList = result.map((r) => ({ value: r.referenceId, label: r.referenceNumber }))
const mergedGroupList = [...currentGroupList,...groupListOptions]
const uniqueMergedGroupList = Array.from(
new Map(mergedGroupList.map(item => [item.value, item])).values()
)
setGroupListOptions(
uniqueMergedGroupList
);
})
.catch((ex) => {
notification.error({
message: `Notification`,
description: ex.message,
placement: "top",
duration: 4,
});
})
.finally(() => {
setDataLoading(false)
})
}
const initCityList = async (referenceId) => {
const cityList = await fetchCityList(travelAgencyId, referenceId);
setCityListOptions(
cityList.map((city) => {
return {
value: city.CII_SN,
label: city.CityName,
};
})
);
};
const openReviewDrawer = (review) => {
if (review.approvalStatus === 136002 || review.approvalStatus === 136003) {
setSubmitDisabled(true)
} else {
setSubmitDisabled(false)
}
initCityList(review.referenceId)
// Form.Item DatePicker
if (isNotEmpty(review.datePosted)) {
review.datePosted = dayjs(review.datePosted)
}
formReview.setFieldsValue(review)
setOpenReview(true)
}
const closeReviewDrawer = (review) => {
setOpenReview(false);
formReview.resetFields();
}
const onReviewFinish = (values) => {
postReview(values, travelAgencyId)
.then(() => {
notification.info({
message: "Notification",
description: "Success",
placement: "top",
});
setOpenReview(false);
searchReview(formValuesToSub);
})
.catch((ex) => {
notification.error({
message: "Notification",
description: ex.message,
placement: "top",
duration: 4,
});
});
};
return (
<>
<Drawer
title="External Reviews"
closable={{ "aria-label": "Close Button" }}
size={"large"}
open={openReview}
onClose={() => {
closeReviewDrawer()
}}
>
<Form
layout={"vertical"}
form={formReview}
scrollToFirstError
onFinish={(values) => {
onReviewFinish(values);
}}
>
<Form.Item name="reviewId" className="hidden">
<Input />
</Form.Item>
<Form.Item
name="reviewLink"
label="Review Link"
rules={[{ required: true, message: "Please input Review Link" }]}
>
<Input />
</Form.Item>
<Form.Item
name="datePosted"
label="Date Posted"
rules={[{ required: true, message: "Please input Date Posted" }]}
>
<DatePicker
maxDate={dayjs()}
allowClear={false}
className="w-full"
/>
</Form.Item>
<Form.Item name="customerId" label="Customer ID">
<Input />
</Form.Item>
<Form.Item
name="referenceId"
label="Reference Number"
rules={[
{ required: true, message: "Please input Reference Number" },
]}
>
<Select
showSearch
placeholder="Select a Reference Number"
optionFilterProp="children"
onChange={referenceId => {
initCityList(referenceId)
}}
filterOption={(input, option) =>
(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
options={groupListOptions}
/>
</Form.Item>
<Form.Item
name="guideId"
label="Guide"
rules={[{ required: true, message: "Please input Guide" }]}
>
<Select
showSearch
placeholder="Select a guide"
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
options={guideSelectOptions}
/>
</Form.Item>
<Form.Item name="cityId" label="City of Service">
<Select
showSearch
allowClear
placeholder="Select a city"
optionFilterProp="children"
filterOption={(input, option) =>
(option?.label ?? "")
.toLowerCase()
.includes(input.toLowerCase())
}
options={cityListOptions}
/>
</Form.Item>
<Form.Item className="flex justify-end">
<Space className="w-full">
<Button block onClick={() => {
closeReviewDrawer()
}}>Cancel</Button>
<Button type="primary" htmlType="submit" block disabled={submitDisabled}>
Submit
</Button>
</Space>
</Form.Item>
</Form>
</Drawer>
<Space direction="vertical" className="w-full">
<Row>
<Col flex="auto">
<SearchForm
fieldsConfig={{
shows: ["referenceNo", "dates", "approvalStatus"],
fieldProps: {
dates: { label: t("review.ApprovalDate") },
},
}}
onSubmit={() => {
searchReview(formValuesToSub);
}}
/>
</Col>
<Col flex="200px" className="flex justify-center items-center">
<Button
color="cyan"
variant="solid"
icon={<FileAddOutlined />}
onClick={() => openReviewDrawer(initialState.review)}
>
Add Review
</Button>
</Col>
</Row>
<Row>
<Col span={24}>
<Table
rowKey={record => record.reviewId}
bordered
loading={dataLoading}
pagination={{
position: ["bottomCenter"],
simple: true,
}}
columns={reviewListColumns}
dataSource={reviewList}
/>
</Col>
</Row>
</Space>
</>
);
}
export default ReviewList;

@ -3,18 +3,16 @@ 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 { save_database,getFeedbackDetail, getCustomerFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import { getFeedbackDetail, getCustomerFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from '@/components/BackBtn';
import { usingStorage } from '@/hooks/usingStorage';
import {ImageUploader} from '@/components/ImageUploader';
import useFormStore from "@/stores/Form";
const { Title, Text, Paragraph } = Typography;
function Detail() {
const navigate = useNavigate();
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { GRI_SN, RefNo, CII_SN } = useParams();
const {travelAgencyId: myAgencyId, loginToken} = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const {travelAgencyId, loginToken} = usingStorage();
const desc = ['none', 'Unacceptable', 'Poor', 'Fair', 'Very Good', 'Excellent'];
const { notification } = App.useApp();
const [form] = Form.useForm();
@ -23,11 +21,6 @@ function Detail() {
const [feedbackReview, setFeedbackReview] = useState({});
const [feedbackImages, setFeedbackImages] = useState([]);
const [feedbackInfo, setFeedbackInfo] = useState({});
const [ossKey, setOssKey] = useState('');
useEffect(() => {
const key = `ghh/${GRI_SN}-${RefNo}/tourguide_image/travel-agency-${travelAgencyId}`;
setOssKey(key);
}, []);
useEffect(() => {
// console.info('Detail.useEffect: ' + GRI_SN);
@ -43,7 +36,6 @@ function Detail() {
});
}, [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 : [];
@ -69,10 +61,6 @@ function Detail() {
return removeFeedbackImages(info.url);
};
const handleSaveDatabase = (fileList) => {
save_database(GRI_SN, travelAgencyId, fileList);
};
const onFinish = (values) => {
// console.log("Success:", values);
if (values) {
@ -185,9 +173,12 @@ function Detail() {
listType='picture-card'
onChange={handleChange}
onRemove={handRemove}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload photos</div>
</div>
</Upload>
</Form.Item>
<ImageUploader osskey={ossKey} style={{margin: '16px'}} handleSaveDatabase={handleSaveDatabase} />
<Form.Item
name='info_content'
rules={[

@ -3,19 +3,15 @@ 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 { save_database,getFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import { getFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
import {ImageUploader} from '@/components/ImageUploader';
const { Title, Text, Paragraph } = Typography;
import useFormStore from "@/stores/Form";
function Detail() {
const navigate = useNavigate();
const [{ agency, ...formValues }] = useFormStore((state) => [state.formValuesToSub]);
const navigate = useNavigate();
const { GRI_SN,RefNo } = useParams();
const {travelAgencyId: myAgencyId, loginToken} = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const {travelAgencyId, loginToken} = usingStorage();
const desc = ["none", "Unacceptable", "Poor", "Fair", "Very Good", "Excellent"];
const { notification } = App.useApp();
const [form] = Form.useForm();
@ -24,11 +20,6 @@ function Detail() {
const [feedbackReview, setFeedbackReview] = useState({});
const [feedbackImages, setFeedbackImages] = useState([]);
const [feedbackInfo, setFeedbackInfo] = useState({});
const [ossKey, setOssKey] = useState('');
useEffect(() => {
const key = `ghh/${GRI_SN}-${RefNo}/tourguide_image/travel-agency-${travelAgencyId}`;
setOssKey(key);
}, []);
useEffect(() => {
// console.info("Detail.useEffect: " + GRI_SN);
@ -44,7 +35,6 @@ function Detail() {
}, [GRI_SN]);
const HWO_Guide = feedbackRate && feedbackRate.HWO_Guide ? feedbackRate.HWO_Guide : 0;
const HWO_Driver = feedbackRate && feedbackRate.HWO_Driver ? feedbackRate.HWO_Driver : 0;
const HWO_Car = feedbackRate && feedbackRate.HWO_Car ? feedbackRate.HWO_Car : 0;
@ -71,10 +61,6 @@ function Detail() {
return removeFeedbackImages(info.url);
};
const handleSaveDatabase = (fileList) => {
save_database(GRI_SN, travelAgencyId, fileList);
};
const onFinish = values => {
// console.log("Success:", values);
if (values) {
@ -185,9 +171,12 @@ function Detail() {
listType="picture-card"
onChange={handleChange}
onRemove={handRemove}>
<div>
<PlusOutlined />
<div style={{ marginTop: 8 }}>Upload photos</div>
</div>
</Upload>
</Form.Item>
<ImageUploader osskey={ossKey} style={{margin: '16px'}} handleSaveDatabase={handleSaveDatabase} />
<Form.Item
name="info_content"
rules={[

@ -78,7 +78,7 @@ function Index() {
},
}}
onSubmit={(err, formVal, filedsVal) => {
fetchFeedbackList(formVal.agency || travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime);
fetchFeedbackList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime);
}}
/>
<Row>

@ -2,25 +2,20 @@ 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 "@haina/utils-commons";
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";
import useFormStore from "@/stores/Form";
const { Title, Text } = Typography;
function Detail() {
const navigate = useNavigate();
const { GMDSN, GSN, VEI: agency } = useParams();
const { userId, travelAgencyId: myAgencyId, loginToken } = usingStorage();
const [{agency: pickAgency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const travelAgencyId = Number(agency) || pickAgency; // || myAgencyId;
const { GMDSN, GSN } = useParams();
const { userId, travelAgencyId, loginToken } = usingStorage();
const [form] = Form.useForm();
const [dataLoading, setDataLoading] = useState(false);
const [edited, setEdited] = useState(true); //
@ -363,7 +358,7 @@ function Detail() {
</Col>
<Col span={4}>
<BackBtn />
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/history/0/338787/${travelAgencyId}`)}>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/history/0/338787`)}>
Billing Records
</Button>
</Col>

@ -1,7 +1,7 @@
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 "@haina/utils-commons";
import { formatDate, isNotEmpty } from "@/utils/commons";
import SearchForm from "@/components/SearchForm";
import dayjs from "dayjs";
import BackBtn from "@/components/BackBtn";
@ -10,8 +10,8 @@ import useInvoiceStore from "@/stores/Invoice";
import { usingStorage } from "@/hooks/usingStorage";
function History() {
const { travelAgencyId: myAgencyId } = usingStorage();
const { GMDSN, GSN, VEI: travelAgencyId } = useParams();
const { travelAgencyId } = usingStorage();
const { GMDSN, GSN } = useParams();
const [dataLoading, setDataLoading] = useState(false);
const [invoiceZDDetail, setInvoiceZDDetail] = useState([]);
const { notification } = App.useApp();

@ -1,18 +1,15 @@
import { NavLink, useNavigate } from "react-router-dom";
import { Row, Col, Space, Button, Table, App, Steps } from "antd";
import { formatDate, isNotEmpty } from "@haina/utils-commons";
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";
import useFormStore from "@/stores/Form";
function Index() {
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { travelAgencyId: myAgencyId } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const {travelAgencyId, } = usingStorage();
const [invoiceList, fetchInvoiceList] = useInvoiceStore((state) => [state.invoiceList, state.fetchInvoiceList]);
const navigate = useNavigate();
const { notification } = App.useApp();
@ -23,7 +20,7 @@ function Index() {
title: "Ref.No",
dataIndex: "GroupName",
key: "GroupName",
render: (text, record) => <NavLink to={`/invoice/detail/${record.key}/${record.gmd_gri_sn}/${record.gmd_vei_sn}`}>{text}</NavLink>,
render: (text, record) => <NavLink to={`/invoice/detail/${record.key}/${record.gmd_gri_sn}`}>{text}</NavLink>,
},
{
title: "Arrival Date",
@ -75,7 +72,7 @@ function Index() {
return (
<Space direction="vertical" style={{ width: "100%" }}>
<Row gutter={16}>
<Col md={18} sm={18} xs={24} >
<Col md={16} sm={16} xs={24} >
<SearchForm
initialValue={{
dates: [dayjs().subtract(2, 'M').startOf('M'), dayjs().endOf('M')],
@ -89,13 +86,13 @@ function Index() {
},
}}
onSubmit={(err, formVal, filedsVal) => {
fetchInvoiceList(formVal.agency || travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.enddate, formVal.invoiceStatus);
fetchInvoiceList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.enddate, formVal.invoiceStatus);
}}
/>
</Col>
<Col md={6} sm={6} xs={24} >
<Col md={8} sm={8} xs={24} >
<Space>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/438241/0`)}>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/detail/0/395074`)}>
Misc. Invoice
</Button>
<Button icon={<AuditOutlined />} onClick={() => navigate(`/invoice/paid`)}>

@ -1,20 +1,15 @@
import { NavLink, useNavigate } from 'react-router-dom';
import { useEffect } from 'react';
import { Row, Col, Space, Table, Image } from 'antd';
import { formatDate, isNotEmpty } from '@haina/utils-commons';
import { formatDate, isNotEmpty } from '@/utils/commons';
import SearchForm from '@/components/SearchForm';
import dayjs from 'dayjs';
import BackBtn from '@/components/BackBtn';
import useInvoiceStore from '@/stores/Invoice';
import { usingStorage } from '@/hooks/usingStorage';
import useFormStore from "@/stores/Form";
function Paid() {
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const {travelAgencyId: myAgencyId, } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const {travelAgencyId, } = usingStorage();
const [invoicePaid, fetchInvoicePaid] = useInvoiceStore((state) => [state.invoicePaid, state.fetchInvoicePaid]);
const navigate = useNavigate();
const showTotal = (total) => `Total ${total} items`;

@ -1,20 +1,16 @@
import { useNavigate, useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { Row, Col, Space, Table, Typography } from 'antd';
import { formatDate, isNotEmpty } from '@haina/utils-commons';
import { formatDate, isNotEmpty } from '@/utils/commons';
import BackBtn from '@/components/BackBtn';
import { fetchInvoicePaidDetail } from '@/stores/Invoice';
import { usingStorage } from '@/hooks/usingStorage';
import useFormStore from "@/stores/Form";
const { Title } = Typography;
function PaidDetail() {
const navigate = useNavigate();
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { travelAgencyId: myAgencyId } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const {travelAgencyId, } = usingStorage();
const { flid } = useParams();
const [invoicePaidDetail, setInvoicePaidDetail] = useState([]);

@ -1,34 +1,36 @@
import { useParams } from "react-router-dom";
import { useEffect, useState } from "react";
import { Row, Col, Space, Typography, Divider } from "antd";
import { fetchNoticeDetail } from "@/stores/Notice";
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
import { 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 } = Typography;
function Detail() {
const { t } = useTranslation();
const { CCP_BLID } = useParams();
const { userId } = usingStorage();
const {userId} = usingStorage();
const [noticeInfo, setNoticeInfo] = useState({});
useEffect(() => {
// 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 className="whitespace-pre-line">
{noticeInfo.CCP_BLContent}
</div>
<div dangerouslySetInnerHTML={{ __html: comm.escape2Html(noticeInfo.CCP_BLContent) }}></div>
</Paragraph>
</Col>
<Col span={4}>

@ -1,142 +0,0 @@
import { useParams } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { App, Row, Col, Space, Typography, Divider, List, Tag, Button, Radio, Badge } from 'antd';
import { CheckCircleOutlined, ClockCircleOutlined } from '@ant-design/icons';
import { fetchNotifsDetail, fetchMyNotifsDetail, fetchNotifsStatus, fetchNotifsStatusAgency, updateNotifsConfirm } from '@/stores/Notice';
import BackBtn from '@/components/BackBtn';
import { usingStorage } from '@/hooks/usingStorage';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
import { PERM_NOTIFS_MANAGEMENT } from '@/config';
import { useTranslation } from 'react-i18next';
const { Title, Paragraph } = Typography;
function Detail() {
const { t } = useTranslation();
const { message } = App.useApp();
const isPermitted = useAuthStore((state) => state.isPermitted);
const { CCP_BLID } = useParams();
const { userId } = usingStorage();
const [noticeInfo, setNoticeInfo] = useState({});
const [read, setRead] = useState(-1);
const [statusNumber, setStatusNumber] = useState({});
const [recipientsAgency, setRecipientsAgency] = useState([]);
const [renderRecipients, setRenderRecipients] = useState([]);
const getDetail = (params) => {
const fetchFunction = isPermitted(PERM_NOTIFS_MANAGEMENT) ? fetchNotifsDetail : fetchMyNotifsDetail;
return fetchFunction(params);
};
useEffect(() => {
getDetail(CCP_BLID).then((res) => {
setNoticeInfo(res);
setRead(res?.notificationsReadState || 0);
});
if (isPermitted(PERM_NOTIFS_MANAGEMENT)) {
fetchNotifsStatus(CCP_BLID).then((res) => {
setStatusNumber(res);
});
fetchNotifsStatusAgency(CCP_BLID).then((res) => {
setRecipientsAgency(res);
setRenderRecipients(res);
});
}
}, []);
const [cLoading, setCLoading] = useState(false);
const handleReadConfirm = () => {
setCLoading(true);
updateNotifsConfirm(CCP_BLID).then((res) => {
message.success(t('Success'));
setRead(1);
setCLoading(false);
});
};
const onFilterConfirm = ({ target: { value } }) => {
setRenderRecipients(value === 'all' ? recipientsAgency : recipientsAgency.filter((item) => `${item.receiptConfirmed}` === value));
};
return (
<Space direction='vertical' style={{ width: '100%' }}>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={16}>
<Title level={2}>{noticeInfo.title}</Title>
<Divider orientation='right'>{noticeInfo.date}</Divider>
<Paragraph>
<div className='whitespace-pre-line' dangerouslySetInnerHTML={{__html: noticeInfo.body}}></div>
</Paragraph>
{!isPermitted(PERM_NOTIFS_MANAGEMENT) && read===0 && (
<Button type='primary' ghost onClick={handleReadConfirm} loading={cLoading}>
{t('Confirm')}&nbsp;
{t('Read')}
</Button>
)}
<Divider className='border-neutral-300' />
<RequireAuth subject={PERM_NOTIFS_MANAGEMENT}>
<Divider orientation='left' className='border-neutral-300'>
{t('Recipients')}
<Radio.Group optionType='button' size='small' className='ms-4' defaultValue={'all'} onChange={onFilterConfirm}>
<Radio.Button value='all'>{t('All')}</Radio.Button>
<Radio.Button value='1'>
<Space>
<span>{t('Read')}</span>
<Badge count={statusNumber.confirmationCount} showZero overflowCount={999} color={'green'} size='small' />
</Space>
</Radio.Button>
<Radio.Button value='0'>
<Space>
<span>{t('UnRead')}</span>
<Badge count={statusNumber.pendingConfirmationCount} showZero overflowCount={999} color={'orange'} size='small' />
</Space>
</Radio.Button>
</Radio.Group>
</Divider>
<List
size='small'
itemLayout='horizontal'
dataSource={renderRecipients}
pagination={{
pageSize: 10,
showTotal: (total) => {
return t('Total') + `${total}`;
},
}}
renderItem={(item) => (
<List.Item
actions={[
<>
{item.receiptConfirmed === 1 ? (
<Tag color='green' icon={<CheckCircleOutlined />}>
{t('Read')}
</Tag>
) : (
<Tag color='orange' icon={<ClockCircleOutlined />}>
{t('UnRead')}
</Tag>
)}
</>,
]}>
<List.Item.Meta
title={item.agencyName}
// description={item.agencyName}
/>
</List.Item>
)}
/>
</RequireAuth>
</Col>
<Col span={4}>
<BackBtn />
</Col>
</Row>
</Space>
);
}
export default Detail;

@ -1,227 +0,0 @@
import { createContext, useEffect, useMemo, useState, useRef } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { App, Row, Col, Space, Typography, Divider, Form, Input, Select, Button, Tag } from 'antd';
import { useTranslation } from 'react-i18next';
import { fetchNotifsDetail, fetchNotifsStatusAgency, postNotifsNewOrEdit } from '@/stores/Notice';
import useFormStore from "@/stores/Form";
import BackBtn from '@/components/BackBtn';
import { usingStorage } from '@/hooks/usingStorage';
import VendorSelector from '@/components/VendorSelector';
import { isEmpty, objectMapper } from '@haina/utils-commons';
import Editor from '@/components/Editor';
const { Title, Paragraph } = Typography;
const allOverseasAgency = [
{ key: 30008, value: 30008, label: 'Luxe Voyage Tours-已签海纳协议' },
{ key: 31596, value: 31596, label: '印度Legend' },
{ key: 32531, value: 32531, label: '越南ANP TRAVEL' },
{ key: 32704, value: 32704, label: '泰北 NUT ' },
{ key: 32717, value: 32717, label: 'JCT株式会社' },
{ key: 32865, value: 32865, label: '泰国IAT' },
{ key: 32876, value: 32876, label: 'Japan Fleet' },
// { key: 33031, value: 33031, label: 'testA' },
// { key: 33032, value: 33032, label: 'testB' },
{ key: 33200, value: 33200, label: '新加坡途游' },
{ key: 33508, value: 33508, label: '日本DJ(注意加服务交通费)' },
{ key: 33796, value: 33796, label: '韩国 Halla Tour' },
];
const Edit = ({ ...props }) => {
const { t } = useTranslation();
const { message } = App.useApp();
const navigate = useNavigate();
const { CCP_BLID } = useParams();
const { userId } = usingStorage();
const actionNew = useMemo(() => CCP_BLID === '0', [CCP_BLID]);
const [form] = Form.useForm();
const clickedButton = useRef('');
const quillRef = useRef();
const setCache = useFormStore(state => state.setCache);
const [showAgencySelector, setShowAgencySelector] = useState(1);
const [showAgencySeleted, setShowAgencySeleted] = useState(0);
// const [noticeInfo, setNoticeInfo] = useState({});
// const [recipientsAgency, setRecipientsAgency] = useState([]);
useEffect(() => {
if (CCP_BLID !== '0') {
fetchNotifsDetail(CCP_BLID).then((res) => {
form.setFieldsValue({
title: res.title,
body: res.body,
target: { value: '_list', key: '_list', label: '我自己选' },
});
setShowAgencySelector(1);
setShowAgencySeleted(1);
});
fetchNotifsStatusAgency(CCP_BLID).then((res) => {
const _list = res.map((item) => ({ key: item.agencyId, value: item.agencyId, label: item.agencyName }));
// setRecipientsAgency(_list);
form.setFieldsValue({
agency_list: _list,
});
setCache({ vendorList: _list })
});
} else {
setCache({ vendorList: allOverseasAgency })
}
}, []);
const onValuesChange = (changeValues, allValues) => {
if ('target' in changeValues) {
setShowAgencySelector(changeValues.target.value === '_list' ? 1 : 0);
setShowAgencySeleted(changeValues.target.value === '_list' ? 1 : 0);
// if (changeValues.target.value !== '_list') {
// setShowAgencySeleted(0);
// }
}
if ('agency_list' in changeValues) {
setShowAgencySeleted(changeValues.agency_list.length > 0 ? 1 : 0);
}
// console.log('', changeValues);
};
const handleRemoveAgency = (ele) => {
// console.log('', ele);
const newSeleted = form.getFieldValue('agency_list').filter((item) => item.key !== ele.key);
// remove from form fieldsValues
form.setFieldsValue({ agency_list: newSeleted });
setShowAgencySeleted(newSeleted.length > 0 ? 1 : 0);
};
const [cLoading, setCLoading] = useState(false);
const onSubmitNotifs = (values) => {
// console.log('🟩', clickedButton.current);
// console.log('Received values of form, origin form value: \n', values);
setCLoading(true);
const formValuesMapper = {
title: { key: 'title', transform: (value) => value || '' },
body: { key: 'body', transform: (value) => value || '' },
// target: { key: 'groups', transform: (value) => value?.value || '' }, // === '_list' ? '' : value.value
target: { key: 'groups', transform: (value) => (value?.value || '') === '_list' ? '' : value.value }, // === '_list' ? '' : value.value
agency_list: {
key: 'ids',
transform: (value) => {
// .join(',')
return Array.isArray(value) ? value.map((ele) => Number(ele.value)) : value ? value.value : [];
},
},
};
let dest = {};
const { target, agency_list, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, formValuesMapper) }; // +
// dest = objectMapper(values, formValuesMapper);
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
}
}
const { ids, groups, ...readyFormData } = dest;
readyFormData.recipientsAgency = { ids: isEmpty(groups) ? ids : [], groups: groups || '' };
readyFormData.publish_state = clickedButton.current === 'saveAndPublish' ? '1' : '0';
readyFormData.id = CCP_BLID === '0' ? '' : CCP_BLID;
// console.log('form value send to onSubmit:\n', readyFormData);
postNotifsNewOrEdit(CCP_BLID, readyFormData).then((res) => {
message.success(t('Success'));
navigate('/notice');
setCLoading(false);
});
};
const onFinishFailed = ({ values, errorFields }) => {
// console.log('form validate failed', '\nform values:', values, '\nerrorFields', errorFields);
};
return (
<>
<Space direction='vertical' style={{ width: '100%' }}>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={16}>
<Form
form={form}
layout={'vertical'}
onFinish={onSubmitNotifs}
onValuesChange={onValuesChange}
onFinishFailed={onFinishFailed}>
<Form.Item name='title' label='公告标题' rules={[{ required: true }]}>
<Input placeholder='请输入公告标题' />
</Form.Item>
<Form.Item noStyle>
<Space.Compact className='w-full'>
<Form.Item name='target' label='公告区域' initialValue={{value: '_list', label: '我自己选'}} className='w-1/3' rules={[{ required: true }]}>
<Select placeholder='请选择公告区域' labelInValue>
<Select.Option value='all'>所有</Select.Option>
<Select.Option value='domestic'>国内</Select.Option>
<Select.Option value='global'>海外</Select.Option>
<Select.Option value='_list'>我自己选</Select.Option>
</Select>
</Form.Item>
<Form.Item
name={`agency_list`}
label={'供应商'}
dependencies={['target']}
hidden={showAgencySelector !== 1}
className='w-auto flex-auto'
rules={[{ required: showAgencySelector }]}>
<VendorSelector />
</Form.Item>
</Space.Compact>
</Form.Item>
<Form.Item shouldUpdate hidden={showAgencySeleted !== 1}>
{() =>
showAgencySelector === 1 ? (
<div className='max-h-96 overflow-auto '>
{!isEmpty(form.getFieldValue('agency_list')) && <b>已选择的供应商: </b>}
{(form.getFieldValue('agency_list') || []).map((item, index) => (
<Tag key={item.value} closable={true} onClose={() => handleRemoveAgency(item)}>
{item.label}
</Tag>
))}
{!isEmpty(form.getFieldValue('agency_list')) && (
<Button
size='small'
danger
onClick={() => {
form.setFieldValue('agency_list', []);
setShowAgencySeleted(0);
}}>
{t('Clear')}
</Button>
)}
</div>
) : (
<></>
)
}
</Form.Item>
<Form.Item name='body' label='公告内容' rules={[{ required: true, }]}>
<Editor ref={quillRef} />
</Form.Item>
<Form.Item>
<Space>
<Button type='primary' ghost htmlType='submit' loading={cLoading} onClick={() => (clickedButton.current = 'save')}>
先保存
</Button>
<Button type='primary' htmlType='submit' loading={cLoading} onClick={() => (clickedButton.current = 'saveAndPublish')}>
保存并发布
</Button>
</Space>
</Form.Item>
</Form>
</Col>
<Col span={4}>
<BackBtn />
</Col>
</Row>
</Space>
</>
);
};
export default Edit;

@ -1,137 +0,0 @@
import { Link, NavLink, useNavigate } from 'react-router-dom';
import { useEffect, useState } from 'react';
import { App, Row, Col, Space, Typography, Badge, List, Button, Flex, Tabs, Popconfirm } from 'antd';
import useNoticeStore, { fetchMyNotifsList, fetchNotifsList, delNotifs } from '@/stores/Notice';
import { usingStorage } from '@/hooks/usingStorage';
import useAuthStore from '@/stores/Auth';
import { PERM_NOTIFS_MANAGEMENT, PERM_NOTIFS_DELETE_DANGER } from '@/config';
import RequireAuth from '@/components/RequireAuth';
import { useTranslation } from 'react-i18next';
function Index() {
const { t } = useTranslation();
const {message} = App.useApp();
const navigate = useNavigate();
const isPermitted = useAuthStore((state) => state.isPermitted);
const { userId } = usingStorage();
// const getBulletinUnReadCount = useNoticeStore((state) => state.getBulletinUnReadCount);
const getNoticeUnReadCount = useNoticeStore((state) => state.getNoticeUnReadCount);
const [noticeList, setNoticeList] = useState([]);
const getList = (params) => {
const fetchFunction = isPermitted(PERM_NOTIFS_MANAGEMENT) ? fetchNotifsList : fetchMyNotifsList;
return fetchFunction(params);
};
useEffect(() => {
getList().then((data) => {
setNoticeList(data);
});
if (!isPermitted(PERM_NOTIFS_MANAGEMENT)) {
getNoticeUnReadCount(userId); //
}
}, []);
const handleListByState = (state) => {
getList({ notificationsReadState: state }).then((res) => {
setNoticeList(res);
});
};
const handleDelNotifs = (id) => {
delNotifs(id).then((res) => {
message.success(t('Success'));
getList().then((data) => {
setNoticeList(data);
});
});
};
return (
<Space direction='vertical' style={{ width: '100%' }}>
<Row gutter={16}>
<Col span={4}></Col>
<Col span={16}>
<Flex align='center' justify='space-between'>
{/* <Typography.Title level={4}>公告列表</Typography.Title> */}
{isPermitted(PERM_NOTIFS_MANAGEMENT) ? (
<Typography.Title level={4}>公告列表</Typography.Title>
) : (
<Tabs
items={[
{ key: '', label: t('All') },
{ key: '0', label: t('UnRead') },
{ key: '1', label: t('Read') },
]}
defaultActiveKey=''
onChange={handleListByState}></Tabs>
)}
<RequireAuth subject={PERM_NOTIFS_MANAGEMENT}>
<Link to='/notice/edit/0'>
<Button>公告发布</Button>
</Link>
</RequireAuth>
</Flex>
<List
dataSource={noticeList}
pagination={{
pageSize: 20,
}}
renderItem={(item) => (
<List.Item
className={item.notificationsReadState ? '' : 'font-bold'}
// debug: publish_state
actions={
item.publish_state === 0
? [
<RequireAuth subject={PERM_NOTIFS_MANAGEMENT} key='notifs-edit'>
<NavLink to={`/notice/edit/${item.notificationId}`}>{t('Edit')}</NavLink>
</RequireAuth>,
// <RequireAuth subject={PERM_NOTIFS_MANAGEMENT} key='notifs-pub'>
// <Button key='publish' disabled={item.publish_state === 10} size='small' type='link' onClick={() => {}}>
// {t('Publish')}
// </Button>
// </RequireAuth>,
<RequireAuth subject={PERM_NOTIFS_MANAGEMENT} key='notifs-del'>
<Popconfirm key='del'
title={t('sureDelete')}
onConfirm={e => handleDelNotifs(item.notificationId)}
okText={t('Yes')}
cancelText={t('No')}
>
<Button size='small' type='link' danger>
{t('Delete')}
</Button>
</Popconfirm>
</RequireAuth>,
]
: [
<RequireAuth subject={PERM_NOTIFS_DELETE_DANGER} key='notifs-del'>
<Popconfirm key='del'
title={t('sureDelete')}
onConfirm={e => handleDelNotifs(item.notificationId)}
okText={t('Yes')}
cancelText={t('No')}
>
<Button size='small' type='link' danger>
{t('Delete')}
</Button>
</Popconfirm>
</RequireAuth>,
]
}>
<Typography.Text>[{item.date}]</Typography.Text>
<NavLink to={item.publish_state === 1 ? `/notice/${item.notificationId}` : `/notice/edit/${item.notificationId}`}> {item.title}</NavLink>{' '}
{item.notificationsReadState ? '' : <Badge dot />}
</List.Item>
)}></List>
</Col>
<Col span={4}></Col>
</Row>
</Space>
);
}
export default Index;

@ -1,19 +1,17 @@
import { useEffect, useState } from 'react';
import { useParams, Link } from 'react-router-dom';
import { App, Empty, Button, Collapse, Table, Space, Alert } from 'antd';
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, groupBy, isEmpty, isNotEmpty } from '@haina/utils-commons';
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';
import { ClockCircleOutlined, PlusCircleFilled } from '@ant-design/icons';
import ProductQuotationLogPopover, { columnsSets } from './Detail/ProductQuotationLogPopover';
const PriceTable = ({ productType, dataSource, refresh }) => {
const { t } = useTranslation('products');
@ -28,8 +26,6 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
// console.log(dataSource);
const [logOpenPriceRow, setLogOpenPriceRow] = useState(null); // price id
const handleAuditPriceItem = (state, row, rowIndex) => {
postProductsQuoteAuditAction(state, { id: row.id, travel_agency_id: activeAgency.travel_agency_id })
.then((json) => {
@ -59,43 +55,34 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
const trCls = tri%2 !== 0 ? ' bg-stone-50' : ''; //
const [infoI, quoteI] = r.rowSpanI;
const bigTrCls = quoteI === 0 && tri !== 0 ? 'border-collapse border-double border-0 border-t-4 border-stone-300' : ''; // 线
// && isNotEmpty(r.lastedit_changed)
const editedCls = (r.audit_state_id === 0 ) ? '!bg-amber-100' : ''; // ,
const newCls = (r.audit_state_id === -1 ) ? '!bg-sky-100' : ''; // ,
const editedCls = (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') : '';
const lodHighlightCls = (r.id === logOpenPriceRow ) ? '!bg-violet-300 !text-violet-900' : '';
return [trCls, bigTrCls, newCls, editedCls, lodHighlightCls].join(' ');
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: 'title',
dataIndex: ['info', 'title'],
width: '16rem',
title: t('Title'),
onCell: (r, index) => ({ rowSpan: r.rowSpan }),
className: 'bg-white',
render: (text, r) => {
const title = text || r.lgc_details?.['2']?.title || r.lgc_details?.['1']?.title || '';
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit`
: isPermitted(PERM_PRODUCTS_OFFER_PUT)
? `/products/edit`
: '';
return (
<div className=''>
{isNotEmpty(itemLink) ? (
<div className='' onClick={() => setEditingProduct({ info: r.info })}>
<Link to={itemLink}>{title}</Link>
</div>
) : (
title
)}
</div>
);
},
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}` : ''),
},
...columnsSets(t),
{ key: 'weekdays', dataIndex: ['weekdays'], title: t('Weekdays'), render: (text, r) => text || t('Unlimited') },
{
key: 'state',
title: t('State'),
@ -108,62 +95,17 @@ const PriceTable = ({ productType, dataSource, refresh }) => {
title: '',
key: 'action',
render: (_, r, ri) =>
[-1, 0, 3].includes(Number(r.audit_state_id)) ? (
<>
<Space className='w-full [&>*:last-child]:ms-auto'>
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
{Number(r.audit_state_id) === 0 && (
<div className='flex gap-2'>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</div>
)}
</RequireAuth>
<ProductQuotationLogPopover
method={'history'}
{...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
onOpenChange={(open) => setLogOpenPriceRow(open ? r.id : null)}
/>
(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,
},
// {
// title: '',
// key: 'action2',
// width: '6rem',
// className: 'bg-white align-bottom',
// onCell: (r, index) => ({ rowSpan: r.rowSpan }),
// render: (_, r) => {
// const showPublicBtn = null; // r.pendingQuotation ? <Popover title='' trigger={['click']}> <Button size='small' className='ml-2' onClick={() => { }}></Button></Popover> : null;
// const btn2 = !r.showPublicBtn ? (
// <>
// <ProductQuotationSnapshotPopover
// // <ProductQuotationLogPopover
// method={'snapshot'}
// {...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
// triggerProps={{ type: 'primary', ghost: true, size: 'small' }}
// placement='bottom'
// className='max-w-[1000px]'
// />
// </>
// ) : null;
// return <div className='flex flex-col gap-2 justify-end'>{btn2}</div>;
// },
// },
];
return (
<Table
size={'small'}
className='border-collapse'
rowHoverable={false}
rowClassName={rowStyle}
pagination={false}
{...{ columns }}
dataSource={renderData}
rowKey={(r) => r.id}
/>
);
return <Table size={'small'} className='border-collapse' rowHoverable={false} rowClassName={rowStyle} pagination={false} {...{ columns, }} dataSource={renderData} rowKey={(r) => r.id} />;
};
/**
@ -179,54 +121,40 @@ const TypesPanels = (props) => {
useEffect(() => {
// ; , ; ,
const hasDataTypes = Object.keys(agencyProducts);
let tempKey = '';
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => {
const _children = agencyProducts[ele.value].reduce(
.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(', '),
weekdays: q.weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`weekdaysShort.${w}`))
.join(', '),
info: c.info,
lgc_details: c.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {}),
rowSpan: i === 0 ? c.quotation.length : 0,
rowSpanI: [ri, i],
showPublicBtn: c.quotation.some(q2 => [0, 3].includes(q2.audit_state_id)),
}))
),
[]
);
tempKey = _children.length > 0 && tempKey==='' ? ele.key : tempKey;
const _childrenByState = groupBy(_children, 'audit_state_id');
// if (_children.length > 0) console.log('PriceTable\n'+ele.value+'\n', _children)
return {
...ele,
extra: <Space>
{_childrenByState['1']?.length > 0 && <Alert showIcon type='success' className='py-1 text-xs' message={_childrenByState['1']?.length || 0} />}
{_childrenByState['2']?.length > 0 && <Alert showIcon type='success' className='py-1 text-xs' message={_childrenByState['2']?.length || 0} icon={<ClockCircleOutlined />} />}
{_childrenByState['0']?.length > 0 && <Alert showIcon type='warning' className='py-1 text-xs' message={_childrenByState['0']?.length || 0} />}
{_childrenByState['3']?.length > 0 && <Alert showIcon type='error' className='py-1 text-xs' message={_childrenByState['3']?.length || 0} />}
{_childrenByState['-1']?.length > 0 && <Alert showIcon type='info' className='py-1 text-xs' message={_childrenByState['-1']?.length || 0} icon={<PlusCircleFilled />} />}
<span>{t('Table.Total', { total: _children.length })}</span>
</Space>,
children: (
<PriceTable
// loading={loading}
productType={ele.value}
dataSource={_children}
)}
refresh={props.refresh}
/>
),
}});
}));
setShowTypes(_show);
setActiveKey(isEmpty(_show) ? [] : [tempKey]);
setActiveKey(isEmpty(_show) ? [] : [_show[0].key]);
return () => {};
}, [productsTypes, agencyProducts]);

@ -1,6 +1,6 @@
import { useState } from 'react';
import { App, Divider, Empty, Flex } from 'antd';
import { isEmpty } from '@haina/utils-commons';
import { isEmpty } from '@/utils/commons';
import SecondHeaderWrapper from '@/components/SecondHeaderWrapper';
import Header from './Detail/Header';
import { useParams } from 'react-router-dom';
@ -21,11 +21,15 @@ function Detail() {
const { travelAgencyId } = usingStorage();
const handleGetAgencyProducts = async ({ pick_year, pick_agency, pick_state } = {}) => {
const year = pick_year || use_year || switchParams.use_year || dayjs().year();
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 };
// console.log('', param)
// setEditingProduct({});
if (isEmpty(param.travel_agency_id) || isEmpty(param.use_year)) {
return false;
}
getAgencyProducts(param).catch((ex) => {
setLoading(false);
notification.error({

@ -71,7 +71,7 @@ export const ContractRemarksModal = () => {
open={isRemarksModalOpen}
onOk={() => onRemarksFinish()}
onCancel={() => setRemarksModalOpen(false)}
destroyOnHidden
destroyOnClose
forceRender
>
<Form

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { App, Form, Modal, DatePicker, Divider, Switch, Space, Flex, Radio } from 'antd';
import { isEmpty, objectMapper } from '@haina/utils-commons';
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';
@ -10,100 +10,46 @@ import dayjs from 'dayjs';
import arraySupport from 'dayjs/plugin/arraySupport';
import { copyAgencyDataAction } from '@/stores/Products/Index';
// import useAuthStore from '@/stores/Auth';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_MANAGEMENT } from '@/config';
import ProductsSelector from '@/components/ProductsSelector';
dayjs.extend(arraySupport);
export const CopyProductsForm = ({ action, initialValues, onFormInstanceReady, source, ...props }) => {
const { t } = useTranslation();
const [form] = Form.useForm();
const {
sourceAgency: { travel_agency_id },
sourceYear: use_year,
} = source;
// const isPermitted = useAuthStore((state) => state.isPermitted);
const [showTypeOrItem, setShowTypeOrItem] = useState(1);
const isPermitted = useAuthStore((state) => state.isPermitted);
useEffect(() => {
onFormInstanceReady(form);
}, []);
const onValuesChange = (changeValues, allValues) => {
if ('copyType' in changeValues) {
setShowTypeOrItem(changeValues.copyType === 'type' ? 1 : 2);
}
};
const onValuesChange = (changeValues, allValues) => {};
return (
<Form layout='horizontal' form={form} name='form_in_modal' initialValues={{...initialValues, copyType: 'type'}} onValuesChange={onValuesChange}>
<Flex gap={8}>
<div className='basis-96 shrink-0 flex-auto'>
{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={'copyType'}>
<Radio.Group optionType="button" options={[{ key: 'type', value: 'type', label: t('按类型复制(多选)') }, { key: 'item', value: 'item', label: t('仅复制指定产品(多选)') }]}></Radio.Group>
</Form.Item>
<Form.Item name={`products_types`} label={t('products:ProductType')} dependencies={['products_list']} hidden={showTypeOrItem!==1} rules={[{ required: !(showTypeOrItem!==1) }]}>
<ProductsTypesSelector maxTagCount={1} mode={'multiple'} placeholder={t('Select')} />
</Form.Item>
<Form.Item
name={'products_list'}
label={t('products:ProductName')} dependencies={['products_types']} hidden={showTypeOrItem!==2} rules={[{ required: !(showTypeOrItem!==2) }]}>
<ProductsSelector params={{ travel_agency_id, use_year }} mode={'multiple'} placeholder={t('Select')} />
</Form.Item>
<Divider orientation='left'>{t('products:CopyFormMsg.copyTo')}:</Divider>
{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>
)}
<Divider orientation='left'></Divider>
<Form.Item noStyle>
<Space.Compact className='w-full gap-2'>
<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>
</Space.Compact>
</Form.Item>
<Form.Item name={'with_quote'} label={`${t('products:CopyFormMsg.withQuote')}`}>
<Switch checkedChildren={'含报价金额'} unCheckedChildren={'仅人等+日期'} />
</Form.Item>
</div>
<Form.Item noStyle shouldUpdate>
{() => showTypeOrItem===2 ? (
<div className='max-h-96 overflow-auto divide-x-0 divide-y divide-solid divide-stone-200'>
{!isEmpty(form.getFieldValue('products_list')) && <b>已选择的产品 预览:</b>}
{(form.getFieldValue('products_list') || []).map((item, index) => (
<div key={item.value}>
{index + 1}.&nbsp;{item.label}
</div>
))}
</div>
) : (<></>)}
<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>
</Flex>
</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>
);
};
@ -130,10 +76,9 @@ const formValuesMapper = (values) => {
},
},
'with_quote': { key: 'with_quote', transform: (value) => (value ? 1 : 0) },
'products_list': { key: 'product_id_list', transform: (value) => (Array.isArray(value) ? value.map((ele) => ele.key).join(',') : value ? value.value : '') },
};
let dest = {};
const { agency, year, products_list, ...omittedValue } = values;
const { agency, year, ...omittedValue } = values;
dest = { ...omittedValue, ...objectMapper(values, destinationObject) };
for (const key in dest) {
if (Object.prototype.hasOwnProperty.call(dest, key)) {
@ -155,23 +100,16 @@ export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubm
const [copyLoading, setCopyLoading] = useState(false);
const handleCopyAgency = async (param) => {
param.target_agency = isEmpty(param.target_agency) ? source.sourceAgency.travel_agency_id : param.target_agency;
// ,
param.products_types = param.copyType === 'item' ? '' : param.products_types;
param.product_id_list = param.copyType === 'type' ? '' : param.product_id_list;
setCopyLoading(true);
// debug:
// console.log('ready params', param);
// setCopyLoading(false);
// throw new Error('');
// console.log(param);
// const toID = param.target_agency;
const success = await copyAgencyDataAction({ ...param, source_agency: source.sourceAgency.travel_agency_id }).catch((ex) => {
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'));
@ -184,7 +122,7 @@ export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubm
};
return (
<Modal
width={800}
width={600}
open={open}
title={`${t('Copy')}${t('products:#')}${t('products:Offer')}`}
okText='确认'
@ -197,7 +135,7 @@ export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubm
onCancel();
formInstance?.resetFields();
}}
destroyOnClose destroyOnHidden
destroyOnClose
onOk={async () => {
try {
const values = await formInstance?.validateFields();
@ -215,8 +153,7 @@ export const CopyProductsFormModal = ({ source, action = '#' | 'o', open, onSubm
{source.sourceYear}
</div>
</RequireAuth>
<CopyProductsForm
action={action}
<CopyProductsForm action={action}
source={source}
initialValues={initialValues}
onFormInstanceReady={(instance) => {

@ -3,7 +3,7 @@ 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 '@haina/utils-commons';
import { cloneDeep, pick } from '@/utils/commons';
import SearchForm from '@/components/SearchForm';
import RequireAuth from '@/components/RequireAuth';

@ -1,24 +1,28 @@
import { useEffect, useState } from "react";
import { useParams, Link, useNavigate, useLocation } from "react-router-dom";
import { App, Button, Divider, Popconfirm, Select } from "antd";
import { App, Button, Divider, Popconfirm, Select, Typography } 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 "@haina/utils-commons";
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 AgencyPreview from "../Print/AgencyPreview";
import ExportDocxBtn from "../Print/ExportDocxBtn";
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();
@ -26,6 +30,7 @@ const Header = ({ refresh, ...props }) => {
const showEditA = !location.pathname.includes("edit");
const showAuditA = !location.pathname.includes("audit");
const { travel_agency_id, use_year, audit_state } = useParams();
// console.log('📕', travel_agency_id, use_year, audit_state )
const { travelAgencyId } = usingStorage();
const { t } = useTranslation();
const isPermitted = useAuthStore((state) => state.isPermitted);
@ -38,19 +43,25 @@ const Header = ({ refresh, ...props }) => {
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 currentYear = dayjs().year();
const baseYear = use_year
? Number(use_year === "all" ? currentYear : use_year)
: currentYear;
for (let i = currentYear - 5; i <= baseYear + 5; i++) {
? Number(use_year === "all" ? switchParams.use_year : use_year)
: switchParams.use_year;
// console.log('🔰', baseYear, )
for (let i = currentYear - 5; i <= (currentYear + 2); 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,
@ -157,10 +168,32 @@ const Header = ({ refresh, ...props }) => {
});
};
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">
{/* <div className="grow"> */}
<h2 className="m-0 leading-tight me-auto flex items-center">
{isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? (
<VendorSelector
value={{
@ -181,10 +214,11 @@ const Header = ({ refresh, ...props }) => {
<Select
options={yearOptions}
variant={"borderless"}
className="w-24"
className={"w-24"}
size="large"
value={pickYear}
onChange={handleYearChange}
placeholder="年份"
/>
<Divider type={"vertical"} />
<AuditStateSelector
@ -202,10 +236,16 @@ const Header = ({ refresh, ...props }) => {
className="text-primary round-none"
icon={<ReloadOutlined />}
/>
{isEmpty(pickYear) && <Typography.Text type="danger" className="font-normal text-sm ms-1">请选择年份</Typography.Text>}
</h2>
</div>
<AgencyPreview params={switchParams} />
<ExportDocxBtn params={switchParams} />
{/* </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}>
@ -228,7 +268,7 @@ const Header = ({ refresh, ...props }) => {
className="px-2"
to={
isPermitted(PERM_PRODUCTS_OFFER_AUDIT)
? `/products/${activeAgency.travel_agency_id}/${pickYear}/all/edit`
? `/products/${activeAgency.travel_agency_id}/${pickYear}/${audit_state}/edit`
: `/products/edit`
}
>

@ -1,6 +1,6 @@
import { useState, useEffect } from 'react';
import { Form, Modal, Input, Button } from 'antd';
import { objectMapper } from '@haina/utils-commons';
import { objectMapper } from '@/utils/commons';
import { useTranslation } from 'react-i18next';
import ProductsTypesSelector from '@/components/ProductsTypesSelector';

@ -5,7 +5,7 @@ import { useProductsTypesMapVal, useNewProductRecord } from '@/hooks/useProducts
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, objectMapper, pick, unique } from '@haina/utils-commons';
import { isEmpty, pick } from '@/utils/commons';
import ProductInfoForm from './ProductInfoForm';
import { usingStorage } from '@/hooks/usingStorage';
import Extras from './Extras';
@ -27,44 +27,36 @@ const ProductInfo = ({ ...props }) => {
const [infoEditable, setInfoEditable] = useState(false);
const [priceEditable, setPriceEditable] = useState(false);
useEffect(() => {
const isInternal = isPermitted(PERM_PRODUCTS_MANAGEMENT); // : // topPerm
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(hasEditPer); // topPerm ||
setEditablePerm(topPerm || hasEditPer);
// setEditable(topPerm || (hasAuditPer ? true : (!hasHT && hasEditPer)));
// setEditable(true); // debug: 0
// console.log('editable', hasAuditPer, (notAudit && hasEditPer));
setInfoEditable((isInternal && hasEditPer) || (!hasHT && 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((hasPricePer)); // topPerm ||
setPriceEditable(topPerm || (hasPricePer));
// setPriceEditable(true); // debug: 0
const showExtras = isInternal && hasHT; // !isEmpty(editingProduct) &&
const showExtras = topPerm && hasHT; // !isEmpty(editingProduct) &&
setExtrasVisible(showExtras);
setLgcEdits({});
setInfoEditStatus('');
setEditKeys([]);
return () => {};
}, [activeAgency, editingProduct]);
const [infoEditStatus, setInfoEditStatus] = useState('');
const [lgcEdits, setLgcEdits] = useState({});
// const [editChanged, setEditChanged] = useState({});
const [editKeys, setEditKeys] = useState([]);
const [editLgcKeys, setEditLgcKeys] = useState([]);
const onValuesChange = (changedValues) => {
console.log('onValuesChange', changedValues);
const changedKeys = objectMapper(changedValues, { 'city': 'city_id', 'dept': 'dept_id', 'product_title': 'title', 'lgc_details_mapped': 'lgc_details'});
setEditKeys(prev => unique([...prev, ...Object.keys(changedKeys)]));
// const preValues = pick(editingProduct.info, editKeys);
const onValuesChange = (changedValues, forms) => {
// console.log('onValuesChange', changedValues);
if ('product_title' in changedValues) {
setInfoEditStatus('2');
setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}});
@ -72,10 +64,6 @@ const ProductInfo = ({ ...props }) => {
if ('lgc_details_mapped' in changedValues) {
const lgc = Object.keys(changedValues.lgc_details_mapped)[0];
setLgcEdits({...lgcEdits, [lgc]: {'edit_status': '2'}});
setEditLgcKeys([...editLgcKeys, ...Object.keys(changedValues.lgc_details_mapped)])
setInfoEditStatus('2');
} else if ('quotation' in changedValues) {
// edit_status
} else {
setInfoEditStatus('2');
}
@ -83,12 +71,6 @@ const ProductInfo = ({ ...props }) => {
const onSave = async (err, values, forms) => {
values.travel_agency_id = activeAgency.travel_agency_id;
const editChanged = pick(editingProduct.info, editKeys);
// (editKeys.includes('lgc_details') ? editChanged.lgc_details = editingProduct.lgc_details.map(l => l.lgc) : false);
// , id lastedit_changed
(editKeys.includes('lgc_details') ? editChanged.lgc_details = [...new Set(editLgcKeys)].map(e => Number(e)) : false);
// console.log("editKeys pre values", editKeys, editChanged, '\neditingProduct', );
const copyNewProduct = structuredClone(newProductRecord);
const poster = {
// ...(topPerm ? { } : { 'audit_state': -1 }), // :
@ -96,13 +78,12 @@ const ProductInfo = ({ ...props }) => {
// "created_by": userId,
'travel_agency_id': activeAgency.travel_agency_id,
// "travel_agency_name": "",
'lastedit_changed': editChanged, // isEmpty(editChanged) ? "" : JSON.stringify(editChanged),
'edit_status': infoEditStatus || editingProduct.info.edit_status,
// "lastedit_changed": "",
"edit_status": infoEditStatus || editingProduct.info.edit_status,
};
// console.log("🟢ready to post", poster);
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);
// console.log('onSave', editingProduct.info, readyToSubInfo);
/** lgc_details */
const prevLgcDetailsMapped = editingProduct.lgc_details.reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
@ -114,9 +95,8 @@ const ProductInfo = ({ ...props }) => {
}
}
// console.log('before save', readyToSubInfo, '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
// console.log('before save', '\n lgcEdits:', lgcEdits, '\n mergedLgc', mergedLgc);
// return false; // debug: 0
// throw new Error("Test save");
/** 提交保存 */
setLoading(true);
const { success, result } = await postProductsSaveAction({

@ -1,11 +1,11 @@
import { useEffect, useState } from 'react';
import { App, Form, Input, Row, Col, Select, Button, InputNumber, Checkbox } from 'antd';
import { objectMapper, isEmpty, isNotEmpty, pick } from '@haina/utils-commons';
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, PackageTypes } from '@/hooks/useProductsSets';
import { useProductsTypesFieldsets } from '@/hooks/useProductsSets';
import useProductsStore from '@/stores/Products/Index';
import ProductInfoLgc from './ProductInfoLgc';
import ProductInfoQuotation from './ProductInfoQuotation';
@ -36,13 +36,12 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const [showSave, setShowSave] = useState(true);
useEffect(() => {
form.resetFields();
const _formValue = serverData2Form(editingProduct);
const readyFormVal = pick(_formValue, ['quotation', 'lgc_details_mapped','city', 'city_list', 'dept', 'display_to_c', 'sub_type_D'])
// form.setFieldsValue(serverData2Form(editingProduct));
// ! setFieldsValue
for (const _key in readyFormVal) {
form.setFieldValue(_key, readyFormVal[_key])
}
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);
@ -55,7 +54,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
const onFinish = (values) => {
console.log('Received values of form, origin form value: \n', values);
const dest = formValuesMapper2Server(values);
const dest = formValuesMapper(values);
console.log('form value send to onSubmit:\n', dest);
if (typeof onSubmit === 'function') {
onSubmit(null, dest, values);
@ -81,7 +80,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
}
};
const onIValuesChange = (changedValues, allValues) => {
const dest = formValuesMapper2Server(allValues);
const dest = formValuesMapper(allValues);
// console.log('form onValuesChange', Object.keys(changedValues), changedValues);
if ('product_title' in changedValues) {
const editTitle = (changedValues.product_title);
@ -104,9 +103,8 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
onFinish={onFinish}
onValuesChange={onIValuesChange}
// onFieldsChange={onFieldsChange}
initialValues={{ ...(editingProduct?.info || {}), sub_type_D: editingProduct?.info?.item_type || '' }}
onFinishFailed={onFinishFailed}
scrollToFirstError>
initialValues={editingProduct?.info}
onFinishFailed={onFinishFailed} scrollToFirstError >
<Row>
{getFields({ sort, initialValue: editingProduct?.info, hides, shows, fieldProps, fieldComProps, form, t, dataSets: { weekdays }, editable: infoEditable })}
{/* {showSubmit && (
@ -120,8 +118,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
)} */}
</Row>
{/* <Divider className='my-1' /> */}
<Form.Item
className='mb-0'
<Form.Item className='mb-0'
name={'lgc_details_mapped'}
rules={[
() => ({
@ -141,7 +138,7 @@ const InfoForm = ({ onSubmit, onReset, onValuesChange, editablePerm, infoEditabl
},
}),
]}>
<ProductInfoLgc editable={infoEditable} formInstance={form} pickEditedInfo={structuredClone(pickEditedInfo)} />
<ProductInfoLgc editable={infoEditable} formInstance={form} pickEditedInfo={pickEditedInfo} />
</Form.Item>
<Form.Item name='quotation'>
@ -241,14 +238,6 @@ function getFields(props) {
</Form.Item>,
fieldProps?.duration?.col || midCol
),
item(
'city_list',
99,
<Form.Item name='city_list' label={t('多城市')} tooltip={t('把产品绑定到多个城市')}>
<CitySelector mode='multiple' maxTagCount={10} {...styleProps} {...editableProps('city_list')} />
</Form.Item>,
fieldProps?.city_list?.col || midCol
),
item(
'km',
99,
@ -262,7 +251,7 @@ function getFields(props) {
99,
<Form.Item name='recommends_rate' label={t('RecommendsRate')} {...fieldProps.recommends_rate} tooltip={t('FormTooltip.RecommendsRate')}>
{/* <Input placeholder={t('RecommendsRate')} allowClear /> */}
<InputNumber style={{width: '100%'}} {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} />
<InputNumber {...styleProps} {...editableProps('recommends_rate')} min={1} max={1000} />
{/* <Select
{...styleProps}
{...editableProps('recommends_rate')}
@ -277,15 +266,7 @@ function getFields(props) {
]}
/> */}
</Form.Item>,
fieldProps?.recommends_rate?.col || (props.shows.includes('sort_order') ? midCol/2 : midCol)
),
item(
'sort_order',
99,
<Form.Item name='sort_order' label={t('SortOrder')} {...fieldProps.sort_order} >
<InputNumber style={{width: '100%'}} {...styleProps} {...editableProps('sort_order')} max={1000} />
</Form.Item>,
fieldProps?.sort_order?.col || midCol/2
fieldProps?.recommends_rate?.col || midCol
),
item(
'display_to_c',
@ -325,25 +306,6 @@ function getFields(props) {
</Form.Item>,
fieldProps?.display_to_c?.col || midCol
),
item(
'sub_type_D',
99,
<Form.Item
name='sub_type_D'
label={t('subTypeD')}
{...fieldProps.sub_type_D}
rules={[{ required: true }]}
// tooltip={t('FormTooltip.subTypeD')}
>
<Select
labelInValue={false}
options={PackageTypes}
{...styleProps}
{...editableProps('sub_type_D')}
/>
</Form.Item>,
fieldProps?.sub_type_D?.col || midCol
),
item(
'open_weekdays',
99,
@ -408,37 +370,12 @@ function getFields(props) {
return children;
}
const serverData2Form = (_productItem) => {
const productItem = structuredClone(_productItem);
const infoForRender = {
city: productItem?.info?.city_id ? { value: productItem?.info?.city_id, label: productItem?.info?.city_name } : undefined,
dept: { value: productItem?.info?.dept_id, label: productItem?.info?.dept_name },
display_to_c: productItem.info?.display_to_c || '0',
city_list: productItem?.info?.city_list ? productItem?.info?.city_list?.map((ele) => ({ value: ele.id, label: ele.name })) : undefined,
sub_type_D: productItem?.info?.item_type || '',
};
const lgc_details_mapped = (productItem?.lgc_details || []).reduce((r, c) => ({ ...r, [c.lgc]: c }), {});
const quotation = productItem?.quotation || [];
return { ...productItem, ...(productItem?.info || {}), ...infoForRender, lgc_details_mapped };
};
const formValuesMapper2Server = (_values) => {
const values = structuredClone(_values);
const formValuesMapper = (values) => {
const destinationObject = {
'city': [
{ key: 'city_id', transform: (value) => value?.value || value?.key || '' },
{ key: 'city_name', transform: (value) => value?.label || '' },
],
'city_list': [
{ key: 'city_list', transform: (value) => {
return value.map(option => {
return {
id: option?.value || option?.key || '',
name: option?.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 || '') },
@ -479,15 +416,13 @@ const formValuesMapper2Server = (_values) => {
},
],
'product_title': { key: 'title' },
'sub_type_D': { key: 'item_type'},
'sort_order': { key: 'sort_order'},
};
let dest = {};
const { city, dept, product_title, sub_type_D, ...omittedValue } = values;
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] ?? '');
dest[key] = typeof dest[key] === 'string' ? (dest[key] || '').trim() : dest[key];
}
}
// omit empty

@ -4,7 +4,7 @@ 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 '@haina/utils-commons';
import { cloneDeep, isEmpty, isNotEmpty } from '@/utils/commons';
const ProductInfoLgc = ({ editable, formInstance, pickEditedInfo, ...props }) => {
const { t } = useTranslation('products');

@ -1,497 +1,350 @@
import { useState, useEffect } from "react";
import {
Table,
Form,
Modal,
Button,
Radio,
Input,
Flex,
Card,
InputNumber,
Checkbox,
DatePicker,
Space,
App,
Popconfirm,
Tooltip,
} from "antd";
import { useTranslation } from "react-i18next";
import {
CloseOutlined,
StarTwoTone,
PlusOutlined,
QuestionCircleOutlined,
PushpinTwoTone
} from "@ant-design/icons";
import { useDatePresets } from "@/hooks/useDatePresets";
import dayjs from "dayjs";
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'
import useProductsStore from "@/stores/Products/Index";
import useAuthStore from '@/stores/Auth'
import PriceCompactInput from "@/views/products/Detail/PriceCompactInput";
import { formatGroupSize, formatGroupSizeRender } from "@/hooks/useProductsSets";
const { RangePicker } = DatePicker
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,
},
};
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
}
const getYearRange = (year) => [
dayjs().year(year).startOf("y"),
dayjs().year(year).endOf("y"),
];
const defaultUseDate = {
'useDate': [dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')]
}
const generateDefinitionValue = (year) => ({
useDateList: [{ useDate: getYearRange(year) }],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [defaultPriceValue],
});
const defaultDefinitionValue = {
'useDateList': [defaultUseDate],
'unitId': '0',
'currency': 'RMB',
'weekend': [],
'priceList': [defaultPriceValue]
}
const ProductInfoQuotation = ({ editable, ...props }) => {
const { onChange } = props;
const [isPermitted, currentUser] = useAuthStore(
(state) => [state.isPermitted, state.currentUser])
const { t } = useTranslation();
const { onChange } = props
const [
quotationList,
newEmptyQuotation,
appendQuotationList,
saveOrUpdateQuotation,
deleteQuotation,
switchParams,
] = useProductsStore((state) => [
state.quotationList,
state.newEmptyQuotation,
state.appendQuotationList,
state.saveOrUpdateQuotation,
state.deleteQuotation,
state.switchParams,
]);
const batchSetupInitialValues = {
defList: [
//
{
useDateList: [{ useDate: getYearRange(switchParams.use_year) }],
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().year(switchParams.use_year).subtract(2, "M").startOf("M"),
dayjs().year(switchParams.use_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 { t } = useTranslation()
const [defaultUseDates, setDefaultUseDates] = useState(
getYearRange(switchParams.use_year)
);
const [defaultDefinitionValue, setDefaultDefinitionValue] = useState(
generateDefinitionValue(switchParams.use_year)
);
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false);
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false);
const [groupAllSize, setGroupAllSize] = useState(false);
const [groupMaxUnlimit, setGroupMaxUnlimit] = useState(false);
const { notification } = App.useApp();
const [quotationForm] = Form.useForm();
const [batchSetupForm] = Form.useForm();
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 datePresets = useDatePresets()
useEffect(() => {
setDefaultUseDates(getYearRange(switchParams.use_year));
setDefaultDefinitionValue(generateDefinitionValue(switchParams.use_year));
}, [switchParams]);
const [quotationList, newEmptyQuotation, appendQuotationList, saveOrUpdateQuotation, deleteQuotation] =
useProductsStore((state) => [state.quotationList, state.newEmptyQuotation, state.appendQuotationList, state.saveOrUpdateQuotation, state.deleteQuotation])
const triggerChange = (changedValue) => {
onChange?.(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);
};
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 = () => {
setGroupAllSize(false); //
const emptyQuotation = newEmptyQuotation(defaultUseDates);
quotationForm.setFieldsValue(emptyQuotation);
setQuotationModalOpen(true);
};
const emptyQuotation = newEmptyQuotation()
quotationForm.setFieldsValue(emptyQuotation)
setQuotationModalOpen(true)
}
const onQuotationFinish = (values) => {
const newList = saveOrUpdateQuotation(values);
triggerChange(newList);
setQuotationModalOpen(false);
};
const newList = saveOrUpdateQuotation(values)
triggerChange(newList)
setQuotationModalOpen(false)
}
const onBatchSetupFinish = () => {
const defList = batchSetupForm.getFieldsValue().defList;
const newList = appendQuotationList(defList);
triggerChange(newList);
setBatchSetupModalOpen(false);
};
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: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" title={t("products:FormTooltip.PriceUnit")}>
<QuestionCircleOutlined className="text-gray-500" />
</Tooltip>
</>
),
dataIndex: "unit_id",
width: "6rem",
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) =>
formatGroupSizeRender(record.group_size_min, record.group_size_max),
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"
styles={{ body: { width: "24rem" } }}
title={t("products:FormTooltip.UseDates")}
>
<QuestionCircleOutlined className="text-gray-500" />
</Tooltip>
</>
),
dataIndex: "use_dates",
render: (_, record) =>
`${record.use_dates_start}-${record.use_dates_end}`,
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:Weekdays'), dataIndex: 'weekdays', width: '4rem' },
{
title: t("products:operation"),
dataIndex: "operation",
width: "10rem",
title: t('products:operation'),
dataIndex: 'operation',
width: '10rem',
render: (_, quotation) => {
if (editable) {
return (
<Space>
<Button
type="link"
onClick={() => onQuotationSeleted(quotation)}
>
{t("Edit")}
</Button>
<Popconfirm
placement="topRight"
icon={<QuestionCircleOutlined style={{ color: "red" }} />}
title="请确认"
description="你要删除这条价格吗?"
onConfirm={() => {
deleteQuotation(quotation)
.then((res) => {
triggerChange(res);
})
.catch((ex) => {
notification.error({
message: "Notification",
description: ex.message,
placement: "top",
duration: 4,
});
});
}}
>
<Button
type="link"
danger
>
{t("Delete")}
</Button>
</Popconfirm>
</Space>
);
} else {
(<Button type='text'><PushpinTwoTone twoToneColor="#c0192a" /></Button>)
}
// 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"
<h2>{t('products:EditComponents.Quotation')}</h2>
<Table size='small'
bordered
dataSource={quotationList}
columns={quotationColumns}
pagination={false}
/>
{editable && (
{
editable &&
<Space>
<Button
onClick={() => onNewQuotation()}
type="primary"
ghost
style={{ marginTop: 16 }}
>
{t("products:addQuotation")}
<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 onClick={() => setBatchSetupModalOpen(true)} type='primary' ghost style={{ marginTop: 16, marginLeft: 16 }}>
批量设置
</Button>
</Space>
)}
}
<Modal
centered
title="批量设置价格"
width={"640px"}
title='批量设置价格'
width={'640px'}
open={isBatchSetupModalOpen}
onOk={() => onBatchSetupFinish()}
onCancel={() => setBatchSetupModalOpen(false)}
destroyOnHidden
destroyOnClose
forceRender
>
<Form
labelCol={{ span: 3 }}
wrapperCol={{ span: 20 }}
form={batchSetupForm}
name="batchSetupForm"
autoComplete="off"
name='batchSetupForm'
autoComplete='off'
initialValues={batchSetupInitialValues}
>
<Form.List name="defList">
<Form.List name='defList'>
{(fields, { add, remove }) => (
<Flex gap="middle" vertical>
<Flex gap='middle' vertical>
{fields.map((field, index) => (
<Card
size="small"
title={
index == 0 ? "全年" : index == 1 ? "特殊时间段" : "其他"
}
size='small'
title={index == 0 ? '旺季' : index == 1 ? '淡季' : '其他'}
key={field.key}
extra={
index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() => {
remove(field.name);
}}
/>
)
}
extra={index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => {
remove(field.name)
}} />}
>
<Form.Item label="币种" name={[field.name, "currency"]}>
<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 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"]}>
<Form.Item label='类型' name={[field.name, 'unitId']}>
<Radio.Group>
<Radio value="0">每人</Radio>
<Radio value="1">每团</Radio>
<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 label='周末' name={[field.name, 'weekend']}>
<Checkbox.Group
options={['5', '6', '7']}
/>
</Form.Item>
<Form.Item label="有效期">
<Form.List name={[field.name, "useDateList"]}>
<Form.Item label='有效期'>
<Form.List name={[field.name, 'useDateList']}>
{(useDateFieldList, useDateOptList) => (
<Flex gap="middle" vertical>
<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 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)
}
/>
)}
{index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => useDateOptList.remove(useDateField.name)} />}
</Space>
))}
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() =>
useDateOptList.add({ useDate: defaultUseDates })
}
block
>
<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"]}>
<Form.Item label='人等'>
<Form.List name={[field.name, 'priceList']}>
{(priceFieldList, priceOptList) => (
<Flex gap="middle" vertical>
<Flex gap='middle' vertical>
{priceFieldList.map((priceField, index) => (
<Space key={priceField.key}>
<Form.Item
noStyle
name={[priceField.name, "priceInput"]}
>
<Form.Item noStyle name={[priceField.name, 'priceInput']}>
<PriceCompactInput />
</Form.Item>
{index == 0 ? (
<StarTwoTone twoToneColor="#eb2f96" />
) : (
<CloseOutlined
onClick={() =>
priceOptList.remove(priceField.name)
}
/>
)}
{index == 0 ? <StarTwoTone twoToneColor='#eb2f96' /> : <CloseOutlined onClick={() => priceOptList.remove(priceField.name)} />}
</Space>
))}
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() =>
priceOptList.add(defaultPriceValue)
}
block
>
<Button type='dashed' icon={<PlusOutlined />} onClick={() => priceOptList.add(defaultPriceValue)} block>
新增人等
</Button>
</Flex>
@ -500,12 +353,7 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
</Form.Item>
</Card>
))}
<Button
type="dashed"
icon={<PlusOutlined />}
onClick={() => add(defaultDefinitionValue)}
block
>
<Button type='dashed' icon={<PlusOutlined />} onClick={() => add(defaultDefinitionValue)} block>
新增设置
</Button>
</Flex>
@ -518,180 +366,130 @@ const ProductInfoQuotation = ({ editable, ...props }) => {
centered
okButtonProps={{
autoFocus: true,
htmlType: "submit",
htmlType: 'submit',
}}
title={t("products:EditComponents.Quotation")}
open={isQuotationModalOpen}
onCancel={() => setQuotationModalOpen(false)}
destroyOnHidden
title={t('account:detail')}
open={isQuotationModalOpen} onCancel={() => setQuotationModalOpen(false)}
destroyOnClose
forceRender
modalRender={(dom) => (
<Form
name="quotationForm"
name='quotationForm'
form={quotationForm}
labelCol={{ span: 4 }}
wrapperCol={{ span: 20 }}
className="max-w-2xl"
className='max-w-2xl'
onFinish={onQuotationFinish}
autoComplete="off"
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 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"
label={t('products:adultPrice')}
name='adult_cost'
rules={[
{
required: true,
message: t("products:Validation.adultPrice"),
message: t('products:Validation.adultPrice'),
},
]}
>
<InputNumber style={{ width: "100%" }} />
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label={t("products:childrenPrice")}
name="child_cost"
label={t('products:childrenPrice')}
name='child_cost'
rules={[
{
required: true,
message: t("products:Validation.childrenPrice"),
message: t('products:Validation.childrenPrice'),
},
]}
>
<InputNumber style={{ width: "100%" }} />
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label={t("products:currency")}
name="currency"
label={t('products:currency')}
name='currency'
rules={[
{
required: true,
message: t("products:Validation.currency"),
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 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"
label={t('products:unit_name')}
name='unit_id'
rules={[
{
required: true,
message: t("products:Validation.unit_name"),
message: t('products:Validation.unit_name'),
},
]}
>
<Radio.Group>
<Radio value="0">每人</Radio>
<Radio value="1">每团</Radio>
<Radio value='0'>每人</Radio>
<Radio value='1'>每团</Radio>
</Radio.Group>
</Form.Item>
<Checkbox
onChange={(e) => {
if (e.target.checked) {
quotationForm.setFieldValue("group_size_min", 1);
quotationForm.setFieldValue("group_size_max", 1000);
setGroupAllSize(true);
} else {
setGroupAllSize(false);
}
}}
>
<span className="font-bold">不分人等(1~1000)</span>
</Checkbox>
<Form.Item
label={t("products:group_size")}
name="group_size_min"
label={t('products:group_size')}
name='group_size_min'
rules={[
{
required: true,
message: t("products:Validation.group_size_min"),
},
{
validator: (_, value) => {
if (value > 1000 || value < 1) {
return Promise.reject("人等必须在 1~1000 之间");
}
return Promise.resolve();
},
message: t('products:Validation.group_size_min'),
},
]}
>
<InputNumber disabled={groupAllSize} style={{ width: "100%" }} />
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Checkbox
disabled={groupAllSize}
onChange={(e) => {
if (e.target.checked) {
quotationForm.setFieldValue("group_size_max", 1000);
setGroupMaxUnlimit(true);
} else {
setGroupMaxUnlimit(false);
}
}}
>
<span className="font-bold">不限(1000)</span>
</Checkbox>
<Form.Item
label={t("products:group_size")}
name="group_size_max"
label={t('products:group_size')}
name='group_size_max'
rules={[
{
required: true,
message: t("products:Validation.group_size_max"),
},
{
validator: (_, value) => {
if (value > 1000 || value < 1) {
return Promise.reject("人等必须在 1~1000 之间");
}
return Promise.resolve();
},
message: t('products:Validation.group_size_max'),
},
]}
>
<InputNumber
disabled={groupAllSize || groupMaxUnlimit}
style={{ width: "100%" }}
/>
<InputNumber style={{ width: '100%' }} />
</Form.Item>
<Form.Item
label={t("products:use_dates")}
name="use_dates"
label={t('products:use_dates')}
name='use_dates'
rules={[
{
required: true,
message: t("products:Validation.use_dates"),
message: t('products:Validation.use_dates'),
},
]}
>
<RangePicker presets={datePresets} style={{ width: "100%" }} />
<RangePicker presets={datePresets} style={{ width: '100%' }} />
</Form.Item>
<Form.Item label={t("products:Weekdays")} name="weekdayList">
<Checkbox.Group options={["5", "6", "7"]} />
<Form.Item
label={t('products:Weekdays')}
name='weekdayList'
>
<Checkbox.Group options={['5', '6', '7']} />
</Form.Item>
</Modal>
</>
);
};
)
}
export default ProductInfoQuotation;
export default ProductInfoQuotation

@ -1,285 +0,0 @@
import { useState, useMemo } from 'react';
import { Button, Table, Popover, Typography } from 'antd';
import { useTranslation } from 'react-i18next';
import { HT_HOST } from '@/config';
import { fetchJSON } from '@haina/utils-request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@haina/utils-commons';
import { usingStorage } from '@/hooks/usingStorage';
/**
* 产品价格日志
*/
const getPPLogAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_log`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格: 已发布的
*/
const getPPRunningAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_running`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格快照
*/
const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
const parseJson = (str) => {
let result;
if (str === null || str === undefined || str === '') {
return {};
}
try {
result = typeof str === 'string' ? JSON.parse(str) : str;
return Array.isArray(result) ? result.reduce((acc, cur) => ({ ...acc, ...cur }), {}) : result;
} catch (e) {
return {};
}
};
const statesForHideEdited = [1, 2];
export const columnsSets = (t, colorize = true) => [
{
key: 'adult',
title: t('AgeType.Adult'),
width: '12rem',
render: (_, { adult_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = isNotEmpty(_changed.adult_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.adult_cost || adult_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
{
key: 'child',
title: t('AgeType.Child'),
width: '12rem',
render: (_, { child_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = isNotEmpty(_changed.child_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.child_cost || child_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
width: '6rem',
render: (_, { audit_state_id, group_size_min, group_size_max, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? (
<div className='text-muted line-through '>{`${_changed.group_size_min ?? group_size_min} - ${_changed.group_size_max ?? group_size_max}`}</div>
) : null;
const editCls = colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{formatGroupSize(group_size_min, group_size_max)}</span>
</div>
);
},
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
width: '12rem',
render: (_, { use_dates_start, use_dates_end, weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? (
<div className='text-muted'>
{isNotEmpty(_changed.use_dates_start) ? <span className=' line-through '>{_changed.use_dates_start}</span> : use_dates_start} ~{' '}
{isNotEmpty(_changed.use_dates_end) ? <span className='t line-through '>{_changed.use_dates_end}</span> : use_dates_end}
</div>
) : null;
const editCls = colorize && !statesForHideEdited.includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${use_dates_start} ~ ${use_dates_end}`}</span>
</div>
);
},
},
{
key: 'weekdays',
dataIndex: ['weekdays'],
title: t('Weekdays'),
width: '6rem',
render: (text, { weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && !statesForHideEdited.includes(audit_state_id);
const ifData = !isEmpty(_changed.weekdays);
const _weekdays = ifData
? _changed.weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`common:weekdaysShort.${w}`))
.join(', ')
: '';
const preValue = ifCompare && ifData ? <div className='text-muted line-through '>{_weekdays}</div> : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
const weekdaysTxt = weekdays
.split(',')
.filter(Boolean)
.map((w) => t(`common:weekdaysShort.${w}`))
.join(', ');
return (
<div>
{preValue}
<span className={editCls}>{weekdaysTxt || t('Unlimited')}</span>
</div>
);
},
},
];
const useLogMethod = (method) => {
const { t } = useTranslation('products');
const methodMap = {
'history': {
title: '📑' + t('versionHistory'),
btnText: t('versionHistory'),
fetchData: async (params) => {
const data = await getPPLogAction(params);
return { data };
},
},
'published': {
title: '✅' + t('versionPublished'),
btnText: t('versionPublished'),
fetchData: async (params) => {
const { travel_agency_id, product_id, price_id, use_year } = params;
const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id, use_year });
return { data: data?.[0]?.quotation || [] };
},
},
'snapshot': {
title: '📷' + t('versionSnapshot'),
btnText: t('versionSnapshot'),
fetchData: async (params) => {
const { price_id, ..._params } = params;
const data = await getPPSnapshotAction(_params);
return data; //?.[0]?.quotation || [];
},
},
};
return methodMap[method];
};
/**
* ProductQuotationLogPopover - A popover component that displays product quotation change logs or published data
*
* This component shows a history of price changes for a specific product quotation in a popover table.
* It supports displaying different data sources (history logs or published data) and shows
* comparison between previous and current values with visual indicators.
*
* @param {Object} props - Component props
* @param {string} props.btnText - The text to display on the trigger button and in the popover header
* @param {'history' | 'published' | 'snapshot'} props.method - Determines data source - "history" for change logs or "published" for published quotations
* @param {Object} props.triggerProps - Additional props to pass to the trigger button
* @param {number} props.travel_agency_id - ID of the travel agency (used in data fetching)
* @param {number} props.product_id - ID of the product (used in data fetching)
* @param {number} props.price_id - ID of the price entry (used in data fetching)
* @param {number} props.use_year - Year to use for fetching data (used in data fetching)
* @param {Function} props.onOpenChange - Callback function to be called when the popover opens or closes
*/
const ProductQuotationLogPopover = ({ method, triggerProps = {}, onOpenChange, ...props }) => {
const { travel_agency_id, product_id, price_id, use_year } = props;
const { travelAgencyId } = usingStorage();
const { t } = useTranslation('products');
const [open, setOpen] = useState(false);
const [logData, setLogData] = useState([]);
const { title, btnText: methodBtnText, fetchData } = useLogMethod(method);
const tablePagination = useMemo(() => method === 'history' ? { pageSize: 5, position: ['bottomLeft']} : { pageSize: 10, position: ['bottomLeft']}, [method]);
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const { data } = await fetchData({ travel_agency_id: travel_agency_id || travelAgencyId, product_id, price_id, use_year });
setLogData(data);
invokeOpenChange(true);
setLoading(false);
};
const invokeOpenChange = (_open) => {
if (typeof onOpenChange === 'function') {
onOpenChange(_open);
}
};
const columns = [...columnsSets(t, false),
{ title: t('common:time'), dataIndex: 'updatetime', key: 'updatetime', width: '10rem', },
{ title: t('common:operator'), dataIndex: 'update_by', key: 'update_by' }
];
return (
<Popover
placement='bottom'
className=''
rootClassName='w-5/6'
{...props}
title={
<div className='flex justify-between mt-0 gap-4 items-center '>
<Typography.Text strong>{title}</Typography.Text>
<Button
size='small'
onClick={() => {
setOpen(false);
invokeOpenChange(false);
}}>
&times;
</Button>
</div>
}
content={
<>
<Table columns={columns} dataSource={logData} rowKey={'id'} size='small' loading={loading} pagination={tablePagination} />
</>
}
trigger={['click']}
open={open}
onOpenChange={(v) => {
setOpen(v);
invokeOpenChange(v);
}}>
<Button {...triggerProps} onClick={getData} title={title}>
{props.btnText || methodBtnText}
</Button>
</Popover>
);
};
export default ProductQuotationLogPopover;

@ -1,299 +0,0 @@
import { useState, useMemo } from 'react';
import { Button, Table, Popover, Typography, List, Flex } from 'antd';
import { useTranslation } from 'react-i18next';
import { HT_HOST } from '@/config';
import { fetchJSON } from '@haina/utils-request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@haina/utils-commons';
import { chunkBy } from '@/hooks/useProductsQuotationFormat';
/**
* 产品价格日志
*/
const getPPLogAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_log`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格: 已发布的
*/
const getPPRunningAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_running`, params)
return errcode !== 0 ? [] : result;
};
/**
* 产品价格快照
*/
const getPPSnapshotAction = async (params) => {
const { errcode, result } = await fetchJSON(`${HT_HOST}/Service_BaseInfoWeb/agency_product_price_snapshot`, params)
return errcode !== 0 ? [] : result;
}
const parseJson = (str) => {
let result;
if (str === null || str === undefined || str === '') {
return {};
}
try {
result = JSON.parse(str);
return Array.isArray(result) ? result.reduce((acc, cur) => ({ ...acc, ...cur }), {}) : result;
} catch (e) {
return {};
}
};
export const columnsSets = (t, colorize = true) => [
{
key: 'adult',
title: t('AgeType.Adult'),
width: '12rem',
render: (_, { adult_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && ![-1, 1, 2].includes(audit_state_id);
const ifData = isNotEmpty(_changed.adult_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.adult_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
{
key: 'child',
title: t('AgeType.Child'),
width: '12rem',
render: (_, { child_cost, currency, unit_id, unit_name, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && ![-1, 1, 2].includes(audit_state_id);
const ifData = isNotEmpty(_changed.child_cost) || isNotEmpty(_changed.unit_id) || isNotEmpty(_changed.currency);
const preValue =
ifCompare && ifData ? (
<div className='text-muted line-through '>{`${_changed.child_cost} ${_changed.currency || currency} / ${t(`PriceUnit.${_changed.unit_id || unit_id}`)}`}</div>
) : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}`}</span>
</div>
);
},
},
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
width: '6rem',
render: (_, { audit_state_id, group_size_min, group_size_max, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? (
<div className='text-muted line-through '>{`${_changed.group_size_min ?? group_size_min} - ${_changed.group_size_max ?? group_size_max}`}</div>
) : null;
const editCls = colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.group_size_min) || isNotEmpty(_changed.group_size_max)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{formatGroupSize(group_size_min, group_size_max)}</span>
</div>
);
},
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
width: '12rem',
render: (_, { use_dates_start, use_dates_end, weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const preValue =
colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? (
<div className='text-muted'>
{isNotEmpty(_changed.use_dates_start) ? <span className=' line-through '>{_changed.use_dates_start}</span> : use_dates_start} ~{' '}
{isNotEmpty(_changed.use_dates_end) ? <span className='t line-through '>{_changed.use_dates_end}</span> : use_dates_end}
</div>
) : null;
const editCls = colorize && ![-1, 1, 2].includes(audit_state_id) && (isNotEmpty(_changed.use_dates_start) || isNotEmpty(_changed.use_dates_end)) ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{`${use_dates_start} ~ ${use_dates_end}`}</span>
</div>
);
},
},
{
key: 'weekdays',
dataIndex: ['weekdays'],
title: t('Weekdays'),
width: '6rem',
render: (text, { weekdays, audit_state_id, lastedit_changed }) => {
const _changed = parseJson(lastedit_changed);
const ifCompare = colorize && ![-1, 1, 2].includes(audit_state_id);
const ifData = !isEmpty((_changed.weekdayList || []).filter((s) => s));
const preValue = ifCompare && ifData ? <div className='text-muted line-through '>{_changed.weekdayList}</div> : null;
const editCls = ifCompare && ifData ? 'text-danger' : '';
return (
<div>
{preValue}
<span className={editCls}>{text || t('Unlimited')}</span>
</div>
);
},
},
];
const useLogMethod = (method) => {
const { t } = useTranslation('products');
const methodMap = {
'history': {
title: '📑' + t('versionHistory'),
btnText: t('versionHistory'),
fetchData: async (params) => {
const data = await getPPLogAction(params);
return {data};
},
},
'published': {
title: '✅' + t('versionPublished'),
btnText: t('versionPublished'),
fetchData: async (params) => {
const { travel_agency_id, product_id, price_id, use_year } = params;
const data = await getPPRunningAction({ travel_agency_id, product_id_list: product_id, use_year });
return {data: data?.[0]?.quotation || []};
},
},
'snapshot': {
title: '📷' + t('versionSnapshot'),
btnText: t('versionSnapshot'),
subTitle: t('点击左侧价格版本查看具体价格'),
fetchData: async (params) => {
const { price_id, ..._params } = params;
const data = await getPPSnapshotAction(_params);
return {data}; //?.[0]?.quotation || [];
},
},
};
return methodMap[method];
};
/**
* ProductQuotationLogPopover - A popover component that displays product quotation change logs or published data
*
* This component shows a history of price changes for a specific product quotation in a popover table.
* It supports displaying different data sources (history logs or published data) and shows
* comparison between previous and current values with visual indicators.
*
* @param {Object} props - Component props
* @param {string} props.btnText - The text to display on the trigger button and in the popover header
* @param {'history' | 'published' | 'snapshot'} props.method - Determines data source - "history" for change logs or "published" for published quotations
* @param {Object} props.triggerProps - Additional props to pass to the trigger button
* @param {number} props.travel_agency_id - ID of the travel agency (used in data fetching)
* @param {number} props.product_id - ID of the product (used in data fetching)
* @param {number} props.price_id - ID of the price entry (used in data fetching)
* @param {number} props.use_year - Year to use for fetching data (used in data fetching)
* @param {Function} props.onOpenChange - Callback function to be called when the popover opens or closes
*/
const ProductQuotationSnapshotPopover = ({ method, triggerProps = {}, onOpenChange, ...props }) => {
const { travel_agency_id, product_id, price_id, use_year } = props;
const { t } = useTranslation('products');
const [open, setOpen] = useState(false);
const [logData, setLogData] = useState([]);
const { title, subTitle, btnText: methodBtnText, fetchData } = useLogMethod(method);
const tablePagination = useMemo(() => method === 'history' ? { pageSize: 5, position: ['bottomLeft']} : { pageSize: 10, position: ['bottomLeft']}, [method]);
const [viewSnapshotItem, setViewSnapshotItem] = useState([]);
const [loading, setLoading] = useState(false);
const getData = async () => {
setLoading(true);
const { data } = await fetchData({ travel_agency_id, product_id, price_id, use_year });
setLogData(data);
invokeOpenChange(true);
setLoading(false);
};
const invokeOpenChange = (_open) => {
if (typeof onOpenChange === 'function') {
onOpenChange(_open);
}
};
const onClickSnapshotItem = (item) => {
console.log(item)
setViewSnapshotItem(item);
console.log('cc\n');
const chunk = chunkBy(2025, [{...item, quotation: item.quotation.map(q => ({...q, WPI_SN: product_id })), info: { id: product_id }}], ['quote_season', 'quote_size']);
console.log(chunk)
};
const columns = [...columnsSets(t, false), { title: '时间', dataIndex: 'updatetime', key: 'updatetime' }];
return (
<Popover
placement='bottom'
className=''
rootClassName='w-5/6'
{...props}
title={
<div className='flex mt-0 gap-4 items-center '>
<Typography.Text strong>{title}</Typography.Text>
{subTitle && <Typography.Text type='secondary'>{subTitle}</Typography.Text>}
<Button
size='small' className='ml-auto'
onClick={() => {
setOpen(false);
invokeOpenChange(false);
}}>
&times;
</Button>
</div>
}
content={
<>
<Flex direction='column' gap='small'>
<List
bordered
dataSource={logData}
loading={loading}
renderItem={(item) => (
<List.Item onClick={() => onClickSnapshotItem(item)} className={viewSnapshotItem.version === item.version ? 'active' : ''}>
{item.version}
</List.Item>
)}
pagination={{ pageSize: 5, size: 'small', showLessItems: true, simple: { readOnly: true } }}
className=' cursor-pointer basis-48 flex flex-col [&>*:first-child]:flex-1 [&_.ant-list-pagination]:m-1 [&_.ant-list-item]:py-1 [&_.ant-list-item.active]:bg-blue-100'
/>
<div className='flex-auto'>
<Table columns={columns} dataSource={viewSnapshotItem.quotation} rowKey={'id'} size='small' loading={loading} pagination={tablePagination} />
</div>
</Flex>
</>
}
trigger={['click']}
open={open}
onOpenChange={(v) => {
setOpen(v);
invokeOpenChange(v);
if (v === false) {
setLogData([]);
setViewSnapshotItem([]);
}
}}>
<Button {...triggerProps} onClick={getData} title={title}>
{props.btnText || methodBtnText}
</Button>
</Popover>
);
};
export default ProductQuotationSnapshotPopover;

@ -5,7 +5,7 @@ import { CaretDownOutlined } from '@ant-design/icons';
import { useTranslation } from 'react-i18next';
import useProductsStore from '@/stores/Products/Index';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import { groupBy, isEmpty, sortBy } from '@haina/utils-commons';
import { groupBy, sortBy } from '@/utils/commons';
import NewProductModal from './NewProductModal';
import ContractRemarksModal from './ContractRemarksModal'
@ -49,11 +49,10 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const productsTypes = useProductsTypes();
const [treeData, setTreeData] = useState([]); // render data
const [treeData, setTreeData] = useState([]);
const [rawTreeData, setRawTreeData] = useState([]);
const [flattenTreeData, setFlattenTreeData] = useState([]);
const [expandedKeys, setExpandedKeys] = useState([]);
const [selectedKeys, setSelectedKeys] = useState([]);
const [autoExpandParent, setAutoExpandParent] = useState(true);
useEffect(() => {
@ -75,35 +74,28 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
const _show = productsTypes
.filter((kk) => hasDataTypes.includes(kk.value))
.map((ele) => ({
...ele,
title: ele.label,
key: ele.value,
children: (agencyProducts[ele.value] || []).reduce((arr, product) => {
const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({ ...rlgc, [clgc.lgc]: clgc }), {});
// const combindCityList = product.info.city_list.indexOf(city => city.id === product.info.city_id) !== -1 ? product.info.city_list : [...product.info.city_list, { id: product.info.city_id, name: product.info.city_name }];
// const cityListName = product.info.city_list.reduce((acc, city) => {
// return acc.concat([city.name]);
// }, []).join(',');
const hasCityList = !isEmpty(product.info.city_list) && product.info.city_list.some(cc => cc.id !== product.info.city_id) ? `【含多城市】` : ``;
const combindCityList = [{ id: product.info.city_id, name: product.info.city_name }];
const flatCityP = combindCityList.map(city => ({
title: `${city.name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || '') + `${hasCityList}`,
// key: `${ele.value}-${product.info.id}`,
key: `${product.info.id}-${city.id}`,
_raw: product,
isLeaf: true,
}));
return arr.concat(flatCityP);
}, []),
// ``
// _children: Object.keys(copyAgencyProducts[ele.value] || []).map(city => {
// return {
// title: city,
// key: `${ele.value}-${city}`,
// children: copyAgencyProducts[ele.value][city],
// };
// }),
}));
...ele,
title: ele.label,
key: ele.value,
children: (agencyProducts[ele.value] || []).map((product) => {
const lgc_map = product.lgc_details.reduce((rlgc, clgc) => ({...rlgc, [clgc.lgc]: clgc}), {});
return {
// title: product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || '',
title: `${product.info.city_name}` + (product.info.title || lgc_map?.['2']?.title || lgc_map?.['1']?.title || product.info.product_title || ''),
// key: `${ele.value}-${product.info.id}`,
key: product.info.id,
_raw: product,
isLeaf: true,
}}),
// ``
// _children: Object.keys(copyAgencyProducts[ele.value] || []).map(city => {
// return {
// title: city,
// key: `${ele.value}-${city}`,
// children: copyAgencyProducts[ele.value][city],
// };
// }),
}));
setTreeData(_show);
setRawTreeData(_show);
setFlattenTreeData(flattenTreeFun(_show));
@ -114,27 +106,9 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
return () => {};
}, [productsTypes, agencyProducts]);
useEffect(() => {
if (isEmpty(editingProduct)) {
return () => {};
}
const allKeysWithCity = [...(editingProduct.info?.city_list || []), { id: editingProduct.info.city_id, name: editingProduct.info.city_name }].map(
(city) => `${editingProduct.info.id}-${city.id}`
);
setSelectedKeys(allKeysWithCity);
return () => {};
}, [editingProduct?.info?.id]);
const [searchValue, setSearchValue] = useState('');
const onSearch = ({ target: { value } }) => {
// const { value } = e.target;
if (isEmpty(value)) {
setTreeData(rawTreeData);
setSearchValue(value);
return;
}
const newExpandedKeys = flattenTreeData
.filter((item) => item.title.includes(value))
.map((item) => getParentKey(item.key, rawTreeData))
@ -142,17 +116,10 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
setExpandedKeys(newExpandedKeys);
setSearchValue(value);
setAutoExpandParent(true);
const matchTree = rawTreeData.map(node1 => {
const _find = node1.children.filter(node2 => node2.title.includes(value));
return _find.length > 0 ? {...node1, children: _find} : null;
}).filter(node => node);
setTreeData(matchTree);
};
const handleNodeSelect = (selectedKeys, { node }) => {
if (node._raw) {
setEditingProduct(node._raw);
const allKeysWithCity = [...node._raw.info.city_list, { id: node._raw.info.city_id, name: node._raw.info.city_name }].map((city) => `${node._raw.info.id}-${city.id}`);
setSelectedKeys(allKeysWithCity);
} else {
// : /
// const isExpand = expandedKeys.includes(selectedKeys[0]);
@ -197,7 +164,7 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
<Tree
blockNode
showLine defaultExpandAll expandAction={'doubleClick'}
selectedKeys={selectedKeys} multiple
selectedKeys={[editingProduct?.info?.id || editingProduct?.info?.product_type_id]}
switcherIcon={<CaretDownOutlined />}
onSelect={handleNodeSelect}
treeData={treeData}

@ -8,7 +8,7 @@ import { useTranslation } from 'react-i18next';
import useProductsStore, { copyAgencyDataAction } from '@/stores/Products/Index';
import { useProductsTypes, useProductsAuditStatesMapVal } from '@/hooks/useProductsSets';
import useFormStore from '@/stores/Form';
import { objectMapper } from '@haina/utils-commons';
import { objectMapper } from '@/utils/commons';
import CopyProductsFormModal from './Detail/CopyProducts';
import useAuthStore from '@/stores/Auth';
import RequireAuth from '@/components/RequireAuth';

@ -1,40 +0,0 @@
import { useNavigate } from "react-router-dom";
import { Row, DatePicker, Flex, Col, Typography } from "antd";
import dayjs from "dayjs";
import { usingStorage } from "@/hooks/usingStorage";
function PickYear() {
const navigate = useNavigate();
const { travelAgencyId } = usingStorage();
return (
<>
<Row justify="center">
<Col span={4}>
<Flex gap="middle" vertical>
<Typography.Title className="text-center" level={3}>
请选择产品年份
</Typography.Title>
<DatePicker
className="w-full"
size="large"
variant="underlined"
needConfirm
inputReadOnly={true}
minDate={dayjs('2022-01-01')}
maxDate={dayjs().add(2, "year")}
allowClear={false}
picker="year"
open={true}
onOk={(date) => {
const useYear = date.year();
navigate(`/products/${travelAgencyId}/${useYear}/all/edit`);
}}
/>
</Flex>
</Col>
</Row>
</>
);
}
export default PickYear;

File diff suppressed because it is too large Load Diff

@ -22,7 +22,7 @@ import {
PositionalTabLeader,
} from 'docx';
import dayjs from 'dayjs';
import { cloneDeep, flush, groupBy, isEmpty, isNotEmpty, sortBy, unique, uniqWith } from '@haina/utils-commons';
import { cloneDeep, flush, groupBy, isEmpty, isNotEmpty, sortBy, unique, uniqWith } from '@/utils/commons';
// Shoulder Season ; peak season
const isFullYearOrLonger = (year, startDate, endDate) => {

@ -22,7 +22,7 @@ import {
PositionalTabLeader,
} from 'docx';
import dayjs from 'dayjs';
import { cloneDeep, groupBy, isEmpty, isNotEmpty, sortBy, unique, uniqWith } from '@haina/utils-commons';
import { cloneDeep, groupBy, isEmpty, isNotEmpty, sortBy, unique, uniqWith } from '@/utils/commons';
// Shoulder Season ; peak season
const isFullYearOrLonger = (year, startDate, endDate) => {

File diff suppressed because it is too large Load Diff

@ -1,65 +0,0 @@
import { Button } from 'antd';
import { useProductsAuditStatesMapVal, useProductsTypesMapVal } from '@/hooks/useProductsSets';
import { useTranslation } from 'react-i18next';
import useProductsStore, { getAgencyAllExtrasAction } from '@/stores/Products/Index';
import RequireAuth from '@/components/RequireAuth';
import { PERM_PRODUCTS_OFFER_AUDIT } from '@/config';
import dayjs from 'dayjs';
import AgencyContract from '../Print/AgencyContract';
import { saveAs } from 'file-saver';
import { Packer } from 'docx';
import { isEmpty } from '@haina/utils-commons';
const ExportDocxBtn = ({ params = { travel_agency_id: '', use_year: '', audit_state: '' }, ...props }) => {
const { t } = useTranslation();
const [agencyProducts] = useProductsStore((state) => [state.agencyProducts]);
const [activeAgency] = useProductsStore((state) => [state.activeAgency]);
const { travel_agency_id, use_year, audit_state } = params;
const auditStatesMap = useProductsAuditStatesMapVal();
const productsTypesMapVal = useProductsTypesMapVal();
const { getRemarkList } = useProductsStore((selector) => ({
getRemarkList: selector.getRemarkList,
}));
const handleDownload = async () => {
// await refresh();
const _agencyExtras = await getAgencyAllExtrasAction(params);
const agencyExtras = Object.keys(_agencyExtras).reduce((acc, pid) => {
const pitemExtras = _agencyExtras[pid];
const _pitem = (pitemExtras || []).map(eitem => ({ ...eitem, info: { ...eitem.info, product_type_name_txt: productsTypesMapVal[eitem.info.product_type_id]?.label || eitem.info.product_type_name } } ));
return { ...acc, [pid]: _pitem };
}, {});
const remarks = await getRemarkList();
const remarksMappedByType = remarks.reduce((r, v) => ({ ...r, [v.product_type_id]: v }), {});
const documentCreator = new AgencyContract();
const doc = documentCreator.create([
params,
activeAgency,
agencyProducts,
agencyExtras,
// remarks,
remarksMappedByType,
]);
const _d = dayjs().format('YYYYMMDD_HH.mm.ss.SSS'); // Date.now().toString(32)
// console.log(params);
const _state = isEmpty(audit_state) ? '' : auditStatesMap[audit_state].label;
Packer.toBlob(doc).then((blob) => {
saveAs(blob, `${activeAgency.travel_agency_name}${use_year}年地接合同-${_state}-${_d}.docx`);
});
};
return (
<>
{/* todo: export, 审核完成之后才能导出 */}
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
<Button size='small' onClick={handleDownload}>
{t('Export')} .docx
</Button>
{/* <PrintContractPDF /> */}
</RequireAuth>
</>
);
};
export default ExportDocxBtn;

@ -1,56 +1,37 @@
import { Row, Col, Space, Button, Table, Divider, Typography, } from 'antd';
import * as comm from '@haina/utils-commons';
import * as comm from '@/utils/commons';
import { usePDF } from 'react-to-pdf';
import dayjs from 'dayjs';
import SearchForm from '@/components/SearchForm';
import useReportStore from '@/stores/Report';
import { useTranslation } from 'react-i18next'
import { usingStorage } from '@/hooks/usingStorage';
import { VSTag } from '@/components/Data';
import { DATE_FORMAT } from '@/config';
function Index() {
const { t } = useTranslation();
const {travelAgencyId, } = usingStorage();
const [loading, setLoading, reset] = useReportStore((state) => [state.loading, state.setLoading, state.reset]);
const [vendorScoresData, vendorScoresDataDiff, getHWVendorScores] = useReportStore((state) => [state.vendorScoresData, state.vendorScoresDataDiff, state.getHWVendorScores]);
const [productScoresData, productScoresDataDiff, getHWProductScores] = useReportStore((state) => [state.productScoresData, state.productScoresDataDiff, state.getHWProductScores]);
const [commendScoresData, commendScoresDataDiff, getHWCommendScores] = useReportStore((state) => [state.commendScoresData, state.commendScoresDataDiff, state.getHWCommendScores]);
const [loading, vendorScoresData, getHWVendorScores] = useReportStore((state) => [state.loading, state.vendorScoresData, state.getHWVendorScores]);
const [productScoresData, getHWProductScores] = useReportStore((state) => [state.productScoresData, state.getHWProductScores]);
const [commendScoresData, getHWCommendScores] = useReportStore((state) => [state.commendScoresData, state.getHWCommendScores]);
const evaluationScores = vendorScoresData.EvaluationScores ? vendorScoresData.EvaluationScores[0] : {};
const evaluationScoresDiff = vendorScoresDataDiff.EvaluationScores ? vendorScoresDataDiff.EvaluationScores[0] : {};
const evaluationScores = vendorScoresData.EvaluationScores ? vendorScoresData.EvaluationScores[0] : [];
const primaryData = vendorScoresData.EvaluationScores
? [comm.pick(vendorScoresData.EvaluationScores[0], ['Groups', 'PersonNum', 'AmountUSD', 'EvaluationScore', 'TPReviews', 'TPReviewRate', 'Complaints', 'ComplaintRate'])]
: [];
let MonthlyData = vendorScoresData.MonthlyData || [];
if (vendorScoresData.EvaluationScores && !comm.isEmpty(vendorScoresDataDiff)) {
primaryData[0].diff = vendorScoresDataDiff.EvaluationScores
? comm.pick(vendorScoresDataDiff.EvaluationScores[0], ['Groups', 'PersonNum', 'AmountUSD', 'EvaluationScore', 'TPReviews', 'TPReviewRate', 'Complaints', 'ComplaintRate'])
: {};
MonthlyData = MonthlyData.map((row, ri) => ({...row, diff: vendorScoresDataDiff.MonthlyData[ri]}))
}
const guideData = (productScoresData?.GuideScores || []).reduce((a, row) => ({...a, [row.key]: row}), {});
const guideDataDiff = (productScoresDataDiff?.GuideScores || []).reduce((a, row) => ({...a, [row.key]: row}), {});
if (productScoresData.GuideScores && !comm.isEmpty(productScoresDataDiff)) {
Object.keys(guideData).forEach(key => {
guideData[key].diff = guideDataDiff[key];
})
}
const evaluationScoresData = [
{ category: 'DMC Services', item: 'Guide', value: evaluationScores.FRTGuide, note: evaluationScores.FRTText, diff: { value: evaluationScoresDiff?.FRTGuide } },
{ category: 'DMC Services', item: 'Driver & Vehicle', value: evaluationScores.FRTGriver, diff: { value: evaluationScoresDiff?.FRTGriver } },
{ category: 'DMC Services', item: 'Food Arrangement', value: evaluationScores.FRTMeal, diff: { value: evaluationScoresDiff?.FRTMeal } },
{ category: 'DMC Services', item: 'Activity', value: evaluationScores.FRTProduct, diff: { value: evaluationScoresDiff?.FRTProduct } },
{ category: 'Itinerary Arrangements', item: 'Hotel', value: evaluationScores.FRTHotel, diff: { value: evaluationScoresDiff?.FRTHotel } },
{ category: 'Itinerary Arrangements', item: 'Travel Advisor&rsquo;s Planning', value: evaluationScores.FRTAdvisor, diff: { value: evaluationScoresDiff?.FRTAdvisor } },
{ category: 'DMC Services', item: 'Guide', value: evaluationScores.FRTGuide, note: evaluationScores.FRTText },
{ category: 'DMC Services', item: 'Driver & Vehicle', value: evaluationScores.FRTGriver },
{ category: 'DMC Services', item: 'Food Arrangement', value: evaluationScores.FRTMeal },
{ category: 'DMC Services', item: 'Activity', value: evaluationScores.FRTProduct },
{ category: 'Itinerary Arrangements', item: 'Hotel', value: evaluationScores.FRTHotel, },
{ category: 'Itinerary Arrangements', item: 'Travel Advisor&rsquo;s Planning', value: evaluationScores.FRTAdvisor },
];
const columns_evaluation = [
{ title: 'Category', dataIndex: 'category', key: 'category',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 4 : index === 4 ? 2 : 0 }) },
{ title: 'Item', dataIndex: 'item', key: 'item', align: 'center' },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'left', render: (text, { diff }) => <>{text}<VSTag diffData={diff?.value} diffPercent={diff?.value ? comm.fixTo2Decimals((text-diff.value)/diff.value*100) : 0} /></> },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', },
{ title: 'Note', dataIndex: 'note', key: 'note',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 7 : 0 })},
];
@ -59,8 +40,8 @@ function Index() {
{ title: '3 scores', dataIndex: 'stand_3', key: 'stand_3', align: 'center', onCell: (_, index) => ({ colSpan: (index === 2 || index===4) ? 3 : 1 }) },
{ title: '4 scores', dataIndex: 'stand_4', key: 'stand_4',align: 'center',onCell: (_, index) => ({ colSpan: (index === 2 || index===4) ? 0 : 1 }) },
{ title: '5 scores', dataIndex: 'stand_5', key: 'stand_5',align: 'center',onCell: (_, index) => ({ colSpan: (index === 2 || index===4) ? 0 : 1 }) },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', render: (text, { diff }) => <>{text}<VSTag diffData={diff?.value} diffPercent={diff?.value ? comm.fixTo2Decimals((text-diff.value)/diff.value*100) : 0} /></> },
{ title: 'Final Scores', dataIndex: 'final_score', key: 'final_score',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 5 : 0 }), render: (text, { diff }) => <>{text}<VSTag diffData={diff?.final_score} diffPercent={diff?.final_score ? comm.fixTo2Decimals((text-diff.final_score)/diff.final_score*100) : 0} /></> },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', },
{ title: 'Final Scores', dataIndex: 'final_score', key: 'final_score',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 5 : 0 })},
{ title: 'Note', dataIndex: 'note', key: 'note',align: 'center', },
];
const DMCData_customer_satisfaction = [
@ -72,10 +53,6 @@ function Index() {
value: evaluationScores.TPReviewRating,
final_score: evaluationScores.AvgCusSatisfaction,
note: evaluationScores.AvgCusSatisfaction,
diff: {
value: evaluationScoresDiff?.TPReviewRating,
final_score: evaluationScoresDiff?.AvgCusSatisfaction,
}
},
{
item: 'Post tour complaints',
@ -85,9 +62,6 @@ function Index() {
value: evaluationScores.PostTourComplaints,
// final_score: evaluationScores.AvgCusSatisfaction,
note: evaluationScores.PostTourComplaintsText,
diff: {
value: evaluationScoresDiff?.PostTourComplaints,
}
},
{
item: 'Complaints resolved during the tour',
@ -96,24 +70,9 @@ function Index() {
// stand_5: '0',
value: evaluationScores.ComplaintsDuringTour,
note: evaluationScores.ComplaintsDuringTourText,
diff: {
value: evaluationScoresDiff?.ComplaintsDuringTour,
}
},
{ item: 'Customer photos', stand_3: '\\', stand_4: '30%', stand_5: '50%',
value: evaluationScores.CustomerPhotoRate,
note: evaluationScores.CustomerPhotoRateText,
diff: {
value: evaluationScoresDiff?.CustomerPhotoRate,
}
},
{ item: 'Evaluation scores', stand_3: '4.5',
value: evaluationScores.EvaluationFormScore,
note: evaluationScores.EvaluationFormScoreText,
diff: {
value: evaluationScoresDiff?.EvaluationFormScore,
}
},
{ item: 'Customer photos', stand_3: '\\', stand_4: '30%', stand_5: '50%', value: evaluationScores.CustomerPhotoRate, note: evaluationScores.CustomerPhotoRateText },
{ item: 'Evaluation scores', stand_3: '4.5', value: evaluationScores.EvaluationFormScore, note: evaluationScores.EvaluationFormScoreText },
];
const columns_DMC_sopport_local = [
@ -121,8 +80,8 @@ function Index() {
{ title: '3 scores', dataIndex: 'stand_3', key: 'stand_3', align: 'center', },
{ title: '4 scores', dataIndex: 'stand_4', key: 'stand_4',align: 'center', },
{ title: '5 scores', dataIndex: 'stand_5', key: 'stand_5',align: 'center', },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', render: (text, { diff }) => <>{text}<VSTag diffData={diff?.value} diffPercent={diff?.value ? comm.fixTo2Decimals((text-diff.value)/diff.value*100) : 0} /></> },
{ title: 'Final Scores', dataIndex: 'final_score', key: 'final_score',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 6 : 0 }), render: (text, { diff }) => <>{text}<VSTag diffData={diff?.final_score} diffPercent={diff?.final_score ? comm.fixTo2Decimals((text-diff.final_score)/diff.final_score*100) : 0} /></> },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', },
{ title: 'Final Scores', dataIndex: 'final_score', key: 'final_score',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 6 : 0 })},
{ title: 'Note', dataIndex: 'note', key: 'note',align: 'center', },
];
const DMCData_sopport_local = [
@ -134,10 +93,6 @@ function Index() {
value: evaluationScores.ResponseEfficiency,
final_score: evaluationScores.AvgLocalResources,
note: evaluationScores.ResponseEfficiencyText,
diff: {
value: evaluationScoresDiff?.ResponseEfficiency,
final_score: evaluationScoresDiff?.AvgLocalResources,
}
},
{
item: 'Provide suggestions and alternatives',
@ -147,9 +102,6 @@ function Index() {
value: evaluationScores.ProvideSuggestions,
// final_score: evaluationScores.AvgCusSatisfaction,
note: evaluationScores.ProvideSuggestionsText,
diff: {
value: evaluationScoresDiff?.ProvideSuggestions,
}
},
{
item: 'Provide local tourism information',
@ -158,48 +110,27 @@ function Index() {
stand_5: '√',
value: evaluationScores.ProvideLocalInfo,
note: evaluationScores.ProvideLocalInfoText,
diff: {
value: evaluationScoresDiff?.ProvideLocalInfo,
}
},
{ item: 'Assist in developing exclusive products',
stand_3: '\\',
stand_4: '\\',
stand_5: '√',
value: evaluationScores.ExclusiveProducts,
note: evaluationScores.ExclusiveProductsText,
diff: {
value: evaluationScoresDiff?.ExclusiveProducts,
}
},
stand_5: '√', value: evaluationScores.ExclusiveProducts, note: evaluationScores.ExclusiveProductsText },
{ item: 'Dedicated tour guide team for AH',
stand_3: '\\',
stand_4: '√',
stand_5: '√',
value: evaluationScores.DedicatedTourGuide,
note: evaluationScores.DedicatedTourGuideText,
diff: {
value: evaluationScoresDiff?.DedicatedTourGuide,
}
},
stand_5: '√', value: evaluationScores.DedicatedTourGuide, note: evaluationScores.DedicatedTourGuideText },
{ item: 'Partner hotels with contracted rate',
stand_3: '\\',
stand_4: '√',
stand_5: '√',
value: evaluationScores.PartnerHotels,
note: evaluationScores.PartnerHotelsText,
diff: {
value: evaluationScoresDiff?.PartnerHotels,
}
},
stand_5: '√', value: evaluationScores.PartnerHotels, note: evaluationScores.PartnerHotelsText },
];
const columns_DMC_pricing = [
{ title: 'Pricing & Settlement 20%', dataIndex: 'item', key: 'item',align: 'center', },
{ title: '3 scores', dataIndex: 'stand_3', key: 'stand_3', align: 'center', },
{ title: '4 scores', dataIndex: 'stand_4', key: 'stand_4',align: 'center', },
{ title: '5 scores', dataIndex: 'stand_5', key: 'stand_5',align: 'center', },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', render: (text, { diff }) => <>{text}<VSTag diffData={diff?.value} diffPercent={diff?.value ? comm.fixTo2Decimals((text-diff.value)/diff.value*100) : 0} /></> },
{ title: 'Final Scores', dataIndex: 'final_score', key: 'final_score',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 3 : 0 }), render: (text, { diff }) => <>{text}<VSTag diffData={diff?.final_score} diffPercent={diff?.final_score ? comm.fixTo2Decimals((text-diff.final_score)/diff.final_score*100) : 0} /></> },
{ title: 'Your Scores', dataIndex: 'value', key: 'value',align: 'center', },
{ title: 'Final Scores', dataIndex: 'final_score', key: 'final_score',align: 'center', onCell: (_, index) => ({ rowSpan: index === 0 ? 3 : 0 })},
{ title: 'Note', dataIndex: 'note', key: 'note',align: 'center', },
];
const DMCData_pricing = [
@ -211,10 +142,6 @@ function Index() {
value: evaluationScores.Quotation,
final_score: evaluationScores.AvgPricingAndSettlement,
note: evaluationScores.QuotationText,
diff: {
value: evaluationScoresDiff?.Quotation,
final_score: evaluationScoresDiff?.AvgPricingAndSettlement,
},
},
{
item: 'Settlement',
@ -223,7 +150,6 @@ function Index() {
stand_5: 'Monthly settlement after the tours',
value: evaluationScores.Settlement,
note: evaluationScores.SettlementText,
diff: { value: evaluationScoresDiff?.Settlement },
},
{
item: 'Cancellation policy',
@ -232,7 +158,6 @@ function Index() {
stand_5: '1 day',
value: evaluationScores.CancellationPolicy,
note: evaluationScores.CancellationPolicyText,
diff: { value: evaluationScoresDiff?.CancellationPolicy },
},];
const columns_primary = [
@ -240,66 +165,48 @@ function Index() {
title: 'Groups',
dataIndex: 'Groups',
key: 'Groups',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.Groups} diffPercent={diff?.Groups ? comm.fixTo2Decimals((text-diff.Groups)/diff.Groups*100) : 0} /></>,
},
{
title: 'Number of People',
dataIndex: 'PersonNum',
key: 'PersonNum',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.PersonNum} diffPercent={diff?.PersonNum ? comm.fixTo2Decimals((text-diff.PersonNum)/diff.PersonNum*100) : 0} /></>,
},
{
title: 'Transaction AmountUSD)',
dataIndex: 'AmountUSD',
key: 'AmountUSD',
render: (value, { diff }) => <>{comm.formatPrice(value)}<VSTag diffData={diff?.AmountUSD} diffPercent={diff?.AmountUSD ? comm.fixTo2Decimals((value-diff.AmountUSD)/diff.AmountUSD*100) : 0} /></>,
//
render: (value) => comm.formatPrice(value),
},
{
title: 'Evaluation Score',
dataIndex: 'EvaluationScore',
key: 'EvaluationScore',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.EvaluationScore} diffPercent={diff?.EvaluationScore ? comm.fixTo2Decimals((text-diff.EvaluationScore)/diff.EvaluationScore*100) : 0} /></>,
},
{
title: 'TP Reviews',
dataIndex: 'TPReviews',
key: 'TPReviews',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.TPReviews} diffPercent={diff?.TPReviews ? comm.fixTo2Decimals((text-diff.TPReviews)/diff.TPReviews*100) : 0} /></>,
},
{
title: 'TP Reviews Rate',
dataIndex: 'TPReviewRate',
key: 'TPReviewRate',
render: (text, { diff }) => <>{comm.formatPercent(text)}<VSTag diffData={diff?.TPReviewRate} diffPercent={diff?.TPReviewRate ? comm.fixTo2Decimals((text-diff.TPReviewRate)/diff.TPReviewRate*100) : 0} /></>,
render: (value) => comm.formatPercent(value),
},
{
title: 'Complaints',
dataIndex: 'Complaints',
key: 'Complaints',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.Complaints} diffPercent={diff?.Complaints ? comm.fixTo2Decimals((text-diff.Complaints)/diff.Complaints*100) : 0} /></>,
},
{
title: 'Complaint Rate',
dataIndex: 'ComplaintRate',
key: 'ComplaintRate',
render: (text, { diff }) => <>{comm.formatPercent(text)}<VSTag diffData={diff?.ComplaintRate} diffPercent={diff?.ComplaintRate ? comm.fixTo2Decimals((text-diff.ComplaintRate)/diff.ComplaintRate*100) : 0} /></>,
render: (value) => comm.formatPercent(value),
},
];
const columns_month = [
{
title: 'Date',
dataIndex: 'VMonth',
key: 'VMonth',
render: (text, { diff }) => (
<>
{text} {diff?.VMonth ? ` VS ${diff.VMonth}` : ''}
</>
),
},
...columns_primary,
];
const columns_month = [{ title: 'Date', dataIndex: 'VMonth', key: 'VMonth' }, ...columns_primary];
const columns_guide = [
{
@ -311,38 +218,33 @@ function Index() {
title: 'Average Scores',
dataIndex: 'VAverage',
key: 'VAverage',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.VAverage} diffPercent={diff?.VAverage ? comm.fixTo2Decimals((text-diff.VAverage)/diff.VAverage*100) : 0} /></>,
},
{
title: 'Group Numbers',
dataIndex: 'ReceptionGroups',
key: 'ReceptionGroups',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.ReceptionGroups} diffPercent={diff?.ReceptionGroups ? comm.fixTo2Decimals((text-diff.ReceptionGroups)/diff.ReceptionGroups*100) : 0} /></>,
},
{
title: 'TP Reviews',
dataIndex: 'CommendNum',
key: 'CommendNum',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.CommendNum} diffPercent={diff?.CommendNum ? comm.fixTo2Decimals((text-diff.CommendNum)/diff.CommendNum*100) : 0} /></>,
},
{
title: 'TP Review Rate',
dataIndex: 'CommendRate',
key: 'CommendRate',
// render: (value) => comm.formatPercent(value),
render: (text, { diff }) => <>{comm.formatPercent(text)}<VSTag diffData={diff?.CommendRate ? comm.formatPercent(diff?.CommendRate || 0) : void 0} diffPercent={diff?.CommendRate ? comm.fixTo2Decimals((text-diff.CommendRate)/diff.CommendRate*100) : 0} /></>,
render: (value) => comm.formatPercent(value),
},
{
title: 'Complaints',
dataIndex: 'Complaints',
key: 'Complaints',
render: (text, { diff }) => <>{text}<VSTag diffData={diff?.Complaints} diffPercent={diff?.Complaints ? comm.fixTo2Decimals((text-diff.Complaints)/diff.Complaints*100) : 0} /></>,
},
{
title: 'Complaint Rate',
dataIndex: 'ComplaintRate',
key: 'ComplaintRate',
render: (text, { diff }) => <>{comm.formatPercent(text)}<VSTag diffData={diff?.ComplaintRate ? comm.formatPercent(diff?.ComplaintRate || 0) : void 0} diffPercent={diff?.ComplaintRate ? comm.fixTo2Decimals((text-diff.ComplaintRate)/diff.ComplaintRate*100) : 0} /></>,
render: (value) => comm.formatPercent(value),
},
];
@ -372,23 +274,6 @@ function Index() {
overrides: { pdf: { compress: true }, canvas: { useCORS: true } },
});
const onGetData = async (formVal) => {
reset();
const fixDate1 = dayjs(formVal.startdate).startOf('month').format(DATE_FORMAT);
const fixDiff1 = dayjs(formVal.datesDiff1).startOf('month').format(DATE_FORMAT);
const res = await Promise.all([
getHWVendorScores(formVal.agency || travelAgencyId, fixDate1, formVal.enddate),
getHWProductScores(formVal.agency || travelAgencyId, fixDate1, formVal.enddate),
getHWCommendScores(formVal.agency || travelAgencyId, fixDate1, formVal.enddate),
...(comm.isEmpty(formVal.datesDiff1) ? [] : [
getHWVendorScores(formVal.agency || travelAgencyId, fixDiff1, formVal.datesDiff2, true),
getHWProductScores(formVal.agency || travelAgencyId, fixDiff1, formVal.datesDiff2, true),
getHWCommendScores(formVal.agency || travelAgencyId, fixDiff1, formVal.datesDiff2, true)
])
]);
setLoading(false);
};
return (
<Space direction='vertical' style={{ width: '100%' }}>
{/* <Typography.Title level={3}></Typography.Title> */}
@ -401,14 +286,15 @@ function Index() {
fieldsConfig={{
shows: ['dates'],
fieldProps: {
dates: { label: 'Select Date', col: 12, hide_vs: false },
dates: { label: 'Select Date', col: 12 },
},
fieldComProps: { dates: { picker: 'month', presets: false } },
}}
confirmText={t('vendor:report.GetReport')}
onSubmit={(err, formVal, filedsVal) => {
// return false;
onGetData(formVal);
getHWVendorScores(travelAgencyId, formVal.startdate, formVal.enddate);
getHWProductScores(travelAgencyId, formVal.startdate, formVal.enddate);
getHWCommendScores(travelAgencyId, formVal.startdate, formVal.enddate);
}}
/>
</Col>
@ -432,7 +318,7 @@ function Index() {
Monthly Data
</Typography.Title>
</Divider>
<Table loading={loading} dataSource={MonthlyData} columns={columns_month} pagination={false} bordered />
<Table loading={loading} dataSource={vendorScoresData.MonthlyData} columns={columns_month} pagination={false} bordered />
</Col>
<Col md={24} lg={24} xxl={16}>
@ -448,7 +334,7 @@ function Index() {
<Table key={'p'} dataSource={DMCData_pricing} columns={columns_DMC_pricing} pagination={false} bordered />
<Divider orientation='right'>
<Typography.Title level={3} type='danger'>
Final Scores: {evaluationScores.FinalScores}{evaluationScoresDiff?.FinalScores ? <><VSTag diffData={evaluationScoresDiff.FinalScores} diffPercent={evaluationScoresDiff?.FinalScores ? comm.fixTo2Decimals((evaluationScores.FinalScores-evaluationScoresDiff.FinalScores)/evaluationScoresDiff.FinalScores*100) : 0} /></> : ''}
Final Scores: {evaluationScores.FinalScores}
</Typography.Title>
</Divider>
<br />
@ -487,28 +373,28 @@ function Index() {
Tour Guides Performence
</Typography.Title>
</Divider>
<Table dataSource={Object.values(guideData)} columns={columns_guide} pagination={false} bordered />
<Table dataSource={productScoresData.GuideScores} columns={columns_guide} pagination={false} bordered />
<Divider orientation='center'>
<Typography.Title level={3} type='success'>
TP Reviews
</Typography.Title>
</Divider>
<Table dataSource={[...commendScoresData?.CommendScores || [], ...commendScoresDataDiff?.CommendScores || []]} columns={columns_commend} pagination={false} bordered />
<Table dataSource={commendScoresData.CommendScores} columns={columns_commend} pagination={false} bordered />
<Divider orientation='center'>
<Typography.Title level={3} type='success'>
Complaints
</Typography.Title>
</Divider>
<Table dataSource={[...commendScoresData?.ComplaintScores || [], ...commendScoresDataDiff?.ComplaintScores || []]} columns={columns_commend} pagination={false} bordered />
<Table dataSource={commendScoresData.ComplaintScores} columns={columns_commend} pagination={false} bordered />
<Divider orientation='center'>
<Typography.Title level={3} type='success'>
Suggestions from Customers
</Typography.Title>
</Divider>
<Table dataSource={[...commendScoresData?.CriticizeScores || [], ...commendScoresDataDiff?.CriticizeScores || []]} columns={columns_commend} pagination={false} bordered />
<Table dataSource={commendScoresData.CriticizeScores} columns={columns_commend} pagination={false} bordered />
</Col>
</Row>
</Space>

@ -1,13 +1,13 @@
import { useParams, useNavigate } from 'react-router-dom'
import { useParams } from 'react-router-dom'
import { useEffect, useState } from 'react'
import { Row, Col, Space, Button, Table, Input, Typography, Modal, Descriptions, Tabs, App, Flex } from 'antd'
import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App } from 'antd'
import {
ArrowLeftOutlined
FileOutlined
} from '@ant-design/icons'
import { usingStorage } from '@/hooks/usingStorage'
import useReservationStore from '@/stores/Reservation'
import { useTranslation } from 'react-i18next'
import {ImageUploader} from '@/components/ImageUploader'
import BackBtn from '@/components/BackBtn'
const { Title, Paragraph } = Typography
const { TextArea } = Input
@ -21,7 +21,7 @@ function Detail() {
dataIndex: 'PCI_Changetext',
},
{
title: (<><div>{t('group:ResSendingDate')}</div><div>YYYY-MM-DD</div></>),
title: t('group:ResSendingDate'),
dataIndex: 'PCI_SendDate',
},
{
@ -33,7 +33,7 @@ function Detail() {
render: attachmentRender
},
{
title: (<><div>{t('group:ConfirmationDate')}</div><div>YYYY-MM-DD</div></>),
title: t('group:ConfirmationDate'),
dataIndex: 'PCI_ConfirmDate',
},
{
@ -42,98 +42,6 @@ function Detail() {
},
];
const itineraryColumns = [
{
title: 'Dates',
dataIndex: 'dates',
key: 'dates',
},
{
title: 'Destinations',
dataIndex: 'destinations',
key: 'destinations',
},
{
title: 'Guides',
dataIndex: 'guideName',
key: 'guideName',
render: (_, itinerary) => {
return (itinerary.guideName + ' ' + itinerary.guideMobile)
}
},
{
title: 'Local Agents',
dataIndex: 'localAgents',
key: 'localAgents',
render: (_, itinerary) => {
return (<span>{itinerary.localAgents} - {itinerary.agentContact} {itinerary.agentMobile}</span>)
}
},
];
const flightColumns = [
{
title: (<><div>Date</div><div>YYYY-MM-DD</div></>),
dataIndex: 'FlightDate',
},
{
title: 'Flight Number',
dataIndex: 'FlightNo',
},
{
title: 'Departure Airport',
dataIndex: 'FromAirport',
},
{
title: 'Terminal',
dataIndex: 'FromTerminal',
},
{
title: 'Arrival Airport',
dataIndex: 'ToAirport',
},
{
title: 'Terminal',
dataIndex: 'ToTerminal',
},
{
title: (<><div>Departure Time</div><div>YYYY-MM-DD</div></>),
dataIndex: 'FlightDate',
},
{
title: 'Arrival Time',
dataIndex: 'FlightEnd',
},
{
title: 'Luggage Allowance',
dataIndex: 'Baggage',
width: "100px",
},
{
title: 'Tickets Number and Passports',
dataIndex: 'destinations',
align: 'left',
render: (_, flight) => {
return (<Table loading={dataLoading} columns={seatColumns} dataSource={flight.seatList} pagination={false} />)
}
},
];
const seatColumns = [
{
title: 'Tickets Number',
dataIndex: 'ticketNo',
},
{
title: 'PNR',
dataIndex: 'pnr',
},
{
title: 'Passengers',
dataIndex: 'memo',
},
]
function detailTextRender(_, confirm) {
const formattedText = confirm.PCI_ConfirmText;
return (
@ -144,10 +52,15 @@ function Detail() {
}
function attachmentRender(_, confirm) {
const attachmentKey = `GHH/${selectedAgencyId}/${reservationId}/PCISN${confirm.key}`;
return (
<>
<ImageUploader osskey={attachmentKey} ignore_case={false} deletable={false} />
{confirm.attachmentList.map(attch => {
return (
<Tag key={attch.file_name} bordered={false} icon={<FileOutlined />}>
<a href={attch.file_url} target='_blank' rel='noreferrer'>{attch.file_name}</a>
</Tag>
)}
)}
</>
);
}
@ -158,7 +71,6 @@ function Detail() {
);
}
const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [confirmText, setConfirmText] = useState('');
@ -169,19 +81,19 @@ function Detail() {
const { notification } = App.useApp();
const { reservationId } = useParams();
const { loginToken } = usingStorage()
const { travelAgencyId, loginToken } = usingStorage()
const [getReservationDetail, reservationDetail, confirmationList, itineraryList, flightList, selectConfirmation, submitConfirmation, selectedAgencyId] =
const [getReservationDetail, reservationDetail, confirmationList, selectConfirmation, submitConfirmation] =
useReservationStore((state) =>
[state.getReservationDetail, state.reservationDetail, state.confirmationList, state.itineraryList, state.flightList, state.selectConfirmation, state.submitConfirmation, state.selectedAgencyId])
[state.getReservationDetail, state.reservationDetail, state.confirmationList, state.selectConfirmation, state.submitConfirmation])
const randomString = new Date().getTime()
const officeWebViewerUrl =
'https://view.officeapps.live.com/op/embed.aspx?wdPrint=1&wdHideGridlines=0&wdHideComments=1&wdEmbedCode=0&src=';
// https://www.chinahighlights.com/public/reservationW220420009.doc
const reservationUrl =
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${selectedAgencyId}&token=${loginToken}&FileType=1&v=${randomString}`
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=1&v=${randomString}`
const nameCardUrl =
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${selectedAgencyId}&token=${loginToken}&FileType=2&v=${randomString}`
`https://p9axztuwd7x8a7.mycht.cn/service-fileServer/DownloadPlanDoc?GRI_SN=${reservationId}&VEI_SN=${travelAgencyId}&token=${loginToken}&FileType=2&v=${randomString}`
const showConfirmModal = (confirm) => {
setIsModalOpen(true);
@ -209,7 +121,7 @@ function Detail() {
setReservationPreviewUrl(officeWebViewerUrl + encodeURIComponent(reservationUrl))
setNameCardPreviewUrl(officeWebViewerUrl + encodeURIComponent(nameCardUrl))
getReservationDetail(selectedAgencyId, reservationId)
getReservationDetail(reservationId)
.catch(ex => {
notification.error({
message: `Notification`,
@ -221,28 +133,7 @@ function Detail() {
.finally(() => {
setDataLoading(false);
});
}, [reservationId]);
const iteneraryAndNameCardItems = [
{
key: 'Itinerary',
label: 'Itinerary',
children: <>
<iframe id='msdoc-iframe-reservation' title='msdoc-iframe-reservation'
src={reservationPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Button type='link' target='_blank' href={reservationUrl}>{t('Download')} Itinerary</Button>
</>,
},
{
key: 'Name Card',
label: 'Name Card',
children: <>
<iframe id='msdoc-iframe-name-card' title='msdoc-iframe-name-card'
src={nameCardPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Button type='link' target='_blank' href={nameCardUrl}>{t('Download')} Name Card</Button>
</>,
},
];
}, [reservationId, getReservationDetail, notification]);
return (
<>
@ -270,18 +161,28 @@ function Detail() {
/>
</Modal>
<Space direction='vertical' className='w-full'>
<Flex horizontal="true" align="flex-start" gap="middle"><Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate('/reservation/newest')} /><Title level={4}> {t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate};</Title></Flex>
<Row gutter={{ md: 24 }}>
<Col span={20}>
<Title level={4}>{t('group:RefNo')}: {reservationDetail.referenceNumber}; {t('group:ArrivalDate')}: {reservationDetail.arrivalDate};</Title>
</Col>
<Col span={4}>
<BackBtn to={'/reservation/newest?back'} />
</Col>
</Row>
<Row gutter={{ md: 24 }}>
<Col span={12} className='w-full'>
<Table loading={dataLoading} columns={itineraryColumns} dataSource={itineraryList} pagination={false} />
<iframe id='msdoc-iframe-reservation' title='msdoc-iframe-reservation'
src={reservationPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Button type='link' target='_blank' href={reservationUrl}>{t('Download')} Itinerary</Button>
</Col>
<Col span={12} className='w-full'>
<Tabs defaultActiveKey="1" items={iteneraryAndNameCardItems} tabPosition="left" />
<iframe id='msdoc-iframe-name-card' title='msdoc-iframe-name-card'
src={nameCardPreviewUrl} frameBorder='0' className='w-full h-[600px]'></iframe>
<Button type='link' target='_blank' href={nameCardUrl}>{t('Download')} Name Card</Button>
</Col>
</Row>
<Row>
<Col span={24}>
<Space direction='vertical' className='w-full'>
<Col span={24}><Space direction='vertical' className='w-full'>
<Table
bordered
loading={dataLoading}
@ -291,13 +192,6 @@ function Detail() {
</Space>
</Col>
</Row>
<Row>
<Col span={24}>
<Table title={() => 'Flight Tickets Info'}
bordered
loading={dataLoading} columns={flightColumns} dataSource={flightList} pagination={false} />
</Col>
</Row>
</Space>
</>
);

@ -2,7 +2,7 @@ import { NavLink } from 'react-router-dom'
import { useState, useEffect } from 'react'
import { Row, Col, Space, Button, Table, Typography, Modal, App, Select } from 'antd'
import dayjs from 'dayjs'
import { groupBy, isEmpty } from '@haina/utils-commons'
import { isEmpty } from '@/utils/commons'
import { useTranslation } from 'react-i18next'
import useFormStore from '@/stores/Form'
import useReservationStore from '@/stores/Reservation'
@ -28,7 +28,7 @@ function Newest() {
},
},
{
title: (<><div>{t('group:ArrivalDate')}</div><div>YYYY-MM-DD</div></>),
title: t('group:ArrivalDate'),
dataIndex: 'arrivalDate',
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')),
},
@ -41,7 +41,7 @@ function Newest() {
dataIndex: 'status'
},
{
title: (<><div>{t('group:ResSendingDate')}</div><div>YYYY-MM-DD</div></>),
title: t('group:ResSendingDate'),
dataIndex: 'reservationDate',
render: (text) => (isEmpty(text) ? '' : dayjs(text).format('YYYY-MM-DD')),
},
@ -95,16 +95,24 @@ function Newest() {
const [guideSelectOptions, setGuideSelectOptions] = useState([])
const formValuesToSub = useFormStore((state) => state.formValuesToSub)
const [fetchAgencyGuideList, fetchReservationList, reservationList, reservationPage, cityList, selectReservation, getCityListByReservationId, setupCityGuide, updateReservationGuide] =
const [fetchAllGuideList, fetchReservationList, reservationList, reservationPage, cityList, selectReservation, getCityListByReservationId, setupCityGuide, updateReservationGuide] =
useReservationStore((state) =>
[state.fetchAgencyGuideList, state.fetchReservationList, state.reservationList, state.reservationPage, state.cityList, state.selectReservation, state.getCityListByReservationId, state.setupCityGuide, state.updateReservationGuide])
const travelAgencyId = formValuesToSub.agency;
[state.fetchAllGuideList, state.fetchReservationList, state.reservationList, state.reservationPage, state.cityList, state.selectReservation, state.getCityListByReservationId, state.setupCityGuide, state.updateReservationGuide])
const { notification } = App.useApp()
useEffect (() => {
initAgencyGuideList(travelAgencyId)
}, [travelAgencyId])
fetchAllGuideList()
.then((guideList) => {
const selectOptions = guideList.map((data) => {
return {
value: data.guideId,
label: data.guideName
}
})
setGuideSelectOptions(selectOptions)
})
}, [fetchAllGuideList])
const showCityGuideModal = (reservation) => {
setDataLoading(true)
@ -137,17 +145,6 @@ function Newest() {
setDataLoading(false);
}
const initAgencyGuideList = (agencyId) => {
fetchAgencyGuideList(agencyId)
.then((guideList) => {
const guideCity = (groupBy(guideList, 'cityName'));
const guideOptions = Object.keys(guideCity).map(city => ({
label: city, options: guideCity[city].map(guide => ({ value: guide.guideId, label: guide.guideName + '(' + guide.mobileNo + ')', })),
}));
setGuideSelectOptions(guideOptions)
})
}
//
const searchReservation = (submitValues, current=1) => {
setDataLoading(true)
@ -198,7 +195,7 @@ function Newest() {
</Space>
</Modal>
<Space direction='vertical' className='w-full'>
{/* <Title level={3}></Title> */}
<Title level={3}></Title>
<SearchForm
initialValue={{unconfirmed: true}}
fieldsConfig={{

@ -2,14 +2,13 @@ import { useState, useEffect } from "react";
import { Grid, Divider, Layout, Spin, Input, Col, Row, Space, List, Table, Button, Typography } from "antd";
import { PhoneOutlined, CustomerServiceOutlined, AudioOutlined, AuditOutlined } from "@ant-design/icons";
import { useParams, useHref, useNavigate, NavLink } from "react-router-dom";
import { isEmpty, formatColonTime } from "@haina/utils-commons";
import { isEmpty, formatColonTime } from "@/utils/commons";
import dayjs from "dayjs";
import SearchForm from "@/components/SearchForm";
import { DATE_FORMAT } from "@/config";
import { TableExportBtn } from "@/components/Data";
import trainTicketStore from "@/stores/Trainticket";
import { usingStorage } from "@/hooks/usingStorage";
import useFormStore from "@/stores/Form";
const planListColumns = [
{
@ -88,9 +87,7 @@ const planListColumns = [
const Trainticket = props => {
const navigate = useNavigate();
const [{agency, ...formValues}] = useFormStore(state => [state.formValuesToSub]);
const { travelAgencyId: myAgencyId } = usingStorage();
const travelAgencyId = agency; // || myAgencyId;
const { travelAgencyId } = usingStorage();
const [getPlanList, planList, loading] = trainTicketStore(state => [state.getPlanList, state.planList, state.loading]);
const showTotal = total => `合计 ${total} `;
@ -114,7 +111,7 @@ const Trainticket = props => {
},
}}
onSubmit={(err, formVal, filedsVal) => {
getPlanList(formVal.agency || travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
getPlanList(travelAgencyId, formVal.referenceNo, formVal.startdate, formVal.endtime, formVal.plan_state, formVal.airticket_state);
}}
/>
</Col>

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

Loading…
Cancel
Save