Compare commits

..

1 Commits

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

@ -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,
@ -89,8 +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_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.2.5",
"version": "2.1.0",
"type": "module",
"scripts": {
"dev": "vite",
@ -13,7 +13,7 @@
"dependencies": {
"@ant-design/icons": "^5.5.1",
"@react-pdf/renderer": "^3.4.0",
"antd": "^5.27.0",
"antd": "^5.17.2",
"dayjs": "^1.11.13",
"docx": "^8.5.0",
"file-saver": "^2.0.5",
@ -23,14 +23,15 @@
"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

@ -19,7 +19,7 @@
"Back": "Back",
"Download": "Download",
"Upload": "Upload",
"Preview": "Preview",
"preview": "Preview",
"Total": "Total",
"Action": "Action",
"Import": "Import",
@ -36,8 +36,6 @@
"Table": {
"Total": "Total {{total}} items"
},
"operator": "Operator",
"time": "Time",
"Login": "Login",
"Username": "Username",
"Realname": "Realname",

@ -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": "请输入成人价",

@ -19,7 +19,7 @@
"Back": "返回",
"Download": "下载",
"Upload": "上传",
"Preview": "预览",
"preview": "预览",
"Total": "总数",
"Action": "操作",
"Import": "导入",
@ -36,8 +36,6 @@
"Table": {
"Total": "共 {{total}} 条"
},
"operator": "操作",
"time": "时间",
"Login": "登录",
"Username": "账号",
"Realname": "姓名",

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

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

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

@ -271,7 +271,7 @@ function getFields(props) {
"agency", //
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
),

@ -12,11 +12,7 @@ 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';
// 权限常量定义
// 账号、权限管理
@ -46,7 +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'; // 价格.录入

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

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

@ -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, ...sortOrder], ['description']],
'Q': [[...infoRecDisplay, 'duration', ...infoDisplay, ...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,24 +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) => {
return max === 1000 ? min <= 1 ? '不分人等' : `${min}人以上` : (`${min}-${max}`+(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)
},

@ -45,10 +45,6 @@ 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 PickYear from './views/products/PickYear'
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA,PERM_TRAIN_TICKET, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT, PERM_PRODUCTS_OFFER_PUT } from '@/config'
import './i18n'
@ -65,19 +61,14 @@ const initRouter = async () => {
{ path: 'account/profile', element: <AccountProfile />},
{ path: 'account/management', element: <RequireAuth subject={PERM_ACCOUNT_MANAGEMENT} result={true}><AccountManagement /></RequireAuth>},
{ path: 'account/role-list', element: <RequireAuth subject={PERM_ROLE_NEW} result={true}><RoleList /></RequireAuth>},
//
{ path: 'reservation/newest', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationNewest /></RequireAuth>},
{ path: 'reservation/:reservationId', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReservationDetail /></RequireAuth>},
//
{ path: 'feedback', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackIndex /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:CII_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackCustomerDetail /></RequireAuth>},
{ path: 'feedback/:GRI_SN/:RefNo', element: <RequireAuth subject={PERM_OVERSEA} result={true}><FeedbackDetail /></RequireAuth>},
//
{ path: 'report', element: <RequireAuth subject={PERM_OVERSEA} result={true}><ReportIndex /></RequireAuth>},
//
{ path: 'notice', element: <NoticeIndex />},
{ path: 'notice/:CCP_BLID', element: <NoticeDetail />},
//
{ path: 'invoice',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceIndex /></RequireAuth>},
{ path: 'invoice/detail/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceDetail /></RequireAuth>},
{ path: 'invoice/history/:GMDSN/:GSN',element:<RequireAuth subject={PERM_OVERSEA} result={true}><InvoiceHistory /></RequireAuth>},
@ -87,19 +78,20 @@ const initRouter = async () => {
{ path: 'airticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketPlan /></RequireAuth>},
{ path: 'airticket/invoice',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoice /></RequireAuth>},
{ path: 'airticket/invoicepaid',element:<RequireAuth subject={PERM_AIR_TICKET} result={true}><AirticketInvoicePaid /></RequireAuth>},
//
{ path: 'trainticket',element: <RequireAuth subject={PERM_TRAIN_TICKET} result={true}><Trainticket /></RequireAuth>},
{ path: 'trainticket/plan/:coli_sn/:gri_sn',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketPlan /></RequireAuth>},
{ path: 'trainticket/invoice',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoice /></RequireAuth>},
{ path: 'trainticket/invoicepaid',element:<RequireAuth subject={PERM_TRAIN_TICKET} result={true}><TrainticketInvoicePaid /></RequireAuth>},
//
{ path: "products",element: <RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsManage /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/audit",element:<RequireAuth subject={PERM_PRODUCTS_MANAGEMENT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/:travel_agency_id/:use_year/:audit_state/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/audit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsAudit /></RequireAuth>},
{ path: "products/edit",element:<RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><ProductsDetail /></RequireAuth>},
{ path: "products/pick-year",element: <RequireAuth subject={PERM_PRODUCTS_OFFER_PUT} result={true}><PickYear /></RequireAuth>},
//
]
},
{
@ -107,8 +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 /> },
]
}
])
@ -131,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,27 +1,5 @@
import { loadScript } from '@/utils/commons';
import { PROJECT_NAME, BUILD_VERSION } from '@/config';
import { fetchJSON } from '@/utils/request'
import { usingStorage } from "@/hooks/usingStorage";
export const sendNotify = async (message) => {
const { userId, travelAgencyId } = usingStorage();
const notifyUrl = 'https://p9axztuwd7x8a7.mycht.cn/dingtalk/dingtalkwork/SendMDMsgByDingRobotToGroup';
const params = {
groupid: 'cidFtzcIzNwNoiaGU9Q795CIg==',
msgTitle: '有人求助',
msgText: `${message}\\n\\nID: ${userId}, ${travelAgencyId} | ${PROJECT_NAME} (${BUILD_VERSION})`,
};
return fetchJSON(notifyUrl, params).then((json) => {
if (json.errcode === 0) {
console.info('发送通知成功');
} else {
throw new Error(json?.errmsg + ': ' + json.errcode);
}
});
};
export const loadPageSpy = (title) => {
@ -42,52 +20,19 @@ export const loadPageSpy = (title) => {
PageSpy.registerPlugin(p)
})
window.$pageSpy = new PageSpy(PageSpyConfig);
window.onerror = async function (msg, url, lineNo, columnNo, error) {
// iframe rrweb Cannot set property attributeName of #<MutationRecord> which has only a getter
//
if (url && url.indexOf('https://page-spy.mycht.cn/plugin/rrweb/index.min.js') > -1) {
console.info('ignore rrweb error')
} else {
// 3
const now = Date.now()
await window.$harbor.uploadPeriods({
startTime: now - 3 * 60000,
endTime: now,
remark: `\`onerror\`自动上传. ${msg}`,
})
}
}
});
};
export const uploadPageSpyLog = async () => {
if (import.meta.env.DEV) return true;
// window.$pageSpy.triggerPlugins('onOfflineLog', 'upload');
if (window.$pageSpy) {
try {
// await window.$harbor.upload() // { clearCache: true, remark: '' }
// 1 , upload : 413 Payload Too Large
const now = Date.now();
await window.$harbor.uploadPeriods({
startTime: now - 60 * 60000,
endTime: now,
});
return true;
} catch (error) {
return false;
}
await window.$harbor.upload() // { clearCache: true, remark: '' }
alert('Success')
} else {
return false;
alert('Failure')
}
}
/**
* @deprecated
* @outdated
*/
export const PageSpyLog = () => {
return (
<>

@ -4,12 +4,10 @@ import { appendRequestParams, fetchJSON, postForm } from '@/utils/request'
import { HT_HOST } from "@/config"
import { loadPageSpy } from '@/pageSpy'
import { usingStorage } from '@/hooks/usingStorage'
import { isEmpty } from "@/utils/commons";
const KEY_LOGIN_TOKEN = 'G-STR:LOGIN_TOKEN'
const KEY_TRAVEL_AGENCY_ID = 'G-INT:TRAVEL_AGENCY_ID'
const KEY_USER_ID = 'G-INT:USER_ID'
const KEY_I18N = 'i18nextLng'
const WILDCARD_TOKEN = '*'
@ -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: {
@ -58,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 环境没有该问题
@ -77,14 +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
travelAgencyName: userJson.VName,
}
}))
loadPageSpy(`${userJson.LoginName}-${userJson.VName}`)
loadPageSpy(`${userJson.real_name}-${userJson.VName}`)
},
authenticate: async (usr, pwd) => {
@ -122,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: {
@ -171,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

@ -110,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);

@ -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,26 +1,24 @@
import { Outlet, Link, useHref, useNavigate, NavLink } from 'react-router-dom'
import { useEffect, useState } from 'react'
import {
Popover, Layout, Menu, ConfigProvider, theme, Dropdown, message, FloatButton, Space, Row, Col, Badge, App as AntApp,
Button, Form, Input
} from 'antd'
import { 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 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 '@/utils/request'
import LogUploader from '@/components/LogUploader'
import { uploadPageSpyLog } from '@/pageSpy';
import { PERM_ACCOUNT_MANAGEMENT, PERM_ROLE_NEW, PERM_OVERSEA, PERM_AIR_TICKET, PERM_PRODUCTS_MANAGEMENT,PERM_TRAIN_TICKET } from '@/config'
@ -37,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()
@ -63,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,
}}>
@ -82,16 +93,17 @@ 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'
@ -150,7 +162,7 @@ function App() {
<Content className='p-6 m-0 min-h-72 bg-white overflow-auto'>
{needToLogin ? <>login...</> : <Outlet />}
</Content>
<Footer>China Highlights International Travel Service Co., LTD, Version: {BUILD_VERSION}({GIT_HEAD})</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." 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 "@/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 (() => {
if (loginStatus === 302) {
navigate(defaultRoute);
navigate(defaultRoute)
}
}, [loginStatus]);
}, [loginStatus])
const onFinish = (values) => {
authenticate(values.username, values.password).catch((ex) => {
console.error(ex);
authenticate(values.username, values.password)
.catch(ex => {
console.error(ex)
notification.error({
message: t("Validation.Title"),
description: t("Validation.LoginFailed"),
placement: "top",
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">
<Row justify='center' align='middle' className='min-h-96'>
<Form
name="login"
layout="vertical"
name='login'
layout='vertical'
form={form}
size="large"
size='large'
labelCol={{
span: 8,
}}
wrapperCol={{
span: 24,
}}
className="max-w-xl"
className='max-w-xl'
initialValues={{
language: defaultLng,
}}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
autoComplete="off"
autoComplete='off'
>
<Form.Item
label={t("Username")}
name="username"
label={t('Username')}
name='username'
rules={[
{
required: true,
message: t("Validation.UsernameIsEmpty"),
message: t('Validation.UsernameIsEmpty'),
},
]}
>
<Input />
</Form.Item>
<Form.Item
label={t("Password")}
name="password"
label={t('Password')}
name='password'
rules={[
{
required: true,
message: t("Validation.PasswordIsEmpty"),
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>
<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 type='primary' htmlType='submit' className='w-full'>
{t('Login')}
</Button>
</Form.Item>
</Form>
</Row>
</>
);
)
}
export default Login;
export default Login

@ -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>China Highlights International Travel Service 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>

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

@ -3,7 +3,7 @@ import { PERM_ROLE_NEW } from '@/config'
import useAccountStore, { fetchPermissionList, fetchPermissionListByRoleId, fetchRoleList } from '@/stores/Account'
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>
@ -164,7 +164,7 @@ function RoleList() {
}}
title={t('account:detail')}
open={isRoleModalOpen} onCancel={() => setRoleModalOpen(false)}
destroyOnHidden
destroyOnClose
forceRender
modalRender={(dom) => (
<Form

@ -6,7 +6,7 @@ import * as config from '@/config';
import { getFeedbackDetail, getCustomerFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from '@/components/BackBtn';
import { usingStorage } from '@/hooks/usingStorage';
import {ImageUploader} from '@/components/ImageUploader';
const { Title, Text, Paragraph } = Typography;
function Detail() {
@ -21,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);
@ -178,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'}}/>
<Form.Item
name='info_content'
rules={[

@ -6,7 +6,6 @@ import * as config from "@/config";
import { getFeedbackDetail, getFeedbackImages, getFeedbackInfo, removeFeedbackImages, postFeedbackInfo } from '@/stores/Feedback';
import BackBtn from "@/components/BackBtn";
import { usingStorage } from "@/hooks/usingStorage";
import {ImageUploader} from '@/components/ImageUploader';
const { Title, Text, Paragraph } = Typography;
function Detail() {
@ -21,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);
@ -177,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} />
<Form.Item
name="info_content"
rules={[

@ -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 [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,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 '@/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) => {
{ 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>
);
const itemLink = isPermitted(PERM_PRODUCTS_OFFER_AUDIT) ? `/products/${travel_agency_id}/${use_year}/${audit_state}/edit` : isPermitted(PERM_PRODUCTS_OFFER_PUT) ? `/products/edit` : '';
return isNotEmpty(itemLink) ? <span onClick={() => setEditingProduct({info: r.info})}><Link to={itemLink} >{title}</Link></span> : title;
} },
// ...(productType === 'B' ? [{ key: 'km', dataIndex: ['info', 'km'], title: t('KM')}] : []),
{ key: 'adult', title: t('AgeType.Adult'), render: (_, { adult_cost, currency, unit_id, unit_name }) => `${adult_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
{ key: 'child', title: t('AgeType.Child'), render: (_, { child_cost, currency, unit_id, unit_name }) => `${child_cost} ${currency} / ${t(`PriceUnit.${unit_id}`)}` },
// {key: 'unit', title: t('Unit'), },
{
key: 'groupSize',
dataIndex: ['group_size_min'],
title: t('group_size'),
render: (_, { group_size_min, group_size_max }) => `${group_size_min} - ${group_size_max}`,
},
{
key: 'useDates',
dataIndex: ['use_dates_start'],
title: t('use_dates'),
render: (_, { use_dates_start, use_dates_end, weekdays }) => `${use_dates_start} ~ ${use_dates_end}`, // + (weekdays ? `, ${t('OnWeekdays')}${weekdays}` : ''),
},
...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'>
(Number(r.audit_state_id)) === 0 ? (
<RequireAuth subject={PERM_PRODUCTS_OFFER_AUDIT}>
{Number(r.audit_state_id) === 0 && (
<div className='flex gap-2'>
<Space>
<Button onClick={() => handleAuditPriceItem('2', r, ri)}></Button>
<Button onClick={() => handleAuditPriceItem('3', r, ri)}></Button>
</div>
)}
</RequireAuth>
<ProductQuotationLogPopover
method={'history'}
{...{ travel_agency_id, product_id: r.info.id, price_id: r.id, use_year }}
onOpenChange={(open) => setLogOpenPriceRow(open ? r.id : null)}
/>
</Space>
</>
</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]);

@ -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,5 +1,5 @@
import { useState, useEffect } from 'react';
import { App, Form, Modal, DatePicker, Divider, Switch, Space, Flex, Radio } from 'antd';
import { App, Form, Modal, DatePicker, Divider, Switch } from 'antd';
import { isEmpty, objectMapper } from '@/utils/commons';
import { useTranslation } from 'react-i18next';
@ -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} >
<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>
<Form.Item
name={'products_list'}
label={t('products:ProductName')} dependencies={['products_types']} hidden={showTypeOrItem!==2} >
<ProductsSelector params={{ travel_agency_id, use_year }} mode={'multiple'} placeholder={t('All')} />
</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')} />
{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>
)}
<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 }]}>
</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 }]}>
<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.Item>
</Flex>
</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) => {

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

@ -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 '@/utils/commons';
import { isEmpty, pick } from '@/utils/commons';
import ProductInfoForm from './ProductInfoForm';
import { usingStorage } from '@/hooks/usingStorage';
import Extras from './Extras';
@ -50,20 +50,13 @@ const ProductInfo = ({ ...props }) => {
setLgcEdits({});
setInfoEditStatus('');
setEditKeys([]);
return () => {};
}, [activeAgency, editingProduct]);
const [infoEditStatus, setInfoEditStatus] = useState('');
const [lgcEdits, setLgcEdits] = useState({});
// const [editChanged, setEditChanged] = useState({});
const [editKeys, setEditKeys] = useState([]);
const onValuesChange = (changedValues) => {
const onValuesChange = (changedValues, forms) => {
// 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);
if ('product_title' in changedValues) {
setInfoEditStatus('2');
setLgcEdits({...lgcEdits, '2': {'edit_status': '2'}});
@ -71,8 +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'}});
} else if ('quotation' in changedValues) {
// edit_status
} else {
setInfoEditStatus('2');
}
@ -80,10 +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);
// console.log("editKeys pre values", editKeys, editChanged, '\neditingProduct', );
const copyNewProduct = structuredClone(newProductRecord);
const poster = {
// ...(topPerm ? { } : { 'audit_state': -1 }), // :
@ -91,10 +78,9 @@ 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);
@ -109,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 '@/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={[
() => ({
@ -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,35 +370,12 @@ function getFields(props) {
return children;
}
const serverData2Form = (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 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 || '') },
@ -477,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

@ -1,487 +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,
} 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 PriceCompactInput from "@/views/products/Detail/PriceCompactInput";
import { formatGroupSize } from "@/hooks/useProductsSets";
const { RangePicker } = DatePicker;
const defaultPriceValue = {
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
};
const getYearRange = (year) => [
dayjs().year(year).startOf("y"),
dayjs().year(year).endOf("y"),
];
const generateDefinitionValue = (year) => ({
useDateList: [{ useDate: getYearRange(year) }],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [defaultPriceValue],
});
const ProductInfoQuotation = ({ editable, ...props }) => {
const { onChange } = props;
const { t } = useTranslation();
const [
quotationList,
newEmptyQuotation,
appendQuotationList,
saveOrUpdateQuotation,
deleteQuotation,
switchParams,
] = useProductsStore((state) => [
state.quotationList,
state.newEmptyQuotation,
state.appendQuotationList,
state.saveOrUpdateQuotation,
state.deleteQuotation,
state.switchParams,
]);
const { RangePicker } = DatePicker
const batchSetupInitialValues = {
defList: [
//
'defList': [
//
{
useDateList: [{ useDate: getYearRange(switchParams.use_year) }],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [
'useDateList': [
{
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
},
'useDate': [
dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')
]
}
],
'unitId': '0',
'currency': 'RMB',
'weekend': [
],
'priceList': [
{
priceInput: {
numberStart: 3,
numberEnd: 4,
audultPrice: 0,
childrenPrice: 0,
},
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
priceInput: {
numberStart: 5,
numberEnd: 6,
audultPrice: 0,
childrenPrice: 0,
},
'priceInput': {
'numberStart': 3,
'numberEnd': 4,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
priceInput: {
numberStart: 7,
numberEnd: 9,
audultPrice: 0,
childrenPrice: 0,
},
'priceInput': {
'numberStart': 5,
'numberEnd': 6,
'audultPrice': 0,
'childrenPrice': 0
}
},
],
{
'priceInput': {
'numberStart': 7,
'numberEnd': 9,
'audultPrice': 0,
'childrenPrice': 0
}
}
]
},
//
//
{
useDateList: [
'useDateList': [
{
useDate: [
dayjs().year(switchParams.use_year).subtract(2, "M").startOf("M"),
dayjs().year(switchParams.use_year).endOf("M"),
'useDate': [
dayjs().add(1, 'year').subtract(2, 'M').startOf('M'), dayjs().add(1, 'year').endOf('M')
]
}
],
},
'unitId': '0',
'currency': 'RMB',
'weekend': [
],
unitId: "0",
currency: "RMB",
weekend: [],
priceList: [
'priceList': [
{
priceInput: {
numberStart: 1,
numberEnd: 2,
audultPrice: 0,
childrenPrice: 0,
},
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
priceInput: {
numberStart: 3,
numberEnd: 4,
audultPrice: 0,
childrenPrice: 0,
},
'priceInput': {
'numberStart': 3,
'numberEnd': 4,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
priceInput: {
numberStart: 5,
numberEnd: 6,
audultPrice: 0,
childrenPrice: 0,
},
'priceInput': {
'numberStart': 5,
'numberEnd': 6,
'audultPrice': 0,
'childrenPrice': 0
}
},
{
priceInput: {
numberStart: 7,
numberEnd: 9,
audultPrice: 0,
childrenPrice: 0,
},
},
],
},
],
};
'priceInput': {
'numberStart': 7,
'numberEnd': 9,
'audultPrice': 0,
'childrenPrice': 0
}
}
]
}
]
}
const defaultPriceValue = {
'priceInput': {
'numberStart': 1,
'numberEnd': 2,
'audultPrice': 0,
'childrenPrice': 0
}
}
const defaultUseDate = {
'useDate': [dayjs().add(1, 'year').startOf('y'), dayjs().add(1, 'year').endOf('y')]
}
const defaultDefinitionValue = {
'useDateList': [defaultUseDate],
'unitId': '0',
'currency': 'RMB',
'weekend': [],
'priceList': [defaultPriceValue]
}
const ProductInfoQuotation = ({ editable, ...props }) => {
const { onChange } = props
const [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 { t } = useTranslation()
const datePresets = useDatePresets();
const [isQuotationModalOpen, setQuotationModalOpen] = useState(false)
const [isBatchSetupModalOpen, setBatchSetupModalOpen] = useState(false)
const { modal, notification } = App.useApp()
const [quotationForm] = Form.useForm()
const [batchSetupForm] = Form.useForm()
useEffect(() => {
setDefaultUseDates(getYearRange(switchParams.use_year));
setDefaultDefinitionValue(generateDefinitionValue(switchParams.use_year));
}, [switchParams]);
const datePresets = useDatePresets()
const [quotationList, newEmptyQuotation, appendQuotationList, saveOrUpdateQuotation, deleteQuotation] =
useProductsStore((state) => [state.quotationList, state.newEmptyQuotation, state.appendQuotationList, state.saveOrUpdateQuotation, state.deleteQuotation])
const triggerChange = (changedValue) => {
onChange?.(changedValue);
};
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: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: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) =>
formatGroupSize(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) => {
// const _rowEditable = [-1,3].includes(quotation.audit_state_id);
const _rowEditable = true; // test: 0
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>
<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>
@ -490,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>
@ -508,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 '@/utils/request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@/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 '@/utils/request';
import { formatGroupSize } from '@/hooks/useProductsSets';
import { isEmpty, isNotEmpty } from '@/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 '@/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(() => {
@ -78,23 +77,16 @@ const ProductsTree = ({ onNodeSelect, ...props }) => {
...ele,
title: ele.label,
key: ele.value,
children: (agencyProducts[ele.value] || []).reduce((arr, product) => {
children: (agencyProducts[ele.value] || []).map((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}`,
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}-${city.id}`,
key: product.info.id,
_raw: product,
isLeaf: true,
}));
return arr.concat(flatCityP);
}, []),
}}),
// ``
// _children: Object.keys(copyAgencyProducts[ele.value] || []).map(city => {
// return {
@ -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}

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

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 '@/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,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, Tag, App, Flex } from 'antd'
import { Row, Col, Space, Button, Table, Input, Typography, Modal, Tag, App } from 'antd'
import {
FileOutlined, 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
@ -52,10 +52,15 @@ function Detail() {
}
function attachmentRender(_, confirm) {
const attachmentKey = `GHH/${travelAgencyId}/${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>
)}
)}
</>
);
}
@ -66,7 +71,6 @@ function Detail() {
);
}
const navigate = useNavigate();
const [isModalOpen, setIsModalOpen] = useState(false);
const [confirmLoading, setConfirmLoading] = useState(false);
const [confirmText, setConfirmText] = useState('');
@ -129,7 +133,7 @@ function Detail() {
.finally(() => {
setDataLoading(false);
});
}, [reservationId]);
}, [reservationId, getReservationDetail, notification]);
return (
<>
@ -157,7 +161,14 @@ function Detail() {
/>
</Modal>
<Space direction='vertical' className='w-full'>
<Flex horizontal="true" align="flex-start" gap="middle"><Button type="text" icon={<ArrowLeftOutlined />} onClick={() => navigate(-1)} /><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'>
<iframe id='msdoc-iframe-reservation' title='msdoc-iframe-reservation'

@ -10,7 +10,7 @@ import { usingStorage } from "@/hooks/usingStorage";
import BackBtn from "@/components/BackBtn";
import { station_names } from "@/views/trainticket/station_name";
import { Upload } from "antd";
import { ImageUploader, ImageViewer,simple_encrypt } from "@/components/ImageUploader";
import ImageUploader from "@/components/ImageUploader";
const TrainticketPlan = props => {
const { coli_sn, gri_sn } = useParams();
@ -207,17 +207,8 @@ const TrainticketPlan = props => {
<Form.Item name="FlightNo" noStyle rules={[{ required: true, message: "请输入车次!" }]}>
<Input placeholder="车次" />
</Form.Item>
<Form.Item
name="TicketNo"
noStyle
rules={[
{ required: true, message: "请输入取票号!" },
{
pattern: /^[a-zA-Z0-9]{10}$/, // 10
message: "取票号必须为 10 个字母或数字",
},
]}>
<Input placeholder="取票号" maxLength={10} />
<Form.Item name="TicketNo" noStyle rules={[{ required: true, message: "请输入取票号!" }]}>
<Input placeholder="取票号" maxLength={9} />
</Form.Item>
</Space>
</Form.Item>
@ -526,12 +517,6 @@ const TrainticketPlan = props => {
</Button>
</Col>
</Row>
<Row>
<Divider orientation="center">客人护照</Divider>
<Col md={24} lg={24} xxl={24}>
<ImageViewer osskey={planDetail ? simple_encrypt(`ghh/${planDetail[0].GRI_SN}-${planDetail[0].GRI_No}/passport_image`) : ""} />
</Col>
</Row>
<Row>
<Divider orientation="center">{planDetail ? `${planDetail[0].GRI_No} - ${planDetail[0].WL}` : ""}</Divider>

@ -8,7 +8,6 @@ export default {
'text-muted',
'bg-red-100',
'bg-sky-100',
'bg-amber-100',
],
darkMode: 'media',
theme: {

@ -1,22 +1,23 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import legacy from "@vitejs/plugin-legacy";
import WindiCSS from 'vite-plugin-windicss';
import packageJson from './package.json';
import dayjs from 'dayjs'
import { execSync } from 'child_process';
const today = new dayjs().format('YYYY-MM-DD HH:mm:ss')
const gitHead = execSync('git rev-parse --short HEAD').toString().trim()
// https://vitejs.dev/config/
export default defineConfig({
define: {
__BUILD_DATE__: JSON.stringify(`${today}`),
__BUILD_VERSION__: JSON.stringify(`${packageJson.version}`),
__GIT_HEAD__: JSON.stringify(`${gitHead}`),
},
plugins: [
react(), WindiCSS(),
legacy({
targets: ["defaults", "not IE 11"],
}),
],
server: {
host: "0.0.0.0",

Loading…
Cancel
Save